@brunosps00/dev-workflow 2.5.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +101 -0
- package/README.md +8 -5
- package/lib/doctor.js +63 -11
- package/lib/hooks.js +112 -27
- package/lib/init.js +27 -11
- package/lib/migrate-skills.js +189 -39
- package/lib/removed-bundled-skills.js +4 -2
- package/lib/skill-registry.js +12 -2
- package/lib/uninstall.js +63 -7
- package/package.json +1 -1
- package/scaffold/en/agent-instructions.md +1 -1
- package/scaffold/en/commands/dw-brainstorm.md +2 -2
- package/scaffold/en/commands/dw-bugfix.md +1 -1
- package/scaffold/en/commands/dw-commit.md +1 -1
- package/scaffold/en/commands/dw-dockerize.md +2 -2
- package/scaffold/en/commands/dw-functional-doc.md +1 -1
- package/scaffold/en/commands/dw-harness-audit.md +1 -1
- package/scaffold/en/commands/dw-intel.md +1 -1
- package/scaffold/en/commands/dw-learn.md +1 -1
- package/scaffold/en/commands/dw-new-project.md +3 -3
- package/scaffold/en/commands/dw-plan.md +2 -2
- package/scaffold/en/commands/dw-qa.md +6 -5
- package/scaffold/en/commands/dw-redesign-ui.md +3 -3
- package/scaffold/en/commands/dw-review.md +2 -2
- package/scaffold/en/commands/dw-skill-health.md +24 -1
- package/scaffold/en/commands/dw-update.md +28 -11
- package/scaffold/en/references/invariants.md +7 -2
- package/scaffold/en/templates/project-onepager.md +1 -1
- package/scaffold/pt-br/agent-instructions.md +1 -1
- package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
- package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
- package/scaffold/pt-br/commands/dw-commit.md +1 -1
- package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
- package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
- package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
- package/scaffold/pt-br/commands/dw-intel.md +1 -1
- package/scaffold/pt-br/commands/dw-learn.md +1 -1
- package/scaffold/pt-br/commands/dw-new-project.md +3 -3
- package/scaffold/pt-br/commands/dw-plan.md +2 -2
- package/scaffold/pt-br/commands/dw-qa.md +6 -5
- package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
- package/scaffold/pt-br/commands/dw-review.md +2 -2
- package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
- package/scaffold/pt-br/commands/dw-update.md +28 -11
- package/scaffold/pt-br/references/invariants.md +8 -2
- package/scaffold/pt-br/templates/project-onepager.md +1 -1
- package/scaffold/scripts/hooks/git-guardrails.mjs +3 -2
- package/scaffold/scripts/hooks/session-cost.mjs +62 -21
- package/scaffold/scripts/hooks/statusline.mjs +14 -11
- package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
- package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
- package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
- package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
- package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
- package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
- package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
- package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
- 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.
|
|
4
|
+
"version": "2.7.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bruno Santos"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,107 @@ 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
|
+
|
|
85
|
+
## [2.6.0] — 2026-10-07
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **Git guardrails under Codex.** `init` and `update` also write the git-guardrails
|
|
90
|
+
`PreToolUse` hook to `.codex/hooks.json` — same payload and deny contract as Claude Code,
|
|
91
|
+
matcher `Bash`. It is the only hook Codex gets: the session-cost tracker parses Claude Code
|
|
92
|
+
transcripts and Codex has no statusLine command. Codex runs project hooks only after the
|
|
93
|
+
project and the hook are trusted. Merge-aware like the Claude side: a malformed
|
|
94
|
+
`.codex/hooks.json` is left untouched, and `uninstall` removes only our entry, deleting the
|
|
95
|
+
file when nothing else is left. `doctor` warns when the Codex guardrail is missing or
|
|
96
|
+
outdated.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **Hooks stopped running whenever the shell left the project root.** The hook and statusLine
|
|
101
|
+
commands were cwd-relative (`node .dw/scripts/hooks/…`). Claude Code runs hooks in the
|
|
102
|
+
session's current directory, which follows every `cd`, so from any subdirectory they failed
|
|
103
|
+
with `Cannot find module` — and a failing `PreToolUse` hook is non-blocking, so Bash ran with
|
|
104
|
+
no git guardrail at all. In one real session, 843 of 1,243 Bash calls ran unguarded this way.
|
|
105
|
+
Commands now resolve `${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel || pwd)}`:
|
|
106
|
+
Claude Code exports the variable to hooks, and Codex, which exports none, gets the git root.
|
|
107
|
+
`update` rewrites the old entries in place, and `doctor` reports a cwd-relative guardrail as
|
|
108
|
+
an issue.
|
|
109
|
+
- The statusline read `.dw/` state (active spec, minimalism mode, today's spend) from the
|
|
110
|
+
current directory. It now reads it from the payload's `workspace.project_dir`.
|
|
111
|
+
- The guardrail's deny message pointed only at `.claude/settings.json`; it now names
|
|
112
|
+
`.codex/hooks.json` too, as does the invariants floor.
|
|
113
|
+
|
|
13
114
|
## [2.5.0] — 2026-09-28
|
|
14
115
|
|
|
15
116
|
### Added
|
package/README.md
CHANGED
|
@@ -13,10 +13,10 @@ 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 `.
|
|
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
|
-
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` (merge-aware — your own hooks/statusline are never overwritten)
|
|
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)
|
|
20
20
|
|
|
21
21
|
> **Compozy-inspired disciplines.** Since 0.5.0, dev-workflow bundles three primitives — `dw-verify`, `dw-memory`, `dw-review-rigor` — adapted from the [Compozy](https://github.com/compozy/compozy) project and invoked internally by existing commands. See [docs/compozy-integration.md](docs/compozy-integration.md) for what was ported and what was not.
|
|
22
22
|
|
|
@@ -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
|
|
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 `.
|
|
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]
|
|
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,8 +9,15 @@ const {
|
|
|
9
9
|
AGENT_TARGETS,
|
|
10
10
|
} = require('./agents');
|
|
11
11
|
const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
|
|
12
|
-
const {
|
|
13
|
-
|
|
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');
|
|
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
|
|
|
16
23
|
function run() {
|
|
@@ -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,
|
|
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
|
|
|
@@ -212,6 +227,8 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
212
227
|
if (!fs.existsSync(file)) issues.push(`Missing hook script: ${path.relative(projectRoot, file)}`);
|
|
213
228
|
}
|
|
214
229
|
|
|
230
|
+
checkCodexHooks(projectRoot, warnings);
|
|
231
|
+
|
|
215
232
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
216
233
|
if (!fs.existsSync(settingsPath)) return; // checkMcp already reports this
|
|
217
234
|
|
|
@@ -222,17 +239,52 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
222
239
|
return; // checkMcp already reports malformed settings
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
(
|
|
228
|
-
)
|
|
229
|
-
|
|
242
|
+
const guardrailCommands = guardrailCommandsOf(settings);
|
|
243
|
+
if (guardrailCommands.length === 0) {
|
|
244
|
+
issues.push('Missing git-guardrails PreToolUse hook in .claude/settings.json');
|
|
245
|
+
} else if (!guardrailCommands.includes(GIT_GUARDRAILS_CMD)) {
|
|
246
|
+
// Pre-2.5.1 installs wired a cwd-relative path: it fails (non-blocking) and stops
|
|
247
|
+
// guarding as soon as the shell leaves the project root.
|
|
248
|
+
issues.push('Outdated git-guardrails PreToolUse hook in .claude/settings.json. Run `dev-workflow update`.');
|
|
249
|
+
}
|
|
230
250
|
|
|
231
251
|
const statusLine = settings.statusLine;
|
|
252
|
+
const statusCommand = String((statusLine && statusLine.command) || '');
|
|
232
253
|
if (!statusLine) {
|
|
233
254
|
issues.push('Missing statusLine in .claude/settings.json');
|
|
234
|
-
} else if (
|
|
235
|
-
warnings.push(
|
|
255
|
+
} else if (!statusCommand.includes(HOOKS_DIR_MARKER)) {
|
|
256
|
+
warnings.push(`Custom statusLine set; dev-workflow statusline is not active (add \`${STATUSLINE_CMD}\`).`);
|
|
257
|
+
} else if (statusCommand !== STATUSLINE_CMD) {
|
|
258
|
+
warnings.push('Outdated dev-workflow statusLine in .claude/settings.json. Run `dev-workflow update`.');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function guardrailCommandsOf(config) {
|
|
263
|
+
const preToolUse = (config && config.hooks && config.hooks.PreToolUse) || [];
|
|
264
|
+
return preToolUse.flatMap((g) =>
|
|
265
|
+
g && Array.isArray(g.hooks)
|
|
266
|
+
? g.hooks.map((h) => h && h.command).filter((c) => typeof c === 'string' && c.includes(GIT_GUARDRAILS_SCRIPT))
|
|
267
|
+
: [],
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Codex is one of several supported agents, so a missing Codex guardrail is a
|
|
272
|
+
// warning rather than an issue.
|
|
273
|
+
function checkCodexHooks(projectRoot, warnings) {
|
|
274
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
275
|
+
let commands = [];
|
|
276
|
+
if (fs.existsSync(file)) {
|
|
277
|
+
try {
|
|
278
|
+
commands = guardrailCommandsOf(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
279
|
+
} catch {
|
|
280
|
+
warnings.push(`Malformed ${CODEX_HOOKS_FILE}; Codex cannot load the git guardrail.`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (commands.length === 0) {
|
|
285
|
+
warnings.push(`Missing git-guardrails hook in ${CODEX_HOOKS_FILE} (Bash under Codex is unguarded). Run \`dev-workflow update\`.`);
|
|
286
|
+
} else if (!commands.includes(GIT_GUARDRAILS_CMD)) {
|
|
287
|
+
warnings.push(`Outdated git-guardrails hook in ${CODEX_HOOKS_FILE}. Run \`dev-workflow update\`.`);
|
|
236
288
|
}
|
|
237
289
|
}
|
|
238
290
|
|
|
@@ -245,4 +297,4 @@ function checkLegacyCommands(projectRoot, warnings) {
|
|
|
245
297
|
}
|
|
246
298
|
}
|
|
247
299
|
|
|
248
|
-
module.exports = { run };
|
|
300
|
+
module.exports = { run, checkHooks };
|
package/lib/hooks.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { readSettings, writeSettings } = require('./mcp');
|
|
2
4
|
|
|
3
5
|
// dev-workflow-owned hook/statusline scripts live under this path. The substring
|
|
4
|
-
// is the marker used to identify OUR entries in .claude/settings.json
|
|
5
|
-
// add/refresh them on update without ever touching
|
|
6
|
+
// is the marker used to identify OUR entries in .claude/settings.json and
|
|
7
|
+
// .codex/hooks.json so we can add/refresh them on update without ever touching
|
|
8
|
+
// the user's own hooks.
|
|
6
9
|
const HOOKS_DIR_MARKER = '.dw/scripts/hooks/';
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// Hooks run in the session's *current* directory, which in Claude Code follows
|
|
12
|
+
// every `cd` in the Bash tool. A cwd-relative `node .dw/...` breaks with "Cannot
|
|
13
|
+
// find module" as soon as the shell sits in a subdirectory, and a failing
|
|
14
|
+
// PreToolUse hook is non-blocking: the command runs unguarded. Claude Code exports
|
|
15
|
+
// $CLAUDE_PROJECT_DIR to hooks; Codex exports no project variable, so fall back to
|
|
16
|
+
// the git root (a worktree's own root), then to the cwd. The fallback is only
|
|
17
|
+
// evaluated when the variable is unset.
|
|
18
|
+
const PROJECT_ROOT = '${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}';
|
|
19
|
+
const hookCommand = (script) => `node "${PROJECT_ROOT}/${HOOKS_DIR_MARKER}${script}"`;
|
|
20
|
+
|
|
21
|
+
// Codex reads project hooks from .codex/hooks.json (same shape as Claude Code's
|
|
22
|
+
// "hooks" block; PreToolUse matches tool_name "Bash"). It only runs them once the
|
|
23
|
+
// project's .codex/ layer and the hook itself are trusted in Codex.
|
|
24
|
+
const CODEX_HOOKS_FILE = path.join('.codex', 'hooks.json');
|
|
25
|
+
|
|
9
26
|
const GIT_GUARDRAILS_SCRIPT = 'git-guardrails.mjs';
|
|
10
|
-
const
|
|
27
|
+
const GIT_GUARDRAILS_CMD = hookCommand(GIT_GUARDRAILS_SCRIPT);
|
|
28
|
+
const STATUSLINE_CMD = hookCommand('statusline.mjs');
|
|
11
29
|
const SESSION_COST_SCRIPT = 'session-cost.mjs';
|
|
30
|
+
const SESSION_COST_CMD = hookCommand(SESSION_COST_SCRIPT);
|
|
12
31
|
|
|
13
32
|
function isOurs(command) {
|
|
14
33
|
return typeof command === 'string' && command.includes(HOOKS_DIR_MARKER);
|
|
@@ -25,8 +44,11 @@ function isOurs(command) {
|
|
|
25
44
|
* - leaves a user-defined statusLine untouched (reports it so the caller can
|
|
26
45
|
* surface a post-update note).
|
|
27
46
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
47
|
+
* Also reconciles the git guardrail into .codex/hooks.json (see installCodexHooks).
|
|
48
|
+
*
|
|
49
|
+
* Returns { hooks, sessionCost, statusLine, codex } where each is 'added' |
|
|
50
|
+
* 'updated' | 'unchanged' (statusLine may also be 'skipped-user', codex
|
|
51
|
+
* 'skipped-malformed').
|
|
30
52
|
*/
|
|
31
53
|
function installHooks(projectRoot) {
|
|
32
54
|
const settings = readSettings(projectRoot);
|
|
@@ -40,7 +62,44 @@ function installHooks(projectRoot) {
|
|
|
40
62
|
writeSettings(projectRoot, settings);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
hooks: hooksResult,
|
|
67
|
+
sessionCost: sessionCostResult,
|
|
68
|
+
statusLine: statusLineResult,
|
|
69
|
+
codex: installCodexHooks(projectRoot),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readCodexHooks(projectRoot) {
|
|
74
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
75
|
+
if (!fs.existsSync(file)) return {};
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
78
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeCodexHooks(projectRoot, config) {
|
|
85
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reconcile the git guardrail into .codex/hooks.json — the only dev-workflow hook
|
|
92
|
+
* Codex gets: session-cost parses Claude Code transcripts and Codex has no
|
|
93
|
+
* statusLine command. Same marker-based contract as the Claude side.
|
|
94
|
+
* Returns 'added' | 'updated' | 'unchanged' | 'skipped-malformed'.
|
|
95
|
+
*/
|
|
96
|
+
function installCodexHooks(projectRoot) {
|
|
97
|
+
const config = readCodexHooks(projectRoot);
|
|
98
|
+
if (config === null) return 'skipped-malformed';
|
|
99
|
+
const before = JSON.stringify(config);
|
|
100
|
+
const result = reconcilePreToolUseGitGuardrails(config);
|
|
101
|
+
if (JSON.stringify(config) !== before) writeCodexHooks(projectRoot, config);
|
|
102
|
+
return result;
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
|
@@ -135,30 +194,14 @@ function reconcileStatusLine(settings) {
|
|
|
135
194
|
}
|
|
136
195
|
|
|
137
196
|
/**
|
|
138
|
-
* Remove ONLY dev-workflow-owned hook + statusLine entries
|
|
139
|
-
* Returns the number of entries removed.
|
|
197
|
+
* Remove ONLY dev-workflow-owned hook + statusLine entries from .claude/settings.json
|
|
198
|
+
* and .codex/hooks.json (used by uninstall). Returns the number of entries removed.
|
|
140
199
|
*/
|
|
141
200
|
function removeHooks(projectRoot) {
|
|
142
201
|
const settings = readSettings(projectRoot);
|
|
143
|
-
let removed = 0;
|
|
144
|
-
|
|
145
202
|
// Remove OUR hook entries from every event (PreToolUse, SessionEnd, PostToolUse, …),
|
|
146
203
|
// identified by the .dw/scripts/hooks/ marker; never touch user-added hooks.
|
|
147
|
-
|
|
148
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
149
|
-
const groups = settings.hooks[event];
|
|
150
|
-
if (!Array.isArray(groups)) continue;
|
|
151
|
-
for (const group of groups) {
|
|
152
|
-
if (!group || !Array.isArray(group.hooks)) continue;
|
|
153
|
-
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
154
|
-
removed += group.hooks.length - kept.length;
|
|
155
|
-
group.hooks = kept;
|
|
156
|
-
}
|
|
157
|
-
settings.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
158
|
-
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
159
|
-
}
|
|
160
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
161
|
-
}
|
|
204
|
+
let removed = removeOurHookEntries(settings);
|
|
162
205
|
|
|
163
206
|
if (settings.statusLine && isOurs(settings.statusLine.command)) {
|
|
164
207
|
delete settings.statusLine;
|
|
@@ -166,6 +209,46 @@ function removeHooks(projectRoot) {
|
|
|
166
209
|
}
|
|
167
210
|
|
|
168
211
|
if (removed > 0) writeSettings(projectRoot, settings);
|
|
212
|
+
return removed + removeCodexHooks(projectRoot);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function removeOurHookEntries(config) {
|
|
216
|
+
let removed = 0;
|
|
217
|
+
if (!config.hooks || typeof config.hooks !== 'object') return 0;
|
|
218
|
+
for (const event of Object.keys(config.hooks)) {
|
|
219
|
+
const groups = config.hooks[event];
|
|
220
|
+
if (!Array.isArray(groups)) continue;
|
|
221
|
+
for (const group of groups) {
|
|
222
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
223
|
+
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
224
|
+
removed += group.hooks.length - kept.length;
|
|
225
|
+
group.hooks = kept;
|
|
226
|
+
}
|
|
227
|
+
config.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
228
|
+
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
231
|
+
return removed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Remove OUR entries from .codex/hooks.json; delete the file only when nothing
|
|
235
|
+
// of the user's is left in it.
|
|
236
|
+
function removeCodexHooks(projectRoot) {
|
|
237
|
+
const config = readCodexHooks(projectRoot);
|
|
238
|
+
if (!config) return 0;
|
|
239
|
+
const removed = removeOurHookEntries(config);
|
|
240
|
+
if (removed === 0) return 0;
|
|
241
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
242
|
+
if (Object.keys(config).length === 0) {
|
|
243
|
+
fs.rmSync(file);
|
|
244
|
+
try {
|
|
245
|
+
fs.rmdirSync(path.dirname(file));
|
|
246
|
+
} catch {
|
|
247
|
+
/* .codex/ still holds other files */
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
writeCodexHooks(projectRoot, config);
|
|
251
|
+
}
|
|
169
252
|
return removed;
|
|
170
253
|
}
|
|
171
254
|
|
|
@@ -173,6 +256,8 @@ module.exports = {
|
|
|
173
256
|
installHooks,
|
|
174
257
|
removeHooks,
|
|
175
258
|
HOOKS_DIR_MARKER,
|
|
259
|
+
CODEX_HOOKS_FILE,
|
|
260
|
+
GIT_GUARDRAILS_SCRIPT,
|
|
176
261
|
GIT_GUARDRAILS_CMD,
|
|
177
262
|
STATUSLINE_CMD,
|
|
178
263
|
SESSION_COST_CMD,
|
package/lib/init.js
CHANGED
|
@@ -4,7 +4,7 @@ const { ensureDir, copyDir, writeFile, upsertDelimitedBlock, log, listFilesRecur
|
|
|
4
4
|
const { selectLanguage } = require('./prompts');
|
|
5
5
|
const { generateWrappers } = require('./wrappers');
|
|
6
6
|
const { installMCPs } = require('./mcp');
|
|
7
|
-
const { installHooks } = require('./hooks');
|
|
7
|
+
const { installHooks, STATUSLINE_CMD } = require('./hooks');
|
|
8
8
|
const { generateAgents } = require('./agents');
|
|
9
9
|
const { migrate: migrateGsd } = require('./migrate-gsd');
|
|
10
10
|
const { migrate: migrateSkills } = require('./migrate-skills');
|
|
@@ -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
|
-
//
|
|
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 .
|
|
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,
|
|
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,
|
|
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);
|
|
@@ -408,13 +418,19 @@ async function run({ force = false, lang = null, mode = 'init', profile = null,
|
|
|
408
418
|
const mcpResults = installMCPs(projectRoot);
|
|
409
419
|
console.log(` ${mcpResults.added} configured, ${mcpResults.skipped} already present\n`);
|
|
410
420
|
|
|
411
|
-
// 9.2. Reconcile dev-workflow hooks + statusline
|
|
412
|
-
//
|
|
421
|
+
// 9.2. Reconcile dev-workflow hooks + statusline into .claude/settings.json and the
|
|
422
|
+
// git guardrail into .codex/hooks.json (marker-based, merge-aware: refreshes our
|
|
423
|
+
// entries, never touches user-defined hooks/statusLine).
|
|
413
424
|
console.log(' Hooks & statusline:');
|
|
414
425
|
const hookResults = installHooks(projectRoot);
|
|
415
|
-
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}\n`);
|
|
426
|
+
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}, codex git-guardrails: ${hookResults.codex}\n`);
|
|
427
|
+
if (hookResults.codex === 'skipped-malformed') {
|
|
428
|
+
console.log(' Note: .codex/hooks.json is not valid JSON; the Codex git guardrail was not installed.\n');
|
|
429
|
+
} else if (hookResults.codex !== 'unchanged') {
|
|
430
|
+
console.log(' Note: Codex runs .codex/hooks.json only after you trust the project and review the hook in Codex.\n');
|
|
431
|
+
}
|
|
416
432
|
if (hookResults.statusLine === 'skipped-user') {
|
|
417
|
-
console.log(
|
|
433
|
+
console.log(` Note: a custom statusLine is set; to show dev-workflow state add \`${STATUSLINE_CMD}\`.\n`);
|
|
418
434
|
}
|
|
419
435
|
|
|
420
436
|
// 9.5. Persist install state for diagnostics and repair.
|