@polymorphism-tech/morph-spec 7.6.0 → 7.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 (42) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +18 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +12 -4
  7. package/framework/MORPH.md +18 -10
  8. package/framework/agents.json +12 -4
  9. package/framework/hooks/README.md +8 -10
  10. package/framework/hooks/claude-code/core/gate-guard.js +36 -16
  11. package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
  12. package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
  13. package/framework/hooks/shared/tasks-validator.js +68 -0
  14. package/framework/mcp/registry.json +5 -48
  15. package/framework/rubrics/backend.json +3 -3
  16. package/framework/rubrics/default.json +3 -3
  17. package/framework/rubrics/frontend.json +3 -3
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  20. package/framework/skills/core/morph-apply/SKILL.md +53 -40
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
  22. package/framework/skills/core/morph-eval/SKILL.md +10 -8
  23. package/framework/skills/core/morph-implement/SKILL.md +27 -21
  24. package/framework/skills/core/morph-plan/SKILL.md +12 -21
  25. package/framework/skills/core/morph-proposal/SKILL.md +3 -8
  26. package/framework/skills/core/morph-review/SKILL.md +30 -14
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/integration/mcp/mcp-tools.md +38 -43
  30. package/package.json +1 -1
  31. package/src/commands/project/doctor.js +36 -1
  32. package/src/commands/state/approve.js +29 -4
  33. package/src/commands/state/score.js +88 -0
  34. package/src/commands/tasks/dag.js +68 -0
  35. package/src/dashboard/scanner/rubrics-scanner.js +1 -1
  36. package/src/lib/generators/recap-generator.js +1 -1
  37. package/src/lib/tasks/dispatch-planner.js +201 -0
  38. package/src/lib/validators/score-aggregator.js +3 -3
  39. package/src/scripts/setup-infra.js +42 -0
  40. package/src/utils/agents-installer.js +142 -10
  41. package/src/utils/hooks-installer.js +13 -5
  42. package/framework/rubrics/eval-rubric.json +0 -66
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
6
6
  user-invokable: true
7
- cliVersion: "7.6.0"
7
+ cliVersion: "7.7.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -42,7 +42,7 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
42
42
 
43
43
  4. Produza `tasks.json` em `.morph/features/{feature}/2-plan/` seguindo o schema em `.morph/framework/schemas/tasks.schema.json`.
44
44
 
45
- **Formato obrigatório** (validado pelo parser em `src/lib/tasks/tasks-json-parser.js`):
45
+ **Formato obrigatório** (schema em `.morph/framework/schemas/tasks.schema.json` — o hook `tasks-json-guard` valida cada write):
46
46
 
