@brunosps00/dev-workflow 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +7 -4
  4. package/lib/doctor.js +17 -2
  5. package/lib/init.js +16 -6
  6. package/lib/migrate-skills.js +189 -39
  7. package/lib/removed-bundled-skills.js +4 -2
  8. package/lib/skill-registry.js +12 -2
  9. package/lib/uninstall.js +61 -5
  10. package/package.json +1 -1
  11. package/scaffold/en/agent-instructions.md +1 -1
  12. package/scaffold/en/commands/dw-brainstorm.md +2 -2
  13. package/scaffold/en/commands/dw-bugfix.md +1 -1
  14. package/scaffold/en/commands/dw-commit.md +1 -1
  15. package/scaffold/en/commands/dw-dockerize.md +2 -2
  16. package/scaffold/en/commands/dw-functional-doc.md +1 -1
  17. package/scaffold/en/commands/dw-harness-audit.md +1 -1
  18. package/scaffold/en/commands/dw-intel.md +1 -1
  19. package/scaffold/en/commands/dw-learn.md +1 -1
  20. package/scaffold/en/commands/dw-new-project.md +3 -3
  21. package/scaffold/en/commands/dw-plan.md +2 -2
  22. package/scaffold/en/commands/dw-qa.md +6 -5
  23. package/scaffold/en/commands/dw-redesign-ui.md +3 -3
  24. package/scaffold/en/commands/dw-review.md +2 -2
  25. package/scaffold/en/commands/dw-skill-health.md +24 -1
  26. package/scaffold/en/commands/dw-update.md +28 -11
  27. package/scaffold/en/references/invariants.md +4 -0
  28. package/scaffold/en/templates/project-onepager.md +1 -1
  29. package/scaffold/pt-br/agent-instructions.md +1 -1
  30. package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
  31. package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
  32. package/scaffold/pt-br/commands/dw-commit.md +1 -1
  33. package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
  34. package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
  35. package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
  36. package/scaffold/pt-br/commands/dw-intel.md +1 -1
  37. package/scaffold/pt-br/commands/dw-learn.md +1 -1
  38. package/scaffold/pt-br/commands/dw-new-project.md +3 -3
  39. package/scaffold/pt-br/commands/dw-plan.md +2 -2
  40. package/scaffold/pt-br/commands/dw-qa.md +6 -5
  41. package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
  42. package/scaffold/pt-br/commands/dw-review.md +2 -2
  43. package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
  44. package/scaffold/pt-br/commands/dw-update.md +28 -11
  45. package/scaffold/pt-br/references/invariants.md +5 -0
  46. package/scaffold/pt-br/templates/project-onepager.md +1 -1
  47. package/scaffold/scripts/hooks/session-cost.mjs +62 -21
  48. package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
  49. package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
  50. package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
  51. package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
  52. package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
  53. package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
  54. package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
  55. package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
  56. package/scaffold/skills/dw-ui-discipline/references/responsive.md +44 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev-workflow",
3
3
  "description": "Standalone, à-la-carte skills from the dev-workflow pipeline: minimalism, search-first, simplification, verification, security review, and more.",
