@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.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +101 -0
  3. package/README.md +8 -5
  4. package/lib/doctor.js +63 -11
  5. package/lib/hooks.js +112 -27
  6. package/lib/init.js +27 -11
  7. package/lib/migrate-skills.js +189 -39
  8. package/lib/removed-bundled-skills.js +4 -2
  9. package/lib/skill-registry.js +12 -2
  10. package/lib/uninstall.js +63 -7
  11. package/package.json +1 -1
  12. package/scaffold/en/agent-instructions.md +1 -1
  13. package/scaffold/en/commands/dw-brainstorm.md +2 -2
  14. package/scaffold/en/commands/dw-bugfix.md +1 -1
  15. package/scaffold/en/commands/dw-commit.md +1 -1
  16. package/scaffold/en/commands/dw-dockerize.md +2 -2
  17. package/scaffold/en/commands/dw-functional-doc.md +1 -1
  18. package/scaffold/en/commands/dw-harness-audit.md +1 -1
  19. package/scaffold/en/commands/dw-intel.md +1 -1
  20. package/scaffold/en/commands/dw-learn.md +1 -1
  21. package/scaffold/en/commands/dw-new-project.md +3 -3
  22. package/scaffold/en/commands/dw-plan.md +2 -2
  23. package/scaffold/en/commands/dw-qa.md +6 -5
  24. package/scaffold/en/commands/dw-redesign-ui.md +3 -3
  25. package/scaffold/en/commands/dw-review.md +2 -2
  26. package/scaffold/en/commands/dw-skill-health.md +24 -1
  27. package/scaffold/en/commands/dw-update.md +28 -11
  28. package/scaffold/en/references/invariants.md +7 -2
  29. package/scaffold/en/templates/project-onepager.md +1 -1
  30. package/scaffold/pt-br/agent-instructions.md +1 -1
  31. package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
  32. package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
  33. package/scaffold/pt-br/commands/dw-commit.md +1 -1
  34. package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
  35. package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
  36. package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
  37. package/scaffold/pt-br/commands/dw-intel.md +1 -1
  38. package/scaffold/pt-br/commands/dw-learn.md +1 -1
  39. package/scaffold/pt-br/commands/dw-new-project.md +3 -3
  40. package/scaffold/pt-br/commands/dw-plan.md +2 -2
  41. package/scaffold/pt-br/commands/dw-qa.md +6 -5
  42. package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
  43. package/scaffold/pt-br/commands/dw-review.md +2 -2
  44. package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
  45. package/scaffold/pt-br/commands/dw-update.md +28 -11
  46. package/scaffold/pt-br/references/invariants.md +8 -2
  47. package/scaffold/pt-br/templates/project-onepager.md +1 -1
  48. package/scaffold/scripts/hooks/git-guardrails.mjs +3 -2
  49. package/scaffold/scripts/hooks/session-cost.mjs +62 -21
  50. package/scaffold/scripts/hooks/statusline.mjs +14 -11
  51. package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
  52. package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
  53. package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
  54. package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
  55. package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
  56. package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
  57. package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
  58. package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
  59. package/scaffold/skills/dw-ui-discipline/references/responsive.md +44 -2
@@ -24,15 +24,16 @@ Antes de sobrescrever arquivos gerenciados, crie um snapshot:
24
24
  ```bash
25
25
  SNAPSHOT_DIR=".dw/.backup/$(date -u +%Y%m%dT%H%M%SZ)"
26
26
  mkdir -p "$SNAPSHOT_DIR"
27
- cp -r .dw/commands .dw/templates .dw/references .dw/scripts "$SNAPSHOT_DIR/" 2>/dev/null
27
+ cp -r .dw/commands .dw/templates .dw/references .dw/scripts .dw/skills "$SNAPSHOT_DIR/" 2>/dev/null
28
28
  # agents/skills (bundled) tambem fazem parte do update
29
29
  [ -d .agents/skills ] && cp -r .agents/skills "$SNAPSHOT_DIR/agents-skills" 2>/dev/null
30
30
  # .claude/settings.json agora e mutado pelo update (MCPs + hooks + statusLine)
31
31
  [ -f .claude/settings.json ] && mkdir -p "$SNAPSHOT_DIR/claude" && cp .claude/settings.json "$SNAPSHOT_DIR/claude/settings.json" 2>/dev/null
32
+ [ -f .dw/install-state.json ] && cp .dw/install-state.json "$SNAPSHOT_DIR/install-state.json" 2>/dev/null
32
33
  echo "Snapshot salvo em $SNAPSHOT_DIR"
33
34
  ```
34
35
 
35
- Manter apenas os 3 snapshots mais recentes (remover os mais antigos) para evitar acumulo.
36
+ Manter apenas os 3 snapshots mais recentes em `.dw/.backup/` (remover os mais antigos) para evitar acumulo. `.dw/.migrated/` não é snapshot: nunca remova.
36
37
 
37
38
  ### 1. Registrar Versão Atual (Obrigatório)
38
39
 
@@ -86,7 +87,13 @@ O comando `update` sobrescreve arquivos gerenciados e PRESERVA:
86
87
 
87
88
  O comando `update` também roda o passo de migração GSD automaticamente — se o projeto tem `.planning/` legado (de uso prévio do GSD), o conteúdo é migrado para `.dw/intel/`, `.dw/spec/active-session.md`, `.dw/spec/quick/`, etc., e `.planning/` é renomeado para `.planning.gsd-archive-<DATA>/` para inspeção. Os arquivos `.claude/commands/gsd/`, `.claude/agents/gsd-*.md`, `.claude/hooks/gsd-*.js` e `.claude/gsd-file-manifest.json` são removidos durante a migração.