47
47
  ```json
48
48
  {
@@ -72,27 +72,17 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
72
72
  - `group` (opcional): backend, frontend, tests, infra, docs. Default backend.
73
73
  - `outputs` (opcional): array de paths que serão criados/modificados.
74
74
 
75
- Antes de retornar, validar com:
76
- ```js
77
- import { validateTasksJson, writeTasksJson } from '../../../src/lib/tasks/tasks-json-parser.js';
78
- const { valid, errors } = validateTasksJson(data);
79
- if (!valid) throw new Error(errors.join('\n'));
80
- await writeTasksJson(tasksPath, data);
81
- ```
75
+ Antes de retornar, valide rodando `morph-spec dag {feature}` — o comando parseia e valida o schema do tasks.json (ids únicos `T\d+`, enums de `status`/`effort`, `dependencies` válidas) e já imprime a análise de DAG que a `/morph-apply` vai usar. Erros de schema abortam com a lista do que corrigir.
82
76
 
83
77
  5. Apresente o Gate 2 segundo trust mode.
84
78
 
85
- Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular sinais de risco:
79
+ Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular os sinais de risco em linguagem natural — sem scripts:
86
80
 
87
- ```js
88
- import { shouldPauseGate, getRiskSignals } from '../../../src/lib/risk-detector.js';
89
- const signals = {
90
- taskCount: data.tasks.length,
91
- designSystemDivergence: /* herdado do brainstorming */,
92
- standardsViolations: /* detectado em mandate */,
93
- };
94
- const { pause, reason } = shouldPauseGate(config, signals);
95
- ```
81
+ - `taskCount` = número de tasks do tasks.json.
82
+ - `designSystemDivergence` = herdado do brainstorming.
83
+ - `standardsViolations` = violações detectadas no mandate.
84
+
85
+ **Decisão de pausa** (regras da tabela do MORPH.md §5): `trust: manual` → pausa sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; `taskCount > 20` → pausa; caso contrário → auto-passa (`pause: false`).
96
86
 
97
87
  **Persista a decisão no `feature.json` autoritativo da feature ANTES de apresentar ou auto-aprovar** — grave
98
88
  `gateDecisions.plan = { pause, reason, signals, trust, decidedAt: <ISO> }` em `.morph/features/{feature}/feature.json`
@@ -137,5 +127,6 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
137
127
  - `.morph/framework/schemas/tasks.schema.json` — schema obrigatório de tasks.json
138
128
  - `.morph/features/{feature}/0-proposal/proposal.md` — escopo da feature
139
129
  - `.morph/features/{feature}/0-proposal/stack-scan.md` — contexto técnico detectado
140
- - `src/lib/tasks/tasks-json-parser.js` — `validateTasksJson`, `writeTasksJson`
141
- - `src/lib/risk-detector.js` — `shouldPauseGate`, `getRiskSignals`
130
+ - `.morph/framework/schemas/tasks.schema.json` — schema e enums de tasks.json
131
+ - `morph-spec dag {feature}` — valida o tasks.json e imprime a análise de DAG
132
+ - `.morph/framework/MORPH.md` §5 — regras de pausa de gate (trust mode)
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
6
6
  user-invokable: true
7
- cliVersion: "7.6.0"
7
+ cliVersion: "7.7.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -52,12 +52,7 @@ Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular sinais de
52
52
  - `designSystemDivergence` — detectado pela morph-brainstorming.
53
53
  - `standardsViolations` — violações de mandate listadas na proposal (não deveria, mas verifica).
54
54
 
55
- Usar `src/lib/risk-detector.js`:
56
-
57
- ```js
58
- import { shouldPauseGate } from '../../../src/lib/risk-detector.js';
59
- const { pause, reason } = shouldPauseGate(config, signals);
60
- ```
55
+ **Decisão de pausa** (regras da tabela do MORPH.md §5, aplicadas em linguagem natural — sem scripts): `trust: manual` → `pause: true` sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; caso contrário → `pause: false` (auto-passa). Monte `reason` com o sinal que disparou.
61
56
 
62
57
  **Se `pause === true`**, apresentar via `AskUserQuestion`:
63
58
 
@@ -136,4 +131,4 @@ Lógica idêntica ao passo 3 (`shouldPauseGate` + AskUserQuestion ou auto-aprova
136
131
  - `.morph/framework/MORPH.md` — charter NLH completo
137
132
  - `framework/skills/core/morph-brainstorming/SKILL.md`
138
133
  - `framework/skills/core/morph-plan/SKILL.md`
139
- - `src/lib/risk-detector.js` — `shouldPauseGate`, `getRiskSignals`
134
+ - `.morph/framework/MORPH.md` §5 — regras de pausa de gate (trust mode)
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
6
6
  user-invokable: true
7
- cliVersion: "7.6.0"
7
+ cliVersion: "7.7.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -33,7 +33,27 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
33
33
  - Se não atendido: registre como "bloqueante" com descrição do gap.
34
34
  - Se parcialmente atendido: registre como "pendência" com o que falta.
35
35
 
36
- 4. Execute morph-eval na implementação completa (fase, não tarefa) para obter scores por dimensão.
36
+ 4. **Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
37
+ - Rode a suíte de testes do projeto e capture a **saída real** (não um resumo seu).
38
+ - Gere o **diff da implementação** (`git diff` do range da feature, ou a lista de arquivos do recap.md com seus conteúdos atuais).
39
+ - Despache o subagent registrado `morph-evaluator` (gerado de `agents.json` — a persona do avaliador é o system prompt dele, com tools read-only), com contexto fresco:
40
+ ```
41
+ dispatch Agent (subagent_type: "morph-evaluator") with prompt:
42
+ contexto: mandate.md + spec.md + diff da implementação + saída real dos testes
43
+ instrução: |
44
+ Você NÃO participou da implementação — avalie apenas o que está no diff,
45
+ contra o mandate. Aplique as rubricas em .morph/framework/evals/rubrics/
46
+ (architecture 30%, contracts 25%, code-quality 25%, test-coverage 20%)
47
+ seguindo as bandas 0-10 de cada uma.
48
+ NÃO reescreva código. NÃO toque em tasks.json, feature.json nem no board
49
+ de sessão. Ao concluir, ENTREGUE o relatório final — terminar idle sem
50
+ reportar é falha de protocolo.
51
+ Retorne: score 0-10 por dimensão, score composto (arch×0.30 + contracts×0.25
52
+ + quality×0.25 + tests×0.20), e feedback priorizado (bloqueantes primeiro)
53
+ para qualquer dimensão < 9.
54
+ ```
55
+ - **Fallback (tipo não registrado):** se `morph-evaluator` não estiver na sua lista de subagent types, despache `general-purpose` colando o campo `persona` do agente `evaluator` (Read em `.morph/framework/agents.json`) no topo do prompt — e **registre o fallback no review-report** (seção de método).
56
+ - Os scores por dimensão e o composto do review-report vêm **desse retorno**, não da sua auto-avaliação. Se o avaliador ficar idle sem reportar, cobre via SendMessage antes de prosseguir.
37
57
 
38
58
  5. Produza `review-report.md` em `.morph/features/{feature}/4-review/` com:
39
59
  - Scores por dimensão (arquitetura, contratos, qualidade, cobertura)
@@ -55,16 +75,12 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
55
75
 
56
76
  8. Apresente o Gate 3 segundo trust mode.
57
77
 
58
- Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular sinais de risco:
78
+ Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular os sinais de risco em linguagem natural — sem scripts:
59
79
 
60
- ```js
61
- import { shouldPauseGate } from '../../../src/lib/risk-detector.js';
62
- const signals = {
63
- evalScore: phaseScores.composite,
64
- standardsViolations: blockingItems.filter(b => b.kind === 'standard').map(b => b.id),
65
- };
66
- const { pause, reason } = shouldPauseGate(config, signals);
67
- ```
80
+ - `evalScore` = score composto retornado pelo **avaliador independente** (passo 4).
81
+ - `standardsViolations` = ids dos itens bloqueantes de standard encontrados no passo 3.
82
+
83
+ **Decisão de pausa** (mesmas regras da tabela do MORPH.md §5): `trust: manual` → `pause: true` sempre; `standardsViolations` não-vazio → `pause: true`; `evalScore < 9` → `pause: true`; caso contrário → `pause: false` (auto-passa).
68
84
 
69
85
  **Se `pause === true`**, use `AskUserQuestion`:
70
86
 
@@ -96,11 +112,11 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
96
112
  - Gate 3 registrado aprovado em `.morph/features/{feature}/feature.json` (autoritativo), na forma canônica `{ approved, timestamp, approvedBy }` — via `morph-spec approve` em auto, ou pelo hook state-sync no caminho manual
97
113
 
98
114
  ## Referências
99
- - `.morph/framework/MORPH.md` — seção 5 (Gate 3), seção 1 (papel Evaluator), Trust Mode
100
- - `framework/skills/core/morph-eval/SKILL.md` — para calcular scores por dimensão
115
+ - `.morph/framework/MORPH.md` — seção 5 (Gate 3 + regras de pausa), seção 1 (papel Evaluator), Trust Mode
116
+ - `.claude/agents/morph-evaluator.md` — subagent do avaliador (gerado de `.morph/framework/agents.json`), despachado no passo 4
117
+ - `framework/skills/core/morph-eval/SKILL.md` — rubricas e bandas que o avaliador aplica
101
118
  - `.morph/framework/evals/trace-schema.md` — formato canônico do trace
102
119
  - `.morph/features/{feature}/2-plan/mandate.md` — critérios de done da feature
103
120
  - `.morph/features/{feature}/2-plan/spec.md` — arquitetura e contratos
104
121
  - `.morph/features/{feature}/2-plan/decisions.md` — ADRs registrados (se existir)
105
122
  - `.morph/features/{feature}/3-implement/recap.md` — registro de execução
106
- - `src/lib/risk-detector.js` — `shouldPauseGate` para Gate 3
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
4
4
  version: core
5
5
  when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
6
6
  user-invokable: true
7
- cliVersion: "7.6.0"
7
+ cliVersion: "7.7.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
4
4
  version: core
5
5
  when-to-invoke: quando precisar consultar um standard específico durante a implementação
6
6
  user-invokable: true
7
- cliVersion: "7.6.0"
7
+ cliVersion: "7.7.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** mcp, model-context-protocol, mcp-tools, context7, playwright, mcp-client
6
6
  > **Load When:** an MCP tool is used in any morph-spec phase
7
7
 
8
- **Verified against:** Claude Code MCP tooling — context7, Playwright, GitHub, Neon CLI. Last-verified: 2026-05-20.
8
+ **Verified against:** Claude Code MCP tooling — context7, Playwright; GitHub via `gh` CLI; Neon via `neonctl`. Last-verified: 2026-07-08.
9
9
 
10
10
  ---
11
11
 
@@ -19,8 +19,8 @@ Before using any MCP tool, check if it's available in the current session:
19
19
  // Pattern: Attempt the call — if the tool doesn't exist, Claude Code will report it
20
20
  // There is no "list MCPs" command; just try the most common operation
21
21
 
22
- // GitHub MCP
23
- await mcp__github__get_repo();
22
+ // GitHub — o morph-spec NÃO usa MCP do GitHub: use o `gh` CLI via Bash
23
+ // gh repo view --json name,defaultBranchRef,languages
24
24
 
25
25
  // Context7 (library docs)
26
26
  await mcp__context7__resolve_library_id({ libraryName: "react" });
@@ -41,18 +41,19 @@ await mcp__playwright__browser_navigate({ url: "https://example.com" });
41
41
 
42
42
  ### Phase 1 (Setup) — Project Discovery
43
43
 
44
- | MCP | Use Case | Example |
45
- |-----|----------|---------|
46
- | **GitHub** | Repo info, recent PRs, issues | `mcp__github__get_repo()` |
44
+ | Tool | Use Case | Example |
45
+ |------|----------|---------|
46
+ | **GitHub CLI (`gh`)** | Repo info, recent PRs, issues | `gh repo view --json ...` (Bash) |
47
47
  | **Filesystem** | Scan project structure | Native Glob/Read preferred |
48
48
 
49
- ```javascript
50
- // Detect project type from repo metadata
51
- const repo = await mcp__github__get_repo();
52
- // → language, default_branch, topics
49
+ ```bash
50
+ # Detect project type from repo metadata (gh CLI via Bash — sem MCP)
51
+ gh repo view --json name,defaultBranchRef,languages,repositoryTopics
52
+ gh pr list --limit 10 --json title,state,updatedAt
53
+ gh issue list --limit 10 --json title,labels
53
54
 
54
- // Fallback: use Glob to detect stack
55
- // Glob: "**/{package.json,*.csproj,*.sln,go.mod,Cargo.toml}"
55
+ # Fallback: use Glob to detect stack
56
+ # Glob: "**/{package.json,*.csproj,*.sln,go.mod,Cargo.toml}"
56
57
  ```