4
- "version": "2.6.0",
4
+ "version": "2.7.0",
5
5
  "author": {
6
6
  "name": "Bruno Santos"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -10,6 +10,78 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
10
  > those versions were released, so they are summaries of what shipped, not
11
11
  > contemporaneous release notes. `git log` remains the authoritative record.
12
12
 
13
+ ## [2.7.0] — 2026-11-09
14
+
15
+ ### Added
16
+
17
+ - **Content stress in `dw-ui-discipline`.** New `references/content-stress.md`: a catalog of
18
+ realistic worst-case values (long and one-letter names, unbreakable emails, `1 members`,
19
+ 1,000+ rows, RTL, emoji-first names), failure signatures with their CSS cause and fix, and
20
+ a per-field truncate/wrap/clamp rule. `/dw-qa` runs it in UI mode — worst-case data injected
21
+ at the data boundary (the project's mocks, or `page.route()`, never by editing the
22
+ component), checked at the real container width, 320px, 200% zoom and dark/RTL — and
23
+ `/dw-redesign-ui` runs it at validation. Adapted from `break-ui` in
24
+ [`emilkowalski/skills`](https://github.com/emilkowalski/skills) (MIT).
25
+ - **Mobile platform fixes** in `responsive.md`: hover gated by `(hover: hover) and
26
+ (pointer: fine)`, `100dvh`/`100svh`, 16px inputs instead of disabling zoom,
27
+ `touch-action: manipulation`, `overscroll-behavior`, `viewport-fit=cover` for safe areas,
28
+ `theme-color` per scheme, and the rule that none of it is verified in device emulation.
29
+ - **Command usage telemetry.** The `session-cost` hook records a `commands` object (reads of
30
+ `.dw/commands/<name>.md`, or a Skill call naming a command) beside `skills`.
31
+ `/dw-skill-health` reports per-command usage and classifies five command families as
32
+ `MERGE CANDIDATE`, `KEEP SEPARATE` or `TOO EARLY` (20 sessions or 30 days of window).
33
+ - `/dw-review` checks motion changes against the escalation triggers in `motion.md` and
34
+ data-rendering changes against the content-stress static signals.
35
+ - `/dw-redesign-ui` directions must diverge on a named axis (layout, density, personality,
36
+ motion, interaction model) and are named for it, never "Option A/B".
37
+ - **Eval-first in `dw-llm-eval`**, from LangChain's eval-driven development post: a four-line
38
+ behavior spec (when / does / must not / keeps working) and a baseline run on the current code
39
+ before the change, where new-behavior cases must fail; a `holdout` split (~20%) never tuned
40
+ against; and drift compared case by case — a case that passed and now fails blocks, whatever
41
+ the average says. `/dw-plan` orders the Eval Plan subtask before implementation; `/dw-qa --ai`
42
+ lists pass → fail flips and reports `holdout` apart from `dev`.
43
+ - **Eval runs are recorded** in `evals/runs/` and compared scenario by scenario; a change that
44
+ adds a dangerous surface adds its eval in the same change. New eval 7 covers deletion in
45
+ `update` and `uninstall`.
46
+
47
+ ### Changed
48
+
49
+ - **Bundled skills install to `.dw/skills/`** instead of `.agents/skills/`. Codex, Copilot and
50
+ OpenCode all scan `.agents/skills/`, so each listed the 27 bundled skills as model-invocable
51
+ next to the command wrappers; commands load them by path, and the tool indexes now hold only
52
+ the `dw-*` entry points. `update` retires a legacy copy from `.agents/skills/` only when it is
53
+ a dev-workflow copy — its `SKILL.md` carries the Structured Return contract and, when the
54
+ previous install state tracked files, is one of them — and leaves a same-named skill
55
+ installed from elsewhere in place, printed as `[kept]`; `uninstall` follows the same rule and
56
+ `doctor` warns about leftovers. Commands, agent instructions and the `/dw-update` snapshot
57
+ point at the new location.
58
+ - **`update` and `uninstall` no longer delete what they retire.** Legacy skill copies, removed
59
+ skills, orphan `dw-*` wrappers and removed command sources are moved to
60
+ `.dw/.migrated/<timestamp>/` with their relative paths, and files the team added inside a
61
+ retired copy are named in the output. A terminal `npx … update` takes no snapshot, so this is
62
+ the copy those users keep. The migration now runs after the new skills are copied, so an
63
+ interrupted update leaves the old copies in place.
64
+ - **`motion.md` rewritten.** Frequency decides before easing (keyboard-triggered and 100+/day
65
+ actions do not animate); `ease-in` is never used on UI, exits included (the previous rule
66
+ said exits ease in); no `scale(0)`, no `transition: all`; trigger-anchored
67
+ `transform-origin`; transitions over keyframes for rapid triggers; property cost tiers;
68
+ `will-change` only after a measured problem; a removal gate with its hazards. Springs are
69
+ allowed only for gesture-driven motion. Adapted from `emilkowalski/skills` (MIT).
70
+ - Reduced motion means fewer and gentler animations, not zero; the accessibility floor adds
71
+ item 11 (zoom is never disabled; content survives 200% zoom).
72
+
73
+ ### Fixed
74
+
75
+ - `/dw-update --rollback` copied the `.agents/skills` snapshot into the existing folder,
76
+ nesting it as `.agents/skills/agents-skills` instead of restoring it.
77
+ - A command wrapper under `.claude/skills/` was counted as a skill use in `costs.jsonl`.
78
+ - `/dw-update --rollback` merged the snapshot over the current files, so files added by the
79
+ newer release survived it; managed `.dw/` folders are now replaced, and the snapshot also
80
+ saves and restores `.dw/install-state.json`. `/dw-update` checks the installed version
81
+ against the CLI's `Installed version` line instead of accepting the old state file.
82
+ - `uninstall` removed `.codex/skills/` wholesale; it now removes only `dw-*` entries and
83
+ dev-workflow skill copies there.
84
+
13
85
  ## [2.6.0] — 2026-10-07
14
86
 
15
87
  ### Added
package/README.md CHANGED
@@ -13,7 +13,7 @@ This will:
13
13
  2. Create `.dw/commands/` with workflow commands
14
14
  3. Create `.dw/templates/` with document templates (PRD, TechSpec, Tasks, ADR, etc.)
15
15
  4. Create `.dw/rules/` (populated by `/dw-analyze-project`) + a curated `.dw/rules-library/` declarative baseline per stack (loaded lazily via `.dw/config/stack-mappings.json`)
16
- 5. Install bundled skills (`dw-verify`, `dw-memory`, `dw-review-rigor`, `dw-ui-discipline`, `dw-testing-discipline`, `security-review`, etc.) to `.agents/skills/`
16
+ 5. Install bundled skills (`dw-verify`, `dw-memory`, `dw-review-rigor`, `dw-ui-discipline`, `dw-testing-discipline`, `security-review`, etc.) to `.dw/skills/` — outside every agent's skill index; commands load them by path
17
17
  6. Generate skill wrappers and project agents for Claude Code, Codex, Copilot, and OpenCode
18
18
  7. Configure MCP servers (Context7 + Playwright)
19
19
  8. Install enforcement hooks (git guardrails + a `SessionEnd` token-cost tracker → `.dw/metrics/costs.jsonl`) + a statusline (branch · active spec · minimalism mode · today's estimated spend) into `.claude/settings.json`, and the git guardrails into `.codex/hooks.json` for Codex (merge-aware — your own hooks/statusline are never overwritten; Codex runs project hooks only after you trust them)
@@ -226,6 +226,7 @@ your-project/
226
226
  │ │ └── concerns.md # Risk map: hot spots, fragile integrations, bug history (Step 9 of /dw-analyze-project)
227
227
  │ ├── constitution.md # Declarative principles (auto-installed when missing)
228
228
  │ ├── skill-registry.json # Local skill taxonomy, triggers, owners, load policy, and context limits
229
+ │ ├── skills/ # Bundled skills, loaded by path from commands (not listed in any agent's skill index)
229
230
  │ ├── references/ # Reference documentation
230
231
  │ ├── scripts/ # Utility scripts
231
232
  │ ├── spec/ # PRD directories — each contains tasks-validation.md
@@ -241,7 +242,7 @@ your-project/
241
242
  │ ├── skills/ # Claude Code wrappers
242
243
  │ ├── agents/ # Claude Code project agents
243
244
  │ └── settings.json # MCP servers (Context7, Playwright)
244
- ├── .agents/skills/ # Codex/Copilot wrappers + bundled skills
245
+ ├── .agents/skills/ # Codex/Copilot wrappers (bundled skills live in .dw/skills/)
245
246
  ├── .agents/agents/ # Codex/Copilot fallback agent definitions
246
247
  ├── .github/agents/ # GitHub Copilot custom agents
247
248
  ├── .opencode/commands/ # OpenCode wrappers
@@ -274,7 +275,7 @@ npx @brunosps00/dev-workflow subtask list
274
275
 
275
276
  ## Bundled Skills
276
277
 
277
- Skills installed to `.agents/skills/` for use by all commands. The bundle is governed by `scaffold/skill-registry.json`, which records each skill's kind, tier, owner, trigger, expected output, load policy, context limit, invocation control (`model` vs `explicit`), and à-la-carte `exportable` flag.
278
+ Skills installed to `.dw/skills/` for use by all commands. They sit outside the directories Codex, Copilot, OpenCode and Claude Code scan for model-invocable skills (`.agents/skills/`, `.claude/skills/`, `.github/skills/`, `.opencode/skills/`), so each tool's skill index lists only the `dw-*` command entry points; commands read the skills they need by path. Releases up to 2.6.0 installed them under `.agents/skills/`; `update` retires those copies once the new ones are in place, moving them to `.dw/.migrated/<timestamp>/` rather than deleting them, and leaves a same-named skill you installed from elsewhere where it is. The bundle is governed by `scaffold/skill-registry.json`, which records each skill's kind, tier, owner, trigger, expected output, load policy, context limit, invocation control (`model` vs `explicit`), and à-la-carte `exportable` flag.
278
279
 
279
280
  Skill kinds:
280
281
 
@@ -378,6 +379,8 @@ Spec-Driven Development patterns — declarative constitution (`.dw/constitution
378
379
 
379
380
  UI discipline (`dw-ui-discipline`) and testing doctrine (`dw-testing-discipline`) are original works in this repository. Earlier dev-workflow versions (≤0.13.x) drew on `pedronauck/skills` `ui-craft` and `testing-boss` as inspiration, but in v0.14.0 those skills were rewritten clean-room after a license audit confirmed that the upstream repo has no explicit LICENSE file at root — the README's MIT claim is unverified. The underlying ideas (grounding before design; behavior over mocks; mutation over coverage) are widely-documented general software engineering principles available in many sources (Beck, Fowler, Meszaros, Feathers, Google SRE Book, WCAG specifications). Browser-DevTools patterns from [`addyosmani/agent-skills`](https://github.com/addyosmani/agent-skills) (MIT) live inside `dw-testing-discipline/references/` (`security-boundary.md`, `three-workflow-patterns.md`).
380
381
 
382
+ The motion, mobile-web and content-stress references in `dw-ui-discipline` (`motion.md`, the "Mobile platform fixes" section of `responsive.md`, `content-stress.md`) adapt rules from [`emilkowalski/skills`](https://github.com/emilkowalski/skills) by Emil Kowalski (MIT): `emil-design-eng`, `review-animations`, `animation-performance`, `remove-animations`, `mobile-native` and `break-ui`. See [docs/skills-ecosystem-comparison.md](docs/skills-ecosystem-comparison.md), section 15.
383
+
381
384
  Minimalism discipline (`dw-minimalism`), per-skill invocation control (`disable-model-invocation` on hidden/internal command wrappers), the git-guardrails enforcement hook, and à-la-carte plugin distribution were adapted from [`DietrichGebert/ponytail`](https://github.com/DietrichGebert/ponytail) (MIT — the "lazy senior" decision ladder, intensity modes, and statusline) and [`mattpocock/skills`](https://github.com/mattpocock/skills) (MIT — user-vs-model invocation and `git-guardrails-claude-code`). See [docs/skills-ecosystem-comparison.md](docs/skills-ecosystem-comparison.md) for the full gap analysis and what was and was not adopted.
382
385
 
383
386
  Incident response (`dw-incident-response`) adapted from [`wilsto/claude-code-starter-kit/incident-response`](https://github.com/wilsto/claude-code-starter-kit) (MIT). The 5-phase workflow structure and runbook templates come from there. wilsto credits the upstream `wshobson/agents` plugin `incident-response` (v1.3.0); attribution chain preserved. Additional reading cited in the skill: Google SRE Book, Etsy Debriefing Facilitation Guide, PagerDuty Incident Response Documentation.
@@ -431,7 +434,7 @@ v1.0.0 consolidated the legacy command surface into the current `dw-*` pipeline.
431
434
  On the next `npx @brunosps00/dev-workflow update`, the following happens automatically:
432
435
 
433
436
  1. The 15 renamed commands' wrappers are detected as orphans (their names no longer exist in `lib/constants.js`) and removed from `.claude/skills/`, `.agents/skills/`, and `.opencode/commands/`.
434
- 2. The migrator prints each removal with the friendly "old → new" mapping (e.g., `[orphan] Removing wrapper .agents/skills/dw-create-prd (replaced in v1.0.0 by 'dw-plan prd')`).
437
+ 2. The migrator prints each removal with the friendly "old → new" mapping (e.g., `[orphan] Retiring wrapper .agents/skills/dw-create-prd (replaced in v1.0.0 by 'dw-plan prd')`).
435
438
  3. The current command wrappers are installed for the consolidated commands.
436
439
  4. The bundled skills are refreshed.
437
440
 
package/lib/doctor.js CHANGED
@@ -9,7 +9,14 @@ const {
9
9
  AGENT_TARGETS,
10
10
  } = require('./agents');
11
11
  const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
12
- const { validateSkillRegistry, readSkillRegistry, validateSkillStructuredReturn } = require('./skill-registry');
12
+ const {
13
+ validateSkillRegistry,
14
+ readSkillRegistry,
15
+ validateSkillStructuredReturn,
16
+ BUNDLED_SKILLS_REL,
17
+ LEGACY_BUNDLED_SKILLS_REL,
18
+ } = require('./skill-registry');
19
+ const { isDevWorkflowSkillCopy } = require('./migrate-skills');
13
20
  const { GIT_GUARDRAILS_SCRIPT, GIT_GUARDRAILS_CMD, STATUSLINE_CMD, HOOKS_DIR_MARKER, CODEX_HOOKS_FILE } = require('./hooks');
14
21
  const removedCommands = require('./removed-commands');
15
22
 
@@ -177,12 +184,20 @@ function checkSkillRegistry(projectRoot, issues, warnings) {
177
184
  const registry = readSkillRegistry();
178
185
  for (const entry of registry.skills) {
179
186
  if (!entry.bundled) continue;
180
- const skillPath = path.join(projectRoot, '.agents', 'skills', entry.name, 'SKILL.md');
187
+ const skillPath = path.join(projectRoot, BUNDLED_SKILLS_REL, entry.name, 'SKILL.md');
181
188
  if (!fs.existsSync(skillPath)) {
182
189
  issues.push(`Missing bundled skill: ${path.relative(projectRoot, skillPath)}`);
183
190
  } else {
184
191
  validateSkillStructuredReturn(entry.name, skillPath, issues, `Installed bundled skill ${entry.name}`);
185
192
  }
193
+ // A copy left under .agents/skills/ by a release up to 2.6.0 is still listed
194
+ // as model-invocable by Codex, Copilot and OpenCode. `update` removes it.
195
+ const legacySkillPath = path.join(projectRoot, LEGACY_BUNDLED_SKILLS_REL, entry.name, 'SKILL.md');
196
+ if (isDevWorkflowSkillCopy(path.dirname(legacySkillPath))) {
197
+ warnings.push(
198
+ `Legacy bundled skill copy at ${path.relative(projectRoot, path.dirname(legacySkillPath))} is still in agent skill indexes; run update to move it to ${BUNDLED_SKILLS_REL}/`
199
+ );
200
+ }
186
201
  }
187
202
  }
188
203
 
package/lib/init.js CHANGED
@@ -12,6 +12,7 @@ const { hasExistingInstall, readInstallState, writeInstallState, listManagedFile
12
12
  const { resolveInstallSelection } = require('./profiles');
13
13
  const { ensureSubtaskLayout } = require('./subtasks');
14
14
  const { printPostUpdateActions } = require('./post-update');
15
+ const { BUNDLED_SKILLS_REL } = require('./skill-registry');
15
16
 
16
17
  const SCAFFOLD_DIR = path.join(__dirname, '..', 'scaffold');
17
18
 
@@ -33,11 +34,10 @@ async function run({ force = false, lang = null, mode = 'init', profile = null,
33
34
  console.log(' Existing dev-workflow install detected; reconciling managed files.\n');
34
35
  }
35
36
 
36
- // 0. GSD migration (idempotent; only runs if legacy GSD artifacts are detected)
37
- // 0.1. Skill migration: removed bundled skills + orphan dw-* wrappers (idempotent)
37
+ // 0. GSD migration (idempotent; only runs if legacy GSD artifacts are detected).
38
+ // The skill migration runs later (5.55), once the new copies exist.
38
39
  if (shouldReconcile) {
39
40
  migrateGsd(projectRoot);
40
- migrateSkills(projectRoot);
41
41
  }
42
42
 
43
43
  // 1. Select language
@@ -332,22 +332,32 @@ async function run({ force = false, lang = null, mode = 'init', profile = null,
332
332
  ensureSubtaskLayout(projectRoot);
333
333
  touchedFiles.push(path.join(projectRoot, '.dw', 'subtasks', '.gitignore'));
334
334
 
335
- // 5.5. Copy bundled skills to .agents/skills/
335
+ // 5.5. Copy bundled skills to .dw/skills/ — outside every agent CLI's skill
336
+ // index; commands load them by path. Legacy copies under .agents/skills/ are
337
+ // retired by migrateSkills() (step 5.55) on update/repair/re-init.
336
338
  const skillsSrcDir = path.join(SCAFFOLD_DIR, 'skills');
337
339
  if (require('fs').existsSync(skillsSrcDir)) {
338
340
  console.log(' Bundled skills:');
339
341
  const skillsResults = copyDir(
340
342
  skillsSrcDir,
341
- path.join(projectRoot, '.agents', 'skills'),
343
+ path.join(projectRoot, BUNDLED_SKILLS_REL),
342
344
  managedForce
343
345
  );
344
346
  totalCreated += skillsResults.created;
345
347
  totalSkipped += skillsResults.skipped;
346
348
  totalOverwritten += skillsResults.overwritten;
347
- touchedFiles.push(...listFilesRecursive(path.join(projectRoot, '.agents', 'skills')));
349
+ touchedFiles.push(...listFilesRecursive(path.join(projectRoot, BUNDLED_SKILLS_REL)));
348
350
  console.log(` ${skillsResults.created} created, ${skillsResults.skipped} skipped, ${skillsResults.overwritten} overwritten\n`);
349
351
  }
350
352
 
353
+ // 5.55. Skill migration: removed bundled skills, legacy .agents/skills copies,
354
+ // orphan dw-* wrappers and removed command sources. Runs after the copy above
355
+ // so an interrupted update never leaves neither copy, and moves what it retires
356
+ // to .dw/.migrated/<ts>/ instead of deleting it (idempotent).
357
+ if (shouldReconcile) {
358
+ migrateSkills(projectRoot, { priorState });
359
+ }
360
+
351
361
  // 5.6. Generate platform-native/fallback agents from the selected modules.
352
362
  console.log(' Agents:');
353
363
  const agentResults = generateAgents(projectRoot, selection.modules, managedForce);
@@ -3,44 +3,88 @@ const path = require('path');
3
3
  const { COMMANDS } = require('./constants');
4
4
  const removedSkills = require('./removed-bundled-skills');
5
5
  const removedCommands = require('./removed-commands');
6
+ const { BUNDLED_SKILLS_REL, LEGACY_BUNDLED_SKILLS_REL } = require('./skill-registry');
7
+
8
+ const MIGRATED_REL = path.join('.dw', '.migrated');
6
9
 
7
10
  /**
8
11
  * Cleanup-on-update for dev-workflow.
9
12
  *
10
- * Two complementary scans:
13
+ * Nothing is deleted outright: every path this module retires is MOVED to
14
+ * `.dw/.migrated/<timestamp>/<original relative path>`, and the destination is
15
+ * printed. A direct `npx … update` from a terminal takes no snapshot (only the
16
+ * `/dw-update` agent command does), so this move is the only copy those users get.
17
+ *
18
+ * Scans:
11
19
  *
12
20
  * 1. Removed bundled skills (data-driven from `removed-bundled-skills.js`):
13
- * Detects `.agents/skills/<name>/` directories that match the manifest of
14
- * skills removed in past releases. Removes them and notes the replacement.
21
+ * `.dw/skills/<name>/` and legacy `.agents/skills/<name>/` directories named in
22
+ * the manifest of skills removed in past releases. Matched by name only: a
23
+ * same-named skill from elsewhere is moved too, which is why it is a move.
15
24
  *
16
- * 2. Orphan command wrappers (state-comparison vs `constants.js`):
17
- * Lists `.claude/skills/dw-*`, `.agents/skills/dw-*`, and
18
- * `.opencode/commands/dw-*.md` wrappers on disk. Any `dw-*` wrapper whose
19
- * base name is NOT present in the current `COMMANDS[lang]` is removed.
25
+ * 1.5. Relocated bundled skills: releases up to 2.6.0 installed the bundled
26
+ * skills under `.agents/skills/`, where Codex, Copilot and OpenCode list them
27
+ * as model-invocable. They now live in `.dw/skills/`. A legacy copy is retired
28
+ * only when `classifyLegacySkill` says it is ours; otherwise it is kept and
29
+ * reported. Files in an ours-copy that dev-workflow never installed are named
30
+ * in the output so the team can bring them back from the move destination.
20
31
  *
21
- * Only touches names with prefix `dw-` (or names explicitly listed in the
22
- * manifest). Never touches third-party skills/commands.
32
+ * 2. Orphan command wrappers (state-comparison vs `constants.js`):
33
+ * `.claude/skills/dw-*`, `.agents/skills/dw-*` and `.opencode/commands/dw-*.md`
34
+ * whose base name is NOT a current command or bundled skill. `dw-` is this
35
+ * package's namespace; a user skill named `dw-*` is moved, not lost.
23
36
  *
24
37
  * Idempotent: silent when nothing to clean.
25
38
  *
26
- * Run automatically from lib/init.js when mode === 'update'.
39
+ * Run from lib/init.js on update/repair/re-init, AFTER the bundled skills were
40
+ * copied to `.dw/skills/`, so an interrupted update never leaves a project with
41
+ * neither copy.
27
42
  */
28
- function migrate(projectRoot) {
43
+ function migrate(projectRoot, { priorState = null, stamp = timestamp() } = {}) {
44
+ const managed = managedSet(priorState);
29
45
  const removed = scanRemovedBundledSkills(projectRoot);
46
+ const relocated = scanRelocatedBundledSkills(projectRoot, managed);
30
47
  const orphans = scanOrphanCommandWrappers(projectRoot);
31
48
  const commandSources = scanRemovedCommandSources(projectRoot);
32
49
 
33
- if (removed.length === 0 && orphans.length === 0 && commandSources.length === 0) {
50
+ if (
51
+ removed.length === 0 &&
52
+ relocated.length === 0 &&
53
+ orphans.length === 0 &&
54
+ commandSources.length === 0
55
+ ) {
34
56
  return null; // silent — nothing to clean
35
57
  }
36
58
 
37
59
  console.log('\n Skill migration:');
60
+ let moved = 0;
61
+ const retire = (target) => {
62
+ moveToBackup(projectRoot, target, stamp);
63
+ moved += 1;
64
+ };
38
65
 
39
66
  for (const entry of removed) {
40
67
  console.log(
41
- ` \x1b[33m-\x1b[0m [v${entry.removedIn}] Removing legacy skill ${entry.name} (replaced by ${entry.replacedBy})`
68
+ ` \x1b[33m-\x1b[0m [v${entry.removedIn}] Retiring legacy skill ${path.relative(projectRoot, entry.path)} (replaced by ${entry.replacedBy})`
42
69
  );
43
- rmRecursive(entry.path);
70
+ retire(entry.path);
71
+ }
72
+
73
+ let relocatedCount = 0;
74
+ for (const entry of relocated) {
75
+ const rel = path.relative(projectRoot, entry.path);
76
+ if (entry.ours) {
77
+ console.log(` \x1b[33m-\x1b[0m [moved] Retiring ${rel} (bundled skills now live in ${BUNDLED_SKILLS_REL}/)`);
78
+ for (const file of entry.foreign) {
79
+ console.log(` ! ${path.join(rel, file)} was not installed by dev-workflow; it is in the backup below`);
80
+ }
81
+ retire(entry.path);
82
+ relocatedCount += 1;
83
+ } else {
84
+ console.log(
85
+ ` \x1b[36m!\x1b[0m [kept] ${rel} has a bundled skill's name but is not a dev-workflow copy (${entry.reason}); left in place`
86
+ );
87
+ }
44
88
  }
45
89
 
46
90
  // Pre-build a map { commandName -> { removedIn, replacedBy } } for friendly messaging.
@@ -53,58 +97,143 @@ function migrate(projectRoot) {
53
97
  const suffix = known
54
98
  ? `(replaced in v${known.removedIn} by \`${known.replacedBy}\`)`
55
99
  : `(command no longer exists)`;
56
- console.log(` \x1b[33m-\x1b[0m [orphan] Removing wrapper ${rel} ${suffix}`);
57
- rmRecursive(entry.path);
100
+ console.log(` \x1b[33m-\x1b[0m [orphan] Retiring wrapper ${rel} ${suffix}`);
101
+ retire(entry.path);
58
102
  }
59
103
 
60
104
  for (const entry of commandSources) {
61
105
  const rel = path.relative(projectRoot, entry.path);
62
106
  console.log(
63
- ` \x1b[33m-\x1b[0m [legacy] Removing command source ${rel} (replaced in v${entry.removedIn} by \`${entry.replacedBy}\`)`
107
+ ` \x1b[33m-\x1b[0m [legacy] Retiring command source ${rel} (replaced in v${entry.removedIn} by \`${entry.replacedBy}\`)`
64
108
  );
65
- rmRecursive(entry.path);
109
+ retire(entry.path);
66
110
  }
67
111
 
112
+ if (moved > 0) {
113
+ console.log(` Moved ${moved} path(s) to ${path.join(MIGRATED_REL, stamp)}/ — copy back anything you want to keep.`);
114
+ }
68
115
  console.log();
69
- return { removed: removed.length, orphans: orphans.length, commandSources: commandSources.length };
116
+ return {
117
+ removed: removed.length,
118
+ relocated: relocatedCount,
119
+ orphans: orphans.length,
120
+ commandSources: commandSources.length,
121
+ backup: moved > 0 ? path.join(projectRoot, MIGRATED_REL, stamp) : null,
122
+ };
70
123
  }
71
124
 
72
125
  function scanRemovedBundledSkills(projectRoot) {
73
126
  const matches = [];
74
- const agentsSkillsDir = path.join(projectRoot, '.agents', 'skills');
127
+ for (const rel of [BUNDLED_SKILLS_REL, LEGACY_BUNDLED_SKILLS_REL]) {
128
+ const skillsDir = path.join(projectRoot, rel);
129
+ if (!fs.existsSync(skillsDir)) continue;
130
+ for (const entry of removedSkills) {
131
+ const target = path.join(skillsDir, entry.name);
132
+ if (fs.existsSync(target)) {
133
+ matches.push({ ...entry, path: target });
134
+ }
135
+ }
136
+ }
137
+ return matches;
138
+ }
75
139
 
76
- if (!fs.existsSync(agentsSkillsDir)) return matches;
140
+ function scanRelocatedBundledSkills(projectRoot, managed) {
141
+ const matches = [];
142
+ const legacyDir = path.join(projectRoot, LEGACY_BUNDLED_SKILLS_REL);
143
+ if (!fs.existsSync(legacyDir)) return matches;
77
144
 
78
- for (const entry of removedSkills) {
79
- const target = path.join(agentsSkillsDir, entry.name);
80
- if (fs.existsSync(target)) {
81
- matches.push({ ...entry, path: target });
145
+ for (const name of listScaffoldSkillDirs()) {
146
+ const target = path.join(legacyDir, name);
147
+ let stat;
148
+ try {
149
+ stat = fs.statSync(target);
150
+ } catch {
151
+ continue;
82
152
  }
153
+ if (!stat.isDirectory()) continue;
154
+ matches.push({ name, path: target, ...classifyLegacySkill(projectRoot, name, managed) });
83
155
  }
84
156
  return matches;
85
157
  }
86
158
 
159
+ /**
160
+ * Is `.agents/skills/<name>/` the copy a release up to 2.6.0 installed?
161
+ *
162
+ * - Its SKILL.md must carry the Structured Return contract every bundled skill
163
+ * has (`**Status:**` and `**Next Step:**`). Without it: not ours.
164
+ * - When the prior install state tracked files, its SKILL.md must be one of
165
+ * them. A same-named skill installed after the last update is not, even if its
166
+ * SKILL.md happens to use the same two labels.
167
+ * - With no tracked files (installs older than install-state), the contract
168
+ * check alone decides.
169
+ *
170
+ * For an ours-copy, `foreign` lists files dev-workflow neither tracked nor ships
171
+ * (a team's addition, or a file from an older release), relative to the dir.
172
+ */
173
+ function classifyLegacySkill(projectRoot, name, managed = new Set()) {
174
+ const dir = path.join(projectRoot, LEGACY_BUNDLED_SKILLS_REL, name);
175
+ if (!isDevWorkflowSkillCopy(dir)) return { ours: false, reason: 'no Structured Return contract', foreign: [] };
176
+ const relDir = path.join(LEGACY_BUNDLED_SKILLS_REL, name);
177
+ if (managed.size > 0 && !managed.has(path.join(relDir, 'SKILL.md'))) {
178
+ return { ours: false, reason: 'not in the previous install state', foreign: [] };
179
+ }
180
+ const scaffoldDir = path.join(__dirname, '..', 'scaffold', 'skills', name);
181
+ const foreign = listFiles(dir).filter(
182
+ (file) => !managed.has(path.join(relDir, file)) && !fs.existsSync(path.join(scaffoldDir, file))
183
+ );
184
+ return { ours: true, reason: null, foreign };
185
+ }
186
+
187
+ // Every bundled SKILL.md carries the Structured Return contract (enforced by
188
+ // lib/skill-registry.js). Necessary, not sufficient: see classifyLegacySkill.
189
+ function isDevWorkflowSkillCopy(skillDir) {
190
+ let content;
191
+ try {
192
+ content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
193
+ } catch {
194
+ return false;
195
+ }
196
+ return content.includes('**Status:**') && content.includes('**Next Step:**');
197
+ }
198
+
199
+ function managedSet(state) {
200
+ return new Set(state && Array.isArray(state.managed_files) ? state.managed_files : []);
201
+ }
202
+
203
+ function listFiles(dir, base = dir) {
204
+ const out = [];
205
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
206
+ const full = path.join(dir, entry.name);
207
+ if (entry.isDirectory()) out.push(...listFiles(full, base));
208
+ else out.push(path.relative(base, full));
209
+ }
210
+ return out;
211
+ }
212
+
213
+ function listScaffoldSkillDirs() {
214
+ const scaffoldSkillsDir = path.join(__dirname, '..', 'scaffold', 'skills');
215
+ if (!fs.existsSync(scaffoldSkillsDir)) return [];
216
+ return fs
217
+ .readdirSync(scaffoldSkillsDir, { withFileTypes: true })
218
+ .filter((entry) => entry.isDirectory())
219
+ .map((entry) => entry.name);
220
+ }
221
+
87
222
  function scanOrphanCommandWrappers(projectRoot) {
88
223
  const orphans = [];
89
224
 
90
225
  // Build the canonical "known names" set:
91
226
  // - Commands from constants.js (en + pt-br union) — these become wrappers.
92
- // - Bundled skills from scaffold/skills/ — these are copied verbatim into
93
- // .agents/skills/ and .claude/skills/ alongside wrappers. They're NOT
94
- // orphans even though they share the .agents/skills/ directory with
95
- // command wrappers.
227
+ // - Bundled skill names from scaffold/skills/ — they now live in .dw/skills/,
228
+ // and a legacy copy under .agents/skills/ is handled by the relocation
229
+ // scan, not reported here as an orphan wrapper.
96
230
  const canonical = new Set();
97
231
  for (const lang of Object.keys(COMMANDS)) {
98
232
  for (const cmd of COMMANDS[lang]) {
99
233
  canonical.add(cmd.name);
100
234
  }
101
235
  }
102
- const scaffoldSkillsDir = path.join(__dirname, '..', 'scaffold', 'skills');
103
- if (fs.existsSync(scaffoldSkillsDir)) {
104
- for (const entry of fs.readdirSync(scaffoldSkillsDir, { withFileTypes: true })) {
105
- if (entry.isDirectory()) canonical.add(entry.name);
106
- }
107
- }
236
+ for (const name of listScaffoldSkillDirs()) canonical.add(name);
108
237
 
109
238
  // Scan locations
110
239
  const locations = [
@@ -152,9 +281,30 @@ function scanRemovedCommandSources(projectRoot) {
152
281
  return matches;
153
282
  }
154
283
 
155
- function rmRecursive(target) {
156
- if (!fs.existsSync(target)) return;
157
- fs.rmSync(target, { recursive: true, force: true });
284
+ function timestamp() {
285
+ return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
286
+ }
287
+
288
+ // Move a retired path under .dw/.migrated/<stamp>/, keeping its relative path.
289
+ function moveToBackup(projectRoot, target, stamp = timestamp()) {
290
+ if (!fs.existsSync(target)) return null;
291
+ const dest = path.join(projectRoot, MIGRATED_REL, stamp, path.relative(projectRoot, target));
292
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
293
+ try {
294
+ fs.renameSync(target, dest);
295
+ } catch {
296
+ fs.cpSync(target, dest, { recursive: true });
297
+ fs.rmSync(target, { recursive: true, force: true });
298
+ }
299
+ return dest;
158
300
  }
159
301
 
160
- module.exports = { migrate };
302
+ module.exports = {
303
+ migrate,
304
+ isDevWorkflowSkillCopy,
305
+ classifyLegacySkill,
306
+ managedSet,
307
+ moveToBackup,
308
+ timestamp,
309
+ MIGRATED_REL,
310
+ };
@@ -5,8 +5,10 @@
5
5
  * this manifest forever so old installs still get cleaned up when the
6
6
  * user runs `dev-workflow update`.
7
7
  *
8
- * Each entry triggers `.agents/skills/<name>/` removal during update
9
- * (see lib/migrate-skills.js).
8
+ * Each entry retires `.dw/skills/<name>/` and `.agents/skills/<name>/` during
9
+ * update by moving them to `.dw/.migrated/<timestamp>/` (see lib/migrate-skills.js).
10
+ * The match is by name only, so the move is what keeps a same-named skill from
11
+ * elsewhere recoverable.
10
12
  */
11
13
  module.exports = [
12
14
  // v0.13.0 — replaced TASTE catalog (161 palettes) with discipline (hard-gate + anti-slop + WCAG floor)
@@ -6,11 +6,19 @@ const { readJson } = require('./utils');
6
6
  const SCAFFOLD_DIR = path.join(__dirname, '..', 'scaffold');
7
7
  const REGISTRY_PATH = path.join(SCAFFOLD_DIR, 'skill-registry.json');
8
8
  const SKILLS_DIR = path.join(SCAFFOLD_DIR, 'skills');
9
+ // Where a consumer project receives the bundled skills. Deliberately outside every
10
+ // directory an agent CLI scans for model-invocable skills (`.claude/skills`,
11
+ // `.agents/skills`, `.github/skills`, `.opencode/skills`): commands load these by
12
+ // path, so listing them in each tool's skill index only crowded the choice the
13
+ // model makes there. Releases up to 2.6.0 installed them under `.agents/skills/`;
14
+ // `migrate-skills.js` moves those out on update.
15
+ const BUNDLED_SKILLS_REL = path.join('.dw', 'skills');
16
+ const LEGACY_BUNDLED_SKILLS_REL = path.join('.agents', 'skills');
9
17
 
10
18
  const VALID_KINDS = new Set(['protocol', 'domain-pack', 'recipe-pack', 'asset-pack']);
11
19
  const VALID_LOAD_POLICIES = new Set(['always-small', 'lazy-references', 'opt-in-runtime']);
12
20
  // Optional invocation control (mattpocock/skills). Bundled skills live under
13
- // .agents/skills/ (read by path from command prose), so this is documental for
21
+ // .dw/skills/ (read by path from command prose), so this is documental for
14
22
  // them and feeds the à-la-carte plugin manifest. Default when absent: 'model'.
15
23
  const VALID_INVOCATIONS = new Set(['model', 'explicit']);
16
24
  const REQUIRED_STRUCTURED_RETURN_FIELDS = [
@@ -150,7 +158,7 @@ function validateSkillStructuredReturn(skillName, skillPath, issues, label = `Sk
150
158
  }
151
159
 
152
160
  function listInstalledSkillNames(projectRoot) {
153
- const installedDir = path.join(projectRoot, '.agents', 'skills');
161
+ const installedDir = path.join(projectRoot, BUNDLED_SKILLS_REL);
154
162
  if (!fs.existsSync(installedDir)) return [];
155
163
  return fs
156
164
  .readdirSync(installedDir, { withFileTypes: true })
@@ -161,6 +169,8 @@ function listInstalledSkillNames(projectRoot) {
161
169
 
162
170
  module.exports = {
163
171
  REGISTRY_PATH,
172
+ BUNDLED_SKILLS_REL,
173
+ LEGACY_BUNDLED_SKILLS_REL,
164
174
  readSkillRegistry,
165
175
  validateSkillRegistry,
166
176
  validateSkillStructuredReturn,