88
89
 
89
- Se o update falhar (erro de rede, permissão, pacote indisponível): reporte o erro ao usuário e PARE. NÃO tente workarounds manuais como copiar arquivos.
90
+ O CLI nunca apaga o que aposenta: move para `.dw/.migrated/<timestamp>/`, mantendo o caminho relativo, e imprime o destino. Isso acontece só depois que as skills novas estão em `.dw/skills/`, então um update interrompido deixa as cópias antigas no lugar. O que ele aposenta, e como decide:
91
+
92
+ - **Cópias legadas de skills bundled** em `.agents/skills/` (`[moved]`): só quando a cópia é do dev-workflow. Skill homônima que não é sai como `[kept]` e fica intocada.
93
+ - **Skills removidas em releases anteriores** (`[v<N>]`) e **fontes de comandos removidos** (`[legacy]`): casadas por nome, então uma skill homônima de outra origem também é movida — é o move que torna isso recuperável.
94
+ - **Wrappers `dw-*` órfãos** (`[orphan]`): `dw-` é o namespace deste pacote; uma skill do usuário chamada `dw-*` é movida, não perdida.
95
+
96
+ Se o update falhar (erro de rede, permissão, pacote indisponível): reporte o erro ao usuário e PARE. NÃO tente workarounds manuais como copiar arquivos. Passe ao usuário o caminho do snapshot do passo 0 e as duas saídas: rodar o update de novo depois de resolver a causa (ele é idempotente), ou `/dw-update --rollback`.
90
97
 
91
98
  ### 4. Capturar Nova Versão
92
99
 
@@ -96,7 +103,7 @@ node -e 'const s = JSON.parse(require("node:fs").readFileSync(".dw/install-state
96
103
 
97
104
  ### Checks de compatibilidade do upgrade
98
105
 
99
- Leia o `.dw/commands/dw-update.md` atualizado após o CLI terminar. Use `.dw/install-state.json` e o output `Installed version` do CLI para a diferença de versões: um pacote executado por `npx` não precisa ser resolvível pelo `node_modules` do consumidor. Estado novo ausente ou inválido é falha de verificação; não declare sucesso do update.
106
+ Leia o `.dw/commands/dw-update.md` atualizado após o CLI terminar. Use `.dw/install-state.json` e o output `Installed version` do CLI para a diferença de versões: um pacote executado por `npx` não precisa ser resolvível pelo `node_modules` do consumidor. Estado novo ausente ou inválido é falha de verificação; não declare sucesso do update. A versão lida no passo 4 tem de ser igual ao lado direito da linha `Installed version: <antiga> -> <nova>` do CLI; se essa linha faltar, ou as versões diferirem, o CLI não terminou e o update falhou.
100
107
 
101
108
  Em um upgrade de versão anterior à 2.3.0 (ou versão legada desconhecida):
102
109
 
@@ -116,6 +123,7 @@ Apresente ao usuário:
116
123
  - Resumo do que o output do `update` mostrou (arquivos copiados, wrappers gerados, MCPs configurados)
117
124
  - As `Post-update agent actions` impressas pelo CLI
118
125
  - Quaisquer avisos ou erros
126
+ - Toda linha `[moved]`, `[v<N>]`, `[orphan]`, `[legacy]` ou `[kept]` do `Skill migration`, o caminho `.dw/.migrated/<timestamp>/` e todo arquivo marcado `was not installed by dev-workflow` (adição do próprio time: ofereça trazê-lo de volta numa skill do time, nunca em `.dw/skills/`, que o update sobrescreve)
119
127
 
120
128
  ### 6. Executar Ações de Agente Pós-Update (Obrigatório)
121
129
 
@@ -144,19 +152,28 @@ Se invocado com `--rollback`:
144
152
  1. Listar snapshots em `.dw/.backup/`
145
153
  2. Se nenhum existir: PARAR e reportar "Nenhum snapshot disponível"
146
154
  3. Se mais de um existir: perguntar ao usuário qual restaurar (padrão: mais recente)
147
- 4. Confirmar com o usuário: "Restaurar snapshot `<path>`? Isso SOBRESCREVE `.dw/commands/`, `.dw/templates/`, `.dw/references/`, `.dw/scripts/`, `.agents/skills/` e `.claude/settings.json`. Prosseguir? [s/N]"
155
+ 4. Confirmar com o usuário: "Restaurar snapshot `<path>`? Isso SUBSTITUI `.dw/commands/`, `.dw/templates/`, `.dw/references/`, `.dw/scripts/` e `.dw/skills/` pelas cópias do snapshot (quando ele as tem), copia o `.agents/skills/` do snapshot por cima do atual sem remover outras entradas, e restaura `.claude/settings.json` e `.dw/install-state.json`. `.dw/templates/overrides/` nunca é tocado. Prosseguir? [s/N]"
148
156
  5. Somente após `s`: copiar de volta
149
157
 