57
58
 
58
59
  ### Phase 2 (UI/UX) — Design References
@@ -143,7 +144,7 @@ await mcp__gemini_image_mcp__continue_editing({
143
144
  |------|----------|---------|
144
145
  | **Neon CLI (`neonctl`) / psql** | Database schema, tables, relationships, RLS | `psql <DB_URL> -c "SELECT ..."` |
145
146
  | **Context7** | Library docs for architecture decisions | `mcp__context7__query_docs()` |
146
- | **GitHub** | Check existing code patterns, PRs | `mcp__github__search_code()` |
147
+ | **GitHub CLI (`gh`)** | Check existing code patterns, PRs | `gh search code` (Bash) |
147
148
 
148
149
  ```bash
149
150
  # === SCHEMA ANALYSIS (Critical for contracts.cs) ===
@@ -183,7 +184,7 @@ psql $(neonctl connection-string) -c "SELECT tablename, policyname, cmd, qual FR
183
184
  | MCP | Use Case | Example |
184
185
  |-----|----------|---------|
185
186
  | **Context7** | Verify library capabilities, API limits | `mcp__context7__query_docs()` |
186
- | **GitHub** | Check issue discussions, known limitations | `mcp__github__search_issues()` |
187
+ | **GitHub CLI (`gh`)** | Check issue discussions, known limitations | `gh search issues` (Bash) |
187
188
 
188
189
  ```javascript
189
190
  // Verify if library supports a required feature
@@ -198,24 +199,17 @@ const docs = await mcp__context7__query_docs({
198
199
 
199
200
  ### Phase 6 (Tasks) — Planning & Organization
200
201
 
201
- | MCP | Use Case | Example |
202
- |-----|----------|---------|
203
- | **GitHub** | Create issues from tasks, link to milestone | `mcp__github__create_issue()` |
202
+ | Tool | Use Case | Example |
203
+ |------|----------|---------|
204
+ | **GitHub CLI (`gh`)** | Create issues from tasks, link to milestone | `gh issue create` (Bash) |
204
205
  | **Context7** | Estimate complexity based on library docs | `mcp__context7__query_docs()` |
205
206
 
206
- ```javascript
207
- // Create GitHub issues from tasks.json (if team uses GitHub Projects)
208
- for (const task of tasks) {
209
- await mcp__github__create_issue({
210
- title: task.title,
211
- body: task.description,
212
- labels: [task.category],
213
- milestone: featureMilestone
214
- });
215
- }
216
-
217
- // Fallback: Bash with gh CLI
218
- // gh issue create --title "T001: Create Entity Lead" --label "domain"
207
+ ```bash
208
+ # Create GitHub issues from tasks.json (if team uses GitHub Projects)
209
+ # — one `gh issue create` per task, iterating tasks.json:
210
+ gh issue create --title "T001: Create Entity Lead" \
211
+ --body "description + doneCriteria da task" \
212
+ --label "backend" --milestone "feature-x"
219
213
  ```
220
214
 
221
215
  ### Phase 7 (Implement) — Build & Deploy
@@ -223,7 +217,7 @@ for (const task of tasks) {
223
217
  | Tool | Use Case | Example |
224
218
  |------|----------|---------|
225
219
  | **Neon CLI (`neonctl`) / psql** | Run migrations, create RLS policies | `dotnet ef database update` |
226
- | **GitHub** | Create PR, push branches | `mcp__github__create_pull_request()` |
220
+ | **GitHub CLI (`gh`)** | Create PR, push branches | `gh pr create` (Bash) |
227
221
  | **Context7** | Look up API usage during coding | `mcp__context7__query_docs()` |
228
222
  | **Playwright** | Smoke test deployed features, verify UI | `mcp__playwright__browser_navigate()` |
229
223
  | **Vercel** | Deploy, check logs, manage env vars | `mcp__vercel__list_projects()` |
@@ -283,7 +277,7 @@ await mcp__vercel__manage_env_vars({
283
277
  target: ['production', 'preview']
284
278
  });
285
279
 
286
- // Fallback: Bash for gh CLI for PRs, vercel CLI for deployments
280
+ // PRs/issues: sempre `gh` CLI (Bash). Deployments fallback: vercel CLI
287
281
  ```
288
282
 
289
283
  ---
@@ -293,7 +287,7 @@ await mcp__vercel__manage_env_vars({
293
287
  | Need | MCP Tool | Native Alternative |
294
288
  |------|----------|--------------------|
295
289
  | Database schema | Neon CLI (`neonctl`) / psql | Grep queries + Read types |
296
- | Repo metadata | GitHub MCP | Bash `gh` CLI |
290
+ | Repo metadata / issues / PRs | Bash `gh` CLI | WebFetch `api.github.com` (read-only) |
297
291
  | Library docs | Context7 | WebSearch + WebFetch |
298
292
  | Live page preview | Playwright MCP | WebFetch URL |
299
293
  | Page interaction (click, type, navigate) | Playwright MCP | Manual testing |
@@ -304,7 +298,7 @@ await mcp__vercel__manage_env_vars({
304
298
  | Vercel deployments | Vercel MCP | Bash `vercel` CLI |
305
299
  | AI image generation | Gemini Image MCP | Manual creation or stock photos |
306
300
 
307
- **Rule:** MCP tools provide structured data (JSON responses). Native tools require manual parsing. **Always prefer MCP when available** — fall back to native when not. **Exceptions:** Neon, Docker, and Azure always use their respective CLIs directly — never MCP.
301
+ **Rule:** MCP tools provide structured data (JSON responses). Native tools require manual parsing. **Always prefer MCP when available** — fall back to native when not. **Exceptions:** Neon, Docker, Azure **and GitHub** always use their respective CLIs directly — never MCP (`gh` já retorna JSON estruturado via `--json`, sem Docker nem PAT em env var).
308
302
 
309
303
  ---
310
304
 
@@ -445,16 +439,17 @@ const docs = await mcp__context7__query_docs({
445
439
  });
446
440
  ```
447
441
 
448
- ### GitHub: Code Search Across Repo
442
+ ### GitHub: Code Search Across Repo (`gh` CLI — sem MCP)
449
443
 
450
- > **v4 config:** Docker — `{ "command": "docker", "args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"] }`. Requires Docker Desktop.
444
+ O morph-spec **não instala nem usa MCP do GitHub**. O `gh` CLI (autenticado uma vez via `gh auth login`) cobre a mesma superfície com JSON estruturado, sem Docker e sem PAT em variável de ambiente:
451
445
 
452
- ```javascript
453
- // Find patterns in large codebase during Phase 2
454
- const results = await mcp__github__search_code({
455
- query: "DbSet leads repo:myorg/myrepo",
456
- });
457
- // → Find all places that query the leads table
446
+ ```bash
447
+ # Find patterns in large codebase during Phase 2
448
+ gh search code "DbSet leads" --repo myorg/myrepo --json path,textMatches
449
+ # → Find all places that query the leads table
450
+
451
+ # Qualquer endpoint REST sem subcomando dedicado:
452
+ gh api repos/{owner}/{repo}/branches --jq '.[].name'
458
453
  ```
459
454
 
460
455
  ---
@@ -463,7 +458,7 @@ const results = await mcp__github__search_code({
463
458
 
464
459
  - **Never pass secrets** in MCP tool parameters (API keys, tokens, passwords)
465
460
  - **Neon** uses neonctl/psql — use database branches for development, not production
466
- - **GitHub MCP** inherits the configured token's permissions
461
+ - **GitHub via `gh` CLI** herda as permissões do login do `gh auth login` (keyring) — sem PAT em env var, sem Docker
467
462
  - **Always validate tool responses** before using in contracts or code generation
468
463
 
469
464
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -12,7 +12,7 @@ import {
12
12
  getInstalledCLIVersion
13
13
  } from '../../utils/version-checker.js';
14
14
  import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
15
- import { checkMissingAgents } from '../../utils/agents-installer.js';
15
+ import { checkMissingAgents, checkAgentsDrift } from '../../utils/agents-installer.js';
16
16
  import { getProjectTags } from '../../lib/stack-filter.js';
17
17
 
18
18
  const isWindows = platform() === 'win32';
@@ -186,6 +186,41 @@ async function doctorFullCommand(frameworkRoot) {
186
186
  } catch { /* ignore parse errors — already caught above */ }
187
187
  }
188
188
 
189
+ // ── 6b. Generated personas drift (.claude/agents ↔ agents.json) ─────────
190
+ // agents.json is the single source of truth; the .claude/agents/morph-*.md
191
+ // files are derived artifacts (ADR-001). Missing = broken dispatch (error);
192
+ // stale/hand-edited or orphan = drift (warning).
193
+ if (await pathExists(agentsPath)) {
194
+ console.log(chalk.cyan('\n .claude/agents/ — Generated personas (drift vs agents.json)'));
195
+ try {
196
+ let projectTags = [];
197
+ const cfgPath = join(frameworkRoot, '.morph/config/config.json');
198
+ if (await pathExists(cfgPath)) {
199
+ try { projectTags = getProjectTags(JSON.parse(await fs.readFile(cfgPath, 'utf8'))); } catch { /* no tags */ }
200
+ }
201
+ const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags);
202
+ check(`Personas installed (${drift.expected - drift.missing.length}/${drift.expected})`,
203
+ drift.missing.length === 0, false,
204
+ drift.missing.length > 0 ? `missing: ${drift.missing.join(', ')} — run 'morph-spec setup-infra --reinstall'` : '');
205
+ check('Personas up-to-date (no drift)', drift.stale.length === 0, true,
206
+ drift.stale.length > 0 ? `stale/hand-edited: ${drift.stale.join(', ')} — regenerate via 'morph-spec setup-infra --reinstall'` : '');
207
+ check('No orphan morph-* agents', drift.orphans.length === 0, true,
208
+ drift.orphans.length > 0 ? `orphans: ${drift.orphans.join(', ')}` : '');
209
+ } catch {
210
+ check('personas drift check', false, true, 'failed to compute');
211
+ }
212
+ }
213
+
214
+ // ── 6c. GitHub tooling — gh CLI (canonical; morph-spec does not use a GitHub MCP) ──
215
+ console.log(chalk.cyan('\n Tooling — GitHub via gh CLI'));
216
+ try {
217
+ execSync('gh --version', { stdio: 'pipe', timeout: 5000 });
218
+ check('gh CLI available', true);
219
+ } catch {
220
+ check('gh CLI available', false, true,
221
+ "GitHub ops (issues/PRs/releases) usam o gh CLI — instale https://cli.github.com e rode 'gh auth login'");
222
+ }
223
+
189
224
  // ── 7. state.json schema version ────────────────────────────────────────
190
225
  console.log(chalk.cyan('\n .morph/state.json — Schema Version'));
191
226
  const statePath = join(frameworkRoot, '.morph/state.json');
@@ -1,6 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory } from '../../core/state/state-manager.js';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
4
6
 
5
7
  /**
6
8
  * Unapprove action function - Revoke a previously approved gate
@@ -158,12 +160,23 @@ export async function approvalStatusCommand(featureName, options = {}) {
158
160
  process.exit(1);
159
161
  }
160
162
 
163
+ // v5 removed the stored feature.phase — derive it from the filesystem +
164
+ // approved gates (same as `morph-spec status`), never print "unknown".
165
+ const featurePath = join(process.cwd(), '.morph', 'features', featureName);
166
+ const phase = feature.phase || derivePhase(featurePath);
167
+
168
+ // The uiux branch is optional: a feature that never took it has no
169
+ // 1-design/ folder and no uiux approval — that is "n/a", not "rejected".
170
+ const uiuxGate = getApprovalGate(featureName, 'uiux');
171
+ const uiuxTaken = uiuxGate?.approved === true || existsSync(join(featurePath, '1-design'));
172
+
161
173
  // JSON output
162
174
  if (options.json) {
163
175
  const status = {
164
176
  feature: featureName,
165
- phase: feature.phase,
177
+ phase,
166
178
  status: feature.status,
179
+ uiuxTaken,
167
180
  gates: {}
168
181
  };
169
182
 
@@ -179,7 +192,7 @@ export async function approvalStatusCommand(featureName, options = {}) {
179
192
  console.log('━'.repeat(60));
180
193
 
181
194
  // Show current phase
182
- console.log(chalk.cyan(`\nCurrent Phase: ${feature.phase || 'unknown'}`));
195
+ console.log(chalk.cyan(`\nCurrent Phase: ${phase}`));
183
196
  console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
184
197
 
185
198
  // Show each gate
@@ -193,6 +206,11 @@ export async function approvalStatusCommand(featureName, options = {}) {
193
206
  continue;
194
207
  }
195
208
 
209
+ if (gateName === 'uiux' && !uiuxTaken) {
210
+ console.log(` ${chalk.gray('—')} ${gateName}: ${chalk.gray('n/a (ramo de design não tomado)')}`);
211
+ continue;
212
+ }
213
+
196
214
  if (gateStatus.approved) {
197
215
  console.log(` ${chalk.green('✓')} ${gateName}: ${chalk.green('Approved')}`);
198
216
  if (gateStatus.timestamp) {
@@ -202,7 +220,14 @@ export async function approvalStatusCommand(featureName, options = {}) {
202
220
  console.log(chalk.gray(` Approved by: ${gateStatus.approvedBy}`));
203
221
  }
204
222
  } else {
205
- console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Not Approved')}`);
223
+ // Distinguish "never evaluated" (initialized approved:false, no
224
+ // timestamp) from an explicit rejection/revocation (has timestamp).
225
+ const neverEvaluated = !gateStatus.timestamp && !gateStatus.rejectedAt && !gateStatus.revokedAt;
226
+ if (neverEvaluated) {
227
+ console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Pending (not yet evaluated)')}`);
228
+ continue;
229
+ }
230
+ console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Revoked')}`);
206
231
  if (gateStatus.reason) {
207
232
  console.log(chalk.gray(` Reason: ${gateStatus.reason}`));
208
233
  }
@@ -0,0 +1,88 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
+
6
+ /** Fixed --dims order: a,c,q,t (mirrors the morph-eval dimension weights). */
7
+ const DIM_KEYS = ['architecture', 'contracts', 'codeQuality', 'testCoverage'];
8
+
9
+ function parseScoreValue(raw, label) {
10
+ const value = Number(raw);
11
+ if (!Number.isFinite(value) || value < 0 || value > 10) {
12
+ console.error(chalk.red(`\n❌ Invalid ${label}: "${raw}" — expected a number between 0 and 10\n`));
13
+ process.exit(1);
14
+ }
15
+ return value;
16
+ }
17
+
18
+ /**
19
+ * `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` — record a
20
+ * task eval score under `taskScores` in the authoritative feature.json.
21
+ *
22
+ * The timestamp is stamped server-side (`updatedAt`), the write is atomic
23
+ * (state-manager), attempts auto-increment and bestScore is kept — this
24
+ * replaces the fragile manual Edit of feature.json from the skills.
25
+ */
26
+ export async function scoreCommand(featureName, taskId, scoreArg, options = {}) {
27
+ try {
28
+ const score = parseScoreValue(scoreArg, 'score');
29
+
30
+ const feature = getFeature(featureName);
31
+ if (!feature) {
32
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
33
+ process.exit(1);
34
+ }
35
+
36
+ let dimensions;
37
+ if (options.dims) {
38
+ const parts = String(options.dims).split(',').map(s => s.trim());
39
+ if (parts.length !== DIM_KEYS.length) {
40
+ console.error(chalk.red(`\n❌ --dims expects ${DIM_KEYS.length} comma-separated values in order a,c,q,t (architecture,contracts,code-quality,test-coverage)\n`));
41
+ process.exit(1);
42
+ }
43
+ dimensions = {};
44
+ DIM_KEYS.forEach((key, i) => {
45
+ dimensions[key] = parseScoreValue(parts[i], `--dims ${key}`);
46
+ });
47
+ }
48
+
49
+ // Non-fatal sanity check: warn when the task id is unknown to tasks.json.
50
+ const tasksPath = join(process.cwd(), '.morph', 'features', featureName, '2-plan', 'tasks.json');
51
+ if (existsSync(tasksPath)) {
52
+ try {
53
+ const tasks = JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
54
+ if (!tasks.some(t => t.id === taskId)) {
55
+ console.error(chalk.yellow(`⚠️ Task "${taskId}" not found in tasks.json — recording anyway`));
56
+ }
57
+ } catch {
58
+ // unreadable tasks.json — the score is still worth recording
59
+ }
60
+ }
61
+
62
+ const prev = feature.taskScores?.[taskId];
63
+ const entry = {
64
+ lastScore: score,
65
+ bestScore: prev?.bestScore != null ? Math.max(prev.bestScore, score) : score,
66
+ attempts: (prev?.attempts ?? 0) + 1,
67
+ ...(dimensions ? { dimensions } : {}),
68
+ updatedAt: new Date().toISOString(),
69
+ };
70
+
71
+ await updateFeature(featureName, `taskScores.${taskId}`, entry);
72
+
73
+ if (options.json) {
74
+ console.log(JSON.stringify({ feature: featureName, task: taskId, ...entry }, null, 2));
75
+ return;
76
+ }
77
+
78
+ console.log(chalk.green(`\n✅ Score recorded: ${featureName}/${taskId} → ${entry.lastScore}`));
79
+ console.log(chalk.gray(` best: ${entry.bestScore} | attempts: ${entry.attempts} | updatedAt: ${entry.updatedAt}`));
80
+ if (dimensions) {
81
+ console.log(chalk.gray(` dims: arch=${dimensions.architecture} contracts=${dimensions.contracts} quality=${dimensions.codeQuality} tests=${dimensions.testCoverage}`));
82
+ }
83
+ console.log('');
84
+ } catch (error) {
85
+ console.error(chalk.red(`\n❌ Error recording score: ${error.message}\n`));
86
+ process.exit(1);
87
+ }
88
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { parseTasksJson } from '../../lib/tasks/tasks-json-parser.js';
5
+ import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
6
+
7
+ /**
8
+ * `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
9
+ * sequential vs parallel dispatch decision, weighted by effort.
10
+ *
11
+ * This is the command morph-apply §2 runs instead of re-deriving the rules
12
+ * (and instead of the old inline JS snippets the LLM could not execute).
13
+ */
14
+ export async function dagCommand(featureName, options = {}) {
15
+ try {
16
+ const tasksPath = join(process.cwd(), '.morph', 'features', featureName, '2-plan', 'tasks.json');
17
+ if (!existsSync(tasksPath)) {
18
+ console.error(chalk.red(`\n❌ tasks.json not found for feature "${featureName}"`));
19
+ console.error(chalk.gray(`Expected at: .morph/features/${featureName}/2-plan/tasks.json\n`));
20
+ process.exit(1);
21
+ }
22
+
23
+ // parseTasksJson validates the schema and throws with the error list.
24
+ const data = await parseTasksJson(tasksPath);
25
+ const plan = planDispatch(data);
26
+
27
+ if (options.json) {
28
+ console.log(JSON.stringify(plan, null, 2));
29
+ return;
30
+ }
31
+
32
+ console.log(chalk.bold(`\n📊 DAG — ${featureName}`));
33
+ console.log('━'.repeat(60));
34
+
35
+ const { stats } = plan;
36
+ console.log(`\nTasks: ${stats.total} total, ${stats.pending} pendente(s)`);
37
+ const groupLine = Object.keys(stats.groups)
38
+ .map(g => `${g}:${stats.groups[g]} (peso ${stats.weights[g]})`)
39
+ .join(' | ');
40
+ if (groupLine) console.log(chalk.gray(`Grupos pendentes: ${groupLine}`));
41
+
42
+ const modeLabel = {
43
+ 'sequential': 'SEQUENCIAL (auto-advance)',
44
+ 'parallel-flows': 'PARALELO por fluxo de domínio',
45
+ 'parallel-roots': 'PARALELO por raízes independentes',
46
+ }[plan.mode];
47
+ console.log(chalk.cyan(`\nDecisão: ${modeLabel}`));
48
+ console.log(` → ${plan.reason}`);
49
+
50
+ for (const flow of plan.flows) {
51
+ console.log(` ${chalk.green('▶')} Fluxo ${chalk.bold(flow.group)} (peso ${flow.weight}): ${flow.taskIds.join(', ')}`);
52
+ }
53
+ if (plan.batch.length > 0) {
54
+ console.log(` ${chalk.green('▶')} Batch paralelo: ${plan.batch.join(', ')}`);
55
+ }
56
+ if (plan.inline.length > 0) {
57
+ console.log(` ${chalk.yellow('○')} Inline (sem dispatch próprio): ${plan.inline.join(', ')}`);
58
+ }
59
+ if (plan.sequentialRemainder.length > 0) {
60
+ console.log(` ${chalk.gray('→')} Sequencial: ${plan.sequentialRemainder.join(', ')}`);
61
+ }
62
+
63
+ console.log('\n' + '━'.repeat(60) + '\n');
64
+ } catch (error) {
65
+ console.error(chalk.red(`\n❌ Error analyzing DAG: ${error.message}\n`));
66
+ process.exit(1);
67
+ }
68
+ }