150
158
  ```bash
151
- cp -r "$SNAPSHOT_DIR/commands" .dw/
152
- cp -r "$SNAPSHOT_DIR/templates" .dw/
153
- cp -r "$SNAPSHOT_DIR/references" .dw/ 2>/dev/null
154
- cp -r "$SNAPSHOT_DIR/scripts" .dw/ 2>/dev/null
155
- [ -d "$SNAPSHOT_DIR/agents-skills" ] && cp -r "$SNAPSHOT_DIR/agents-skills" .agents/skills 2>/dev/null
159
+ SNAPSHOT_DIR=".dw/.backup/<timestamp-escolhido>" # o snapshot confirmado no passo 4
160
+ [ -d "$SNAPSHOT_DIR" ] || { echo "Snapshot não encontrado: $SNAPSHOT_DIR"; exit 1; }
161
+ # Pastas gerenciadas de .dw são substituídas, para não sobrar arquivo da release mais nova.
162
+ for d in commands references scripts skills; do
163
+ [ -d "$SNAPSHOT_DIR/$d" ] && rm -rf ".dw/$d" && cp -r "$SNAPSHOT_DIR/$d" .dw/
164
+ done
165
+ # .dw/templates/overrides/ é do dono: restaure os templates gerenciados em volta dele, nunca o remova.
166
+ if [ -d "$SNAPSHOT_DIR/templates" ]; then
167
+ find .dw/templates -mindepth 1 -maxdepth 1 ! -name overrides -exec rm -rf {} +
168
+ for f in "$SNAPSHOT_DIR/templates"/*; do [ "$(basename "$f")" = overrides ] || cp -r "$f" .dw/templates/; done
169
+ fi
170
+ # .agents/skills é compartilhada com as skills do próprio usuário: mescla, nunca substitui.
171
+ [ -d "$SNAPSHOT_DIR/agents-skills" ] && mkdir -p .agents/skills && cp -r "$SNAPSHOT_DIR/agents-skills/." .agents/skills/ 2>/dev/null
156
172
  [ -f "$SNAPSHOT_DIR/claude/settings.json" ] && cp "$SNAPSHOT_DIR/claude/settings.json" .claude/settings.json 2>/dev/null
173
+ [ -f "$SNAPSHOT_DIR/install-state.json" ] && cp "$SNAPSHOT_DIR/install-state.json" .dw/install-state.json 2>/dev/null
157
174
  ```
158
175
 
159
- 6. Reportar: snapshot restaurado, versão provavelmente recuperada (ler de `.dw/commands/dw-help.md` ou metadata se houver)
176
+ 6. Reportar: snapshot restaurado, e a versão do `.dw/install-state.json` restaurado (ou "unknown" quando o snapshot é anterior a ele). Snapshot de antes da 2.7.0 não tem `skills/`: `.dw/skills/` fica como estava, o que é inofensivo (comandos antigos leem `.agents/skills/`), e o próximo update aposenta de novo as cópias restauradas em `.agents/skills/`.
160
177
 
161
178
  ## Opções Avançadas
162
179
 
@@ -49,8 +49,9 @@ funcionando. Um agente que não consegue destruir trabalho não pode ser convenc
49
49
 
50
50
  **Como isso é verificado:** em parte pelo `.dw/scripts/hooks/git-guardrails.mjs`, um hook `PreToolUse` que
51
51
  nega esses padrões no Bash. O hook é **uma implementação, não a fonte da regra**, e tem dois limites
52
- conhecidos: só cobre Bash no Claude Code, e **falha aberto** — erro de parse ou exceção em runtime deixa o
53
- comando passar. Então a regra obriga mesmo onde o hook não roda.
52
+ conhecidos: só cobre Bash no Claude Code e no Codex (o Codex só o roda depois que o projeto e o hook são
53
+ marcados como confiáveis), e **falha aberto** — erro de parse ou exceção em runtime deixa o comando passar.
54
+ Então a regra obriga mesmo onde o hook não roda.
54
55
 
55
56
  Duas operações ficam de fora do hook de propósito e vivem só aqui: `rebase` e `commit --amend`. As duas são
56
57
  rotina em trabalho não pushado, e uma linha de comando não distingue pushado de não pushado. Negá-las
@@ -80,6 +81,11 @@ Este piso é estreito de propósito. Algo genuinamente destrutivo que não estej
80
81
  isso — aplique o mesmo critério: se a ação destrói trabalho sem segunda cópia, reescreve história
81
82
  compartilhada ou move uma credencial, pare e pergunte. Uma confirmação a mais custa uma mensagem.
82
83
 
84
+ O mesmo vale para as pastas de skills que os agentes compartilham — `.agents/skills/`, `.claude/skills/`,
85
+ `.codex/skills/`: elas guardam também skills do usuário e de outras ferramentas. Fora do `update`/`uninstall`
86
+ do próprio CLI, liste o que removeria e pergunte por entrada; mova o aprovado para `.dw/.migrated/<timestamp>/`,
87
+ nunca `rm`.
88
+
83
89
  ## Atribuição
84
90
 
85
91
  O modelo de duas camadas — um piso que regra de projeto só pode apertar, nunca afrouxar — foi adaptado do
@@ -80,7 +80,7 @@ services: []
80
80
  │ └── api/ (criado por `pnpm dlx @nestjs/cli` ou scaffold escolhido)
81
81
  ├── packages/
82
82
  │ └── shared/ (criado pelo /dw-new-project)
83
- ├── docker-compose.dev.yml (composto a partir de .agents/skills/docker-compose-recipes/)
83
+ ├── docker-compose.dev.yml (composto a partir de .dw/skills/docker-compose-recipes/)
84
84
  ├── .env.example (consolidado dos servicos selecionados)
85
85
  ├── .gitignore (por stack)
86
86
  ├── .dockerignore (por stack)
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * dev-workflow git guardrails — Claude Code PreToolUse hook (matcher: Bash).
3
+ * dev-workflow git guardrails — PreToolUse hook (matcher: Bash) for Claude Code
4
+ * (.claude/settings.json) and Codex (.codex/hooks.json); both send the same payload.
4
5
  *
5
6
  * Blocks irreversible git commands (force push, hard reset, clean -f, branch
6
7
  * deletion, remote branch deletion). Everything else is allowed.
@@ -76,7 +77,7 @@ export function evaluatePayload(input) {
76
77
  permissionDecision: 'deny',
77
78
  permissionDecisionReason:
78
79
  `Blocked by dev-workflow git guardrails: ${rule.why}. ` +
79
- `If this is intentional, run it yourself outside the agent, or adjust .claude/settings.json hooks.`,
80
+ `If this is intentional, run it yourself outside the agent, or adjust the hook in .claude/settings.json or .codex/hooks.json.`,
80
81
  },
81
82
  };
82
83
  }
@@ -12,9 +12,12 @@
12
12
  * USD is a best-effort ESTIMATE from a local price table — tokens are exact.
13
13
  *
14
14
  * The same pass also counts which skills the session's tool calls referenced,
15
- * emitted as `skills` on the row. `/dw-skill-health` reads it as usage evidence.
16
- * An absent `skills` key means the row predates this instrumentation; `{}` means
17
- * the session was observed and nothing fired — those are different facts.
15
+ * emitted as `skills` on the row, and which dev-workflow commands it loaded
16
+ * (a read of `.dw/commands/<name>.md`, which every command wrapper performs, or a
17
+ * Skill-tool call naming a command), emitted as `commands`. `/dw-skill-health`
18
+ * reads both as usage evidence. An absent key means the row predates that
19
+ * instrumentation; `{}` means the session was observed and nothing fired —
20
+ * those are different facts.
18
21
  *
19
22
  * Contract: reads the SessionEnd payload as JSON on stdin. Fails SAFE — any
20
23
  * error exits 0 without writing, so a hook bug never disrupts the session.
@@ -70,7 +73,7 @@ function knownSkills() {
70
73
  return false;
71
74
  }
72
75
  };
73
- for (const base of ['.agents/skills', '.claude/skills']) {
76
+ for (const base of ['.dw/skills', '.agents/skills', '.claude/skills']) {
74
77
  const root = join(PROJECT_ROOT, base);
75
78
  if (!isDir(root)) continue;
76
79
  let top;
@@ -103,8 +106,29 @@ function knownSkills() {
103
106
  }
104
107
 
105
108
  // Matches a skills/ path inside a tool call's input, capturing at most two
106
- // segments so a grouped `group/skill` is preserved.
107
- const SKILL_PATH = /(?:\.agents|\.claude)\/skills\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?)\//g;
109
+ // segments so a grouped `group/skill` is preserved. `.dw/skills/` is where the
110
+ // bundled skills live; `.agents/` and `.claude/` hold wrappers, external and
111
+ // legacy copies.
112
+ const SKILL_PATH = /(?:\.dw|\.agents|\.claude)\/skills\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?)\//g;
113
+
114
+ // Matches the canonical command file every wrapper tells the agent to read.
115
+ const COMMAND_PATH = /\.dw\/commands\/([A-Za-z0-9._-]+)\.md/g;
116
+
117
+ // Commands this project has installed: the names usage counting may emit under
118
+ // `commands`. Their wrappers also sit under `.claude/skills/` and
119
+ // `.agents/skills/`, so knownSkills() sees the same names; a command name is
120
+ // always counted as a command, never as a skill.
121
+ function knownCommands() {
122
+ const names = new Set();
123
+ try {
124
+ for (const file of readdirSync(join(DW_DIR, 'commands'))) {
125
+ if (file.endsWith('.md')) names.add(file.slice(0, -3));
126
+ }
127
+ } catch {
128
+ /* no installed commands — nothing to count */
129
+ }
130
+ return names;
131
+ }
108
132
 
109
133
  // Count skill references in ONE assistant message.
110
134
  //
@@ -113,17 +137,22 @@ const SKILL_PATH = /(?:\.agents|\.claude)\/skills\/([A-Za-z0-9._-]+(?:\/[A-Za-z0
113
137
  // `tool_result` that returned the CONTENTS of a SKILL.md (those cite other
114
138
  // skills) and a user prompt that merely names one — both would report a skill as
115
139
  // fired when it never was. What this measures is loading, not obedience.
116
- function countSkillUses(msg, known, out) {
140
+ function countSkillUses(msg, known, out, commands = new Set(), commandOut = {}) {
117
141
  const content = msg && msg.content;
118
142
  if (!Array.isArray(content)) return;
119
143
  for (const block of content) {
120
144
  if (!block || block.type !== 'tool_use' || !block.input) continue;
121
- // One tool call is one reference per skill, however many times its input
122
- // names that skill. A grep over three files under the same skill directory
123
- // is one load, not three.
145
+ // One tool call is one reference per skill or command, however many times
146
+ // its input names it. A grep over three files under the same skill
147
+ // directory is one load, not three.
124
148
  const seen = new Set();
125
- if (block.name === 'Skill' && typeof block.input.skill === 'string' && known.has(block.input.skill)) {
126
- seen.add(block.input.skill);
149
+ const seenCommands = new Set();
150
+ const note = (name) => {
151
+ if (commands.has(name)) seenCommands.add(name);
152
+ else if (known.has(name)) seen.add(name);
153
+ };
154
+ if (block.name === 'Skill' && typeof block.input.skill === 'string') {
155
+ note(block.input.skill);
127
156
  }
128
157
  let serialized;
129
158
  try {
@@ -136,24 +165,29 @@ function countSkillUses(msg, known, out) {
136
165
  let match;
137
166
  while ((match = SKILL_PATH.exec(serialized)) !== null) {
138
167
  const full = match[1];
139
- const name = known.has(full) ? full : full.split('/')[0];
140
- if (known.has(name)) seen.add(name);
168
+ note(known.has(full) ? full : full.split('/')[0]);
169
+ }
170
+ COMMAND_PATH.lastIndex = 0;
171
+ while ((match = COMMAND_PATH.exec(serialized)) !== null) {
172
+ if (commands.has(match[1])) seenCommands.add(match[1]);
141
173
  }
142
174
  }
143
175
  for (const name of seen) out[name] = (out[name] || 0) + 1;
176
+ for (const name of seenCommands) commandOut[name] = (commandOut[name] || 0) + 1;
144
177
  }
145
178
  }
146
179
 
147
180
  // Sum per-turn usage from the transcript, grouped by model, and count which
148
181
  // skills the session's tool calls referenced.
149
- function tallyTranscript(transcriptPath, known) {
182
+ function tallyTranscript(transcriptPath, known, commands = new Set()) {
150
183
  const byModel = {};
151
184
  const skills = {};
185
+ const commandUses = {};
152
186
  let raw;
153
187
  try {
154
188
  raw = readFileSync(transcriptPath, 'utf-8');
155
189
  } catch {
156
- return { byModel, skills };
190
+ return { byModel, skills, commands: commandUses };
157
191
  }
158
192
  for (const line of raw.split('\n')) {
159
193
  if (!line.trim()) continue;
@@ -166,7 +200,7 @@ function tallyTranscript(transcriptPath, known) {
166
200
  if (obj.type !== 'assistant') continue;
167
201
  const msg = obj && obj.message;
168
202
  try {
169
- countSkillUses(msg, known, skills);
203
+ countSkillUses(msg, known, skills, commands, commandUses);
170
204
  } catch {
171
205
  /* usage evidence is best-effort — never let it break cost tracking */
172
206
  }
@@ -179,7 +213,7 @@ function tallyTranscript(transcriptPath, known) {
179
213
  m.cache_write += u.cache_creation_input_tokens || 0;
180
214
  m.cache_read += u.cache_read_input_tokens || 0;
181
215
  }
182
- return { byModel, skills };
216
+ return { byModel, skills, commands: commandUses };
183
217
  }
184
218
 
185
219
  async function main() {
@@ -198,7 +232,13 @@ async function main() {
198
232
  } catch {
199
233
  known = new Set();
200
234
  }
201
- const { byModel, skills } = tallyTranscript(transcriptPath, known);
235
+ let installedCommands;
236
+ try {
237
+ installedCommands = knownCommands();
238
+ } catch {
239
+ installedCommands = new Set();
240
+ }
241
+ const { byModel, skills, commands } = tallyTranscript(transcriptPath, known, installedCommands);
202
242
  const modelNames = Object.keys(byModel);
203
243
  if (modelNames.length === 0) return process.exit(0); // nothing to record
204
244
 
@@ -223,9 +263,10 @@ async function main() {
223
263
  total_usd: Number(totalUsd.toFixed(4)),
224
264
  total_tokens: totalTokens,
225
265
  models,
226
- // Always emitted, even empty: `{}` records an observed session with no skill
227
- // reference, while an absent key marks a row written before this existed.
266
+ // Always emitted, even empty: `{}` records an observed session with no
267
+ // reference, while an absent key marks a row written before that existed.
228
268
  skills,
269
+ commands,
229
270
  };
230
271
 
231
272
  try {
@@ -3,9 +3,11 @@
3
3
  * dev-workflow statusline — Claude Code statusLine command.
4
4
  *
5
5
  * Prints one line: git branch · dev-workflow active spec (if any) · minimalism
6
- * mode. Reads the session payload (JSON on stdin) for the working dir; falls
7
- * back to process.cwd(). Fails SAFE — any error prints a minimal line so the
8
- * statusline never breaks the session.
6
+ * mode. Reads the session payload (JSON on stdin): the branch comes from the
7
+ * current dir, the .dw/ state from the project dir, since the shell may sit in a
8
+ * subdirectory. Falls back to $CLAUDE_PROJECT_DIR, then process.cwd(). Fails
9
+ * SAFE — any error prints a minimal line so the statusline never breaks the
10
+ * session.
9
11
  *
10
12
  * Minimalism mode is read from .dw/minimalism.json (see the dw-minimalism skill).
11
13
  */
@@ -93,23 +95,24 @@ function activeSpec(cwd) {
93
95
 
94
96
  async function main() {
95
97
  let cwd = process.cwd();
98
+ let root = process.env.CLAUDE_PROJECT_DIR || '';
96
99
  try {
97
100
  const payload = JSON.parse(await readStdin());
98
- cwd =
99
- (payload && payload.workspace && payload.workspace.current_dir) ||
100
- (payload && payload.cwd) ||
101
- cwd;
101
+ const workspace = (payload && payload.workspace) || {};
102
+ cwd = workspace.current_dir || (payload && payload.cwd) || cwd;
103
+ root = workspace.project_dir || root;
102
104
  } catch {
103
- /* use process.cwd() */
105
+ /* use $CLAUDE_PROJECT_DIR / process.cwd() */
104
106
  }
107
+ root = root || cwd;
105
108
 
106
109
  const parts = [];
107
110
  const branch = gitBranch(cwd);
108
111
  if (branch) parts.push(`⎇ ${branch}`);
109
- const spec = activeSpec(cwd);
112
+ const spec = activeSpec(root);
110
113
  if (spec) parts.push(spec);
111
- parts.push(`min:${minimalismMode(cwd)}`);
112
- const cost = costToday(cwd);
114
+ parts.push(`min:${minimalismMode(root)}`);
115
+ const cost = costToday(root);
113
116
  if (cost) parts.push(cost);
114
117
 
115
118
  process.stdout.write(`dw · ${parts.join(' · ')}`);
@@ -37,7 +37,7 @@ This file describes how `/dw-new-project` and `/dw-dockerize` merge the standalo
37
37
 
38
38
  ```yaml
39
39
  # Generated by /dw-new-project on YYYY-MM-DD
40
- # Edit freely. Recipes live in .agents/skills/docker-compose-recipes/
40
+ # Edit freely. Recipes live in .dw/skills/docker-compose-recipes/
41
41
 
42
42
  services:
43
43
  # --- storage tier ---
@@ -52,7 +52,9 @@ Structure:
52
52
  └── runs/<YYYY-MM-DD>.jsonl # results of each eval run
53
53
  ```
54
54
 
55
- See `references/reference-dataset.md` for case-design principles, sampling from production, and when to expand the set.
55
+ Eval-first: run a baseline on the current code before the change; new-behavior cases must fail there. Compare runs case by case: a case that passed and now fails blocks, whatever the average says. Never tune against the `holdout` split.
56
+
57
+ See `references/reference-dataset.md` for case design, baseline, holdout, per-case drift and when to expand the set.
56
58
 
57
59
  ## Consistency (run-to-run stability)
58
60
 
@@ -1,6 +1,6 @@
1
1
  ## Integration with dev-workflow commands
2
2
 
3
- - `/dw-plan tasks`: when the PRD has an AI feature requirement, an eval-plan subtask is mandatory. The task references this skill's oracle ladder.
3
+ - `/dw-plan tasks`: when the PRD has an AI feature requirement, an eval-plan subtask is mandatory, ordered before the implementation subtasks: behavior spec, cases, baseline run (`references/reference-dataset.md` "Baseline before the change"). The task references this skill's oracle ladder.
4
4
  - `/dw-review --code-only`: AI feature PRs require a reference dataset + ≥2 oracle rungs (lower rungs FIRST). The constitution gate also applies — if the project has principles about AI feature reliability, they're enforced here.
5
- - `/dw-qa --ai`: new mode (when this skill is bundled) — runs the reference dataset against the current implementation, logs to `QA/logs/ai/<feature>-<date>.jsonl`, computes precision@k / faithfulness / outcome accuracy per the feature type.
5
+ - `/dw-qa --ai`: new mode (when this skill is bundled) — runs the reference dataset against the current implementation, logs to `QA/logs/ai/<feature>-<date>.jsonl`, computes precision@k / faithfulness / outcome accuracy per the feature type, and lists per-case flips against the prior run.
6
6
  - `/dw-bugfix` when the bug is an AI failure mode (hallucination, tool misuse, classification error): adds the failing case to the reference dataset BEFORE fixing — the case is now a regression test forever.
@@ -37,6 +37,7 @@ Each case is one line in `cases.jsonl`:
37
37
  ```json
38
38
  {
39
39
  "id": "case-001",
40
+ "split": "dev",
40
41
  "input": {
41
42
  "user_message": "I want to cancel my subscription",
42
43
  "user_context": { "tier": "premium", "tenure_months": 18 }
@@ -60,6 +61,7 @@ Each case is one line in `cases.jsonl`:
60
61
  Fields:
61
62
 
62
63
  - **`id`** — stable identifier (you'll reference it in regression reports).
64
+ - **`split`** — `dev` (you tune against it) or `holdout` (you never do). See "Held-out split".
63
65
  - **`input`** — what the system receives. Match the production input shape exactly.
64
66
  - **`expected`** — for rungs 1-3, the deterministic expected output or state change. For rung 4, the rubric-target (what a 5/5 answer would do).
65
67
  - **`rubric_criteria`** — which rubric dimensions this case exercises.
@@ -129,18 +131,70 @@ Process:
129
131
  4. **Categorize** — difficulty, tags, rubric criteria.
130
132
  5. **Promote** to `cases.jsonl` after review.
131
133
 
134
+ ## Baseline before the change
135
+
136
+ > Eval-first, from LangChain's [eval-driven development](https://www.langchain.com/blog/what-is-eval-driven-development)
137
+ > post (2026-11-09): the eval is written and run before the change it measures.
138
+
139
+ When a task adds or changes an AI behavior, the eval exists before the implementation:
140
+
141
+ 1. **Spec the behavior, one at a time.** Four lines, definite enough to score pass/fail: when it should
142
+ happen, what the system does, what it must not do, and which existing behaviors must keep working.
143
+ "The answer is helpful" is not a spec.
144
+ 2. **Add the cases** for it: the new behavior, its must-not boundary (the near miss that should NOT
145
+ trigger it), and the existing cases that cover the keep-working list.
146
+ 3. **Run the baseline** on the current code, before any change, and save it as
147
+ `runs/<YYYY-MM-DD>-baseline.jsonl`.
148
+ 4. **Read the baseline.** New-behavior cases must fail there. A new case that already passes does not test
149
+ the change: rewrite it or drop it. Existing cases that pass are the regression set for this change.
150
+ 5. Implement, re-run, compare case by case (see "Detecting drift").
151
+
152
+ Each line of a run file is one case:
153
+
154
+ ```json
155
+ {"id": "case-001", "split": "dev", "pass": true, "score": 4, "commit": "a1b2c3d", "dataset_version": "2.3"}
156
+ ```
157
+
158
+ `pass` is required (the verdict against the case's oracle); `score` only when a rung-4 judge produced one.
159
+
160
+ ## Held-out split
161
+
162
+ Iterating until every case passes overfits the system to the dataset: the prompt learns the cases, not the
163
+ behavior. Mark about 20% of cases `"split": "holdout"`, spread across difficulty and tags, and:
164
+
165
+ - iterate against `dev` only — never read a holdout failure to decide the next prompt tweak;
166
+ - run `holdout` before merge to main and before deploy;
167
+ - if `dev` improves and `holdout` stays flat or drops, report overfitting instead of the improvement.
168
+
169
+ A holdout case that was used to tune is no longer held out: move it to `dev` and add a fresh one.
170
+
132
171
  ## Sampling for regression runs
133
172
 
134
173
  You don't need to re-run the entire dataset every time. Smart sampling:
135
174
 
136
- - **PR-time:** random sample of 30% + all "high difficulty" cases + any case added in the last 30 days. Fast feedback.
137
- - **Pre-merge to main:** full dataset.
175
+ - **PR-time:** `dev` split only — random sample of 30% + all "high difficulty" cases + any case added in the last 30 days. Fast feedback.
176
+ - **Pre-merge to main:** full dataset, `holdout` included.
138
177
  - **Nightly:** full dataset + judge re-calibration check.
139
178
  - **Pre-deploy:** full dataset + manual eyeball on 10 random outputs.
140
179
 
141
180
  ## Detecting drift
142
181
 
143
- After each run, compare against the prior run on the SAME cases:
182
+ After each run, compare against the prior run on the SAME cases — **case by case first, averages
183
+ second.** An average can rise while a case that used to pass breaks: the change fixed five things and
184
+ overwrote one behavior someone depends on.
185
+
186
+ ```
187
+ Run 2026-05-08 vs 2026-05-01 (dataset 2.3, 24 cases):
188
+ flipped pass → fail: case-007 (dev), case-019 (holdout)
189
+ flipped fail → pass: case-021, case-022, case-023
190
+ unchanged: 19
191
+ ```
192
+
193
+ The gate: **any case that passed in the prior run and fails now blocks**, whatever the average says, until
194
+ it is fixed or accepted in writing (PR or techspec: case id, why the old behavior no longer holds). A
195
+ fail → pass flip on a case nobody was working on is also worth a look: it may be judge drift, not progress.
196
+
197
+ Then the metric deltas:
144
198
 
145
199
  ```
146
200
  Run 2026-05-08 vs 2026-05-01:
@@ -33,12 +33,13 @@ For the four grounding questions, read `references/the-four-grounding-questions-
33
33
  | Any UI work (the full version of the grounding) | `references/hard-gate.md` |
34
34
  | Reviewing or proposing a design | `references/visual-slop.md` (14 patterns + specific anti-default values) |
35
35
  | Designing state coverage | `references/state-matrix.md` |
36
+ | Stress-testing with worst-case data (long names, `1 members`, 1,000 rows, 320px, 200% zoom) | `references/content-stress.md` |
36
37
  | Interactive widget (button, form, modal, anything clickable) | `references/accessibility-floor.md` |
37
38
  | No design authority exists in the project | `references/curated-defaults.md` (palettes / fonts / scales) |
38
39
  | Choosing or reviewing a palette / dark mode | `references/color-oklch.md` (OKLCH, tinted neutrals, contrast) |
39
40
  | Fonts, sizing, hierarchy, line length | `references/type-scale.md` (modular scale, pairing, measure) |
40
- | Adding animation or transitions | `references/motion.md` (easing, duration, reduced-motion) |
41
- | Cross-device / breakpoints / touch | `references/responsive.md` (mobile-first, container queries) |
41
+ | Adding, reviewing or removing animation | `references/motion.md` (frequency gate, easing, performance, removal, review triggers) |
42
+ | Cross-device / touch / mobile web | `references/responsive.md` (mobile-first, container queries, mobile fixes) |
42
43
  | Any user-facing copy (labels, errors, empty states) | `references/ux-writing.md` |
43
44
 
44
45
  ## The 14 visual-slop patterns (full catalog in `references/visual-slop.md`)
@@ -74,7 +75,8 @@ Before any interactive widget ships:
74
75
  - [ ] No keyboard traps.
75
76
  - [ ] Touch targets ≥24×24 CSS pixels (≥44×44 recommended on mobile).
76
77
  - [ ] Heading hierarchy is semantic (no skipped levels).
77
- - [ ] `prefers-reduced-motion` honored.
78
+ - [ ] `prefers-reduced-motion` honored (gentler, not zero).
79
+ - [ ] Zoom never disabled (no `user-scalable=no` / `maximum-scale=1`); content survives 200% zoom.
78
80
 
79
81
  Full verification recipes in `references/accessibility-floor.md`. `/dw-review --code-only` rejects the verdict if any interactive widget ships without these.
80
82
 
@@ -90,7 +92,8 @@ In all bend cases, document the bend in the PR (one line). "I skipped the state
90
92
 
91
93
  - `/dw-redesign-ui` runs the grounding end-to-end. Steps 4 (propose) and 7 (validate) consult this skill explicitly.
92
94
  - `/dw-plan techspec` UI sections must answer the 4 grounding questions and reference the state matrix.
93
- - `/dw-review --code-only` checks UI diffs against the 14 visual-slop patterns and the accessibility floor.
95
+ - `/dw-review --code-only` checks UI diffs against the 14 visual-slop patterns, the accessibility floor, `motion.md` §9 triggers and `content-stress.md` static signals.
96
+ - `/dw-qa` (UI mode) runs the content-stress pass.
94
97
  - `/dw-functional-doc` records the surface-job and scene sentences in the overview for each screen.
95
98
 
96
99
  ## Why this approach
@@ -108,5 +111,5 @@ When invoked directly or by a harness, return or merge this block:
108
111
  - **Evidence:** design source read, four grounding answers, states checked, screenshots, and accessibility checks.
109
112
  - **Artifacts:** UI proposal, design notes, state matrix, screenshot path, or QA finding.
110
113
  - **Decisions:** palette/type/component choices, state handling, and accepted deviations.
111
- - **Risks:** training-data defaults, missing states, contrast/focus failures, unreadable hierarchy, or mobile overflow.
114
+ - **Risks:** training-data defaults, missing states, contrast/focus failures, unreadable hierarchy, worst-case data breaks, high-frequency motion, or mobile overflow.
112
115
  - **Next Step:** exact design/code/QA action or missing input to gather.
@@ -152,24 +152,34 @@ Screen readers switch pronunciation based on this.
152
152
 
153
153
  ### 10. Reduced motion respect
154
154
 
155
- Honor `prefers-reduced-motion`:
155
+ Honor `prefers-reduced-motion`. It means fewer and gentler animations, not zero: drop movement (slide,
156
+ scale, parallax), keep the opacity/color changes that show a state change.
156
157
 
157
158
  ```css
158
159
  @media (prefers-reduced-motion: reduce) {
159
- *, *::before, *::after {
160
- animation-duration: 0.01ms !important;
161
- transition-duration: 0.01ms !important;
162
- }
160
+ .drawer { transition: opacity 200ms ease; transform: none; }
163
161
  }
164
162
  ```
165
163
 
166
164
  Or in Tailwind:
167
165
  ```html
168
- <div className="transition-all motion-reduce:transition-none">
166
+ <div className="transition-transform motion-reduce:transition-none">
169
167
  ```
170
168
 
169
+ A global `animation-duration: 0.01ms !important` reset is an acceptable project-wide fallback, as long as
170
+ every state change stays visible. Details in `motion.md` §7.
171
+
171
172
  Vestibular disorders trigger on parallax, large slide-ins, infinite scroll animations.
172
173
 
174
+ ### 11. Zoom is never disabled
175
+
176
+ `user-scalable=no` and `maximum-scale=1` in the viewport meta block pinch-zoom for low-vision users (WCAG
177
+ 1.4.4 Resize Text). They are usually added to stop iOS zooming into inputs; fix the cause instead —
178
+ inputs at `font-size: 16px` or more (`responsive.md`, "Mobile platform fixes").
179
+
180
+ Content must also survive **200% browser zoom** without clipping or overlapping: fixed heights on text
181
+ boxes are the usual culprit (`content-stress.md`, Step 4).
182
+
173
183
  ## Verification recipes
174
184
 
175
185
  ### Automated (CI)
@@ -195,8 +205,9 @@ Run on every PR. Block merge on violations.
195
205
  1. Open the changed page.
196
206
  2. Tab through with keyboard only. Verify focus visible at every stop.
197
207
  3. Run axe DevTools, fix violations.
198
- 4. Toggle prefers-reduced-motion in DevTools rendering panel. Verify animations stop.
199
- 5. Test with screen reader (VoiceOver on Mac, NVDA on Windows) on at least one critical flow.
208
+ 4. Toggle prefers-reduced-motion in DevTools rendering panel. Verify movement stops and state changes stay visible.
209
+ 5. Zoom the page to 200%. Verify nothing clips, overlaps or becomes unreachable.
210
+ 6. Test with screen reader (VoiceOver on Mac, NVDA on Windows) on at least one critical flow.
200
211
 
201
212
  ### Spot checks
202
213
 
@@ -221,5 +232,6 @@ LLM-produced UI commonly fails on:
221
232
  - `outline: none` with no replacement focus state.
222
233
  - Modals that trap focus only on success path, not on error.
223
234
  - Auto-playing carousels with no pause control.
235
+ - `user-scalable=no` / `maximum-scale=1` in the viewport meta.
224
236
 
225
237
  `/dw-review --code-only` runs axe-style checks on the diff for these.