@polymorphism-tech/morph-spec 8.14.0 → 8.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 8.14.0
6
+ **Version:** 8.15.1
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
368
368
 
369
369
  ---
370
370
 
371
- *morph-spec v8.14.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.15.1 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.14.0",
3
+ "version": "8.15.1",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v8.14.0 by Polymorphism Tech*
188
+ *morph-spec v8.15.1 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.14.0 by Polymorphism Tech*
252
+ *morph-spec v8.15.1 by Polymorphism Tech*
@@ -125,6 +125,7 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
125
125
  - **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (o setup recusa com exit 3).
126
126
  - O estado autoritativo (`feature.json`) viaja **commitado na branch** `morph/{f}`. O `state.json` local do worktree é um índice derivável — materializado por self-heal, nunca sincronizado à mão.
127
127
  - Infra compartilhada (`node_modules`, `.claude`, `.morph/framework`) é linkada por junction; `memory/` e `logs/` são locais de cada worktree (isolamento correto). Se faltar infra num worktree (aberto com `git worktree add` puro), rode `morph-spec worktree link --all`.
128
+ - **Nunca apague um worktree à mão** (`rm -rf`, `Remove-Item -Recurse -Force`, deletar a pasta pelo explorador/IDE). O `morph-spec finish`/`worktree remove` desmonta a junction ANTES de remover (PROVE-or-DECLINE — nunca segue o link para fora); um `rm -rf`/`Remove-Item -Recurse` genérico não sabe disso e, em muitas implementações (inclusive o `Remove-Item -Recurse` do PowerShell no Windows), atravessa a junction e apaga o conteúdo real da raiz que ela aponta — o `.claude`/`node_modules`/`.morph/framework` compartilhados, não só o link. Se um worktree precisa sumir, use sempre `morph-spec finish <f> --pr|--merge` (ou, sem fechar a feature, `git worktree remove`, nunca uma remoção de diretório genérica).
128
129
  - **Ambiente separado (portas):** o setup aloca um **bloco de portas determinístico** por feature e escreve `.morph/worktree.env` no worktree (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` quando o stack tem dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as portas padrão (3000/5000) pertencem à raiz; duas instâncias no default se matam. O arquivo é local e git-invisível; o hook de SessionStart injeta as portas. Overrides em `config.json → fleet.basePort/blockSize`.
129
130
  - **Banco de dados:** worktrees compartilham o mesmo banco por padrão. Para isolamento real com o MCP do Neon disponível, crie um branch de DB `morph/{f}` e anote a connection string no `.morph/worktree.env` (a CLI nunca toca em credenciais); sem Neon, prefixe schema/DB local com o nome da feature. Migration destrutiva num worktree compartilhando banco → integre antes de abrir worktrees irmãos.
130
131
  - **Limitação conhecida:** `node_modules` é compartilhado (junction) — mudança de dependência feita num worktree afeta os irmãos. Mudou dependência: conclua e integre (`finish`) antes de seguir com worktrees paralelos.
@@ -143,9 +143,9 @@
143
143
  ],
144
144
  "stack": ["*"],
145
145
  "standards": [],
146
- "tools": "Read, Glob, Grep",
146
+ "tools": "Read, Glob, Grep, Bash",
147
147
  "model": "opus",
148
- "persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia apenas o mandate, o diff e a saída real dos testes que o orquestrador fornece, com contexto fresco. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
148
+ "persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
149
149
  },
150
150
  "scout": {
151
151
  "title": "Codebase Scout",
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invokable: true
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # create-umbraco-lp
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # migrate-lp-to-umbraco
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
4
4
  version: core
5
5
  when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
6
6
  user-invokable: false
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
6
6
  user-invokable: true
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
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: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -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: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -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: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -45,17 +45,19 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
45
45
  4. **Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
46
46
  - Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores que você anexa ao prompt do avaliador independente (`morph-evaluator`).
47
47
  - 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).
48
- - 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:
48
+ - Despache o subagent registrado `morph-evaluator` (gerado de `agents.json` — a persona do avaliador é o system prompt dele, com Read/Glob/Grep + Bash somente para verificação, sem Write/Edit), com contexto fresco:
49
49
  ```
50
50
  dispatch Agent (subagent_type: "morph-evaluator") with prompt:
51
51
  contexto: mandate.md + spec.md + diff da implementação + saída real dos testes
52
52
  instrução: |
53
- Você NÃO participou da implementação — avalie apenas o que está no diff,
54
- contra o mandate. Aplique as rubricas em .morph/framework/evals/rubrics/
55
- (architecture 30%, contracts 25%, code-quality 25%, test-coverage 20%)
56
- seguindo as bandas 0-10 de cada uma.
53
+ Você NÃO participou da implementação — avalie o que está no diff,
54
+ contra o mandate. Você tem Bash: use `morph-spec verify`, testes/build
55
+ do stack e `git log/diff/show` para conferir por conta própria o que
56
+ foi reportado, em vez de confiar cegamente. Aplique as rubricas em
57
+ .morph/framework/evals/rubrics/ (architecture 30%, contracts 25%,
58
+ code-quality 25%, test-coverage 20%) seguindo as bandas 0-10 de cada uma.
57
59
  NÃO reescreva código. NÃO toque em tasks.json, feature.json nem no board
58
- de sessão.
60
+ de sessão — Bash é só para verificação, nunca para mudar estado.
59
61
  Contrato do relatório final: GRAVE seu relatório em
60
62
  `.morph/features/{feature}/4-review/evaluator-report.md` ANTES de
61
63
  encerrar — esse arquivo é o valor de retorno autoritativo desta
@@ -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: "8.14.0"
7
+ cliVersion: "8.15.1"
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: "8.14.0"
7
+ cliVersion: "8.15.1"
8
8
  ---
9
9
 
10
10
  # morph-standards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.14.0",
3
+ "version": "8.15.1",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -991,7 +991,34 @@ export async function doctorCommand(options = {}) {
991
991
  check('context7 plugin', pluginStatus.context7, false,
992
992
  'Run: morph-spec install-plugin context7');
993
993
  } else {
994
- checks.push({ name: '.claude/', status: 'warn', msg: 'incomplete structure' });
994
+ // `.claude/` exists but lacks commands/ and/or skills/ — ranges from a
995
+ // partial install to the full-wipe incident this command exists to catch
996
+ // (folder survives, 0 items — doctor used to shrug this off as a vague
997
+ // warning with no fix). Count what IS still there: near-total loss is an
998
+ // ERROR with the actionable command, not a warning that reads as optional.
999
+ const pieces = ['agents', 'rules', 'skills', 'commands'];
1000
+ const present = [];
1001
+ for (const piece of pieces) {
1002
+ if (await pathExists(join(claudePath, piece))) present.push(piece);
1003
+ }
1004
+ if (await pathExists(join(claudePath, 'CLAUDE.md'))) present.push('CLAUDE.md');
1005
+ if (await pathExists(join(claudePath, 'settings.local.json'))) present.push('settings.local.json');
1006
+
1007
+ if (present.length <= 1) {
1008
+ checks.push({
1009
+ name: '.claude/',
1010
+ status: 'error',
1011
+ msg: `severely damaged — only ${present.length ? present.join(', ') : 'nothing'} present (expected agents/, rules/, skills/, commands/, CLAUDE.md, settings.local.json) — run 'morph-spec setup-infra' (NOT --reinstall, which only covers stack-aware assets) to fully restore`,
1012
+ });
1013
+ hasErrors = true;
1014
+ } else {
1015
+ checks.push({
1016
+ name: '.claude/',
1017
+ status: 'warn',
1018
+ msg: `incomplete structure — present: ${present.join(', ')} — run 'morph-spec setup-infra' to restore the rest`,
1019
+ });
1020
+ hasWarnings = true;
1021
+ }
995
1022
  }
996
1023
  } else {
997
1024
  checks.push({ name: '.claude/', status: 'missing' });
@@ -1,8 +1,18 @@
1
1
  import chalk from 'chalk';
2
2
  import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, resolve, sep } from 'path';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { promoteUseCases, SPECS_USECASES_PATHSPEC } from '../../lib/specs/usecase-promoter.js';
6
+ import { isValidFeatureName } from '../project/worktree.js';
7
+
8
+ /** Refuse a move whose resolved target escapes `scopeDir`. */
9
+ function assertContained(scopeDir, name, target) {
10
+ const scopeAbs = resolve(scopeDir) + sep;
11
+ const targetAbs = resolve(target);
12
+ if (targetAbs !== resolve(scopeDir) && !(targetAbs + sep).startsWith(scopeAbs)) {
13
+ throw new Error(`recusado: "${name}" resolve para fora de ${scopeDir}`);
14
+ }
15
+ }
6
16
 
7
17
  /**
8
18
  * `morph-spec archive <feature>` — atomic, git-aware archive of a completed
@@ -57,9 +67,22 @@ function readReviewApproved(fjPath) {
57
67
  */
58
68
  export function runArchive(feature, opts = {}) {
59
69
  const cwd = opts.cwd || process.cwd();
60
- const featuresPath = join(cwd, '.morph', 'features', feature);
70
+
71
+ // Gate 0: valid kebab name — refuses BEFORE any path is built from it. Same
72
+ // gate `finish` already applies upstream (defense-in-depth here); the
73
+ // standalone `archiveCommand` has no other gate at all, so without this an
74
+ // unvalidated `feature` reaches `renameSync` and can MOVE a directory outside
75
+ // .morph/features/ (`../../.claude` resolves to `cwd/.claude`).
76
+ if (!isValidFeatureName(feature)) {
77
+ return { ok: false, error: 'invalid-feature-name' };
78
+ }
79
+
80
+ const featuresDir = join(cwd, '.morph', 'features');
61
81
  const archiveDir = join(cwd, '.morph', 'archive');
82
+ const featuresPath = join(featuresDir, feature);
62
83
  const archivePath = join(archiveDir, feature);
84
+ assertContained(featuresDir, feature, featuresPath);
85
+ assertContained(archiveDir, feature, archivePath);
63
86
  const featPathspec = join('.morph', 'features', feature);
64
87
  const archivePathspec = join('.morph', 'archive', feature);
65
88
 
@@ -197,7 +220,10 @@ export async function archiveCommand(featureName, options = {}) {
197
220
  });
198
221
 
199
222
  if (!result.ok) {
200
- if (result.error === 'feature-folder-not-found') {
223
+ if (result.error === 'invalid-feature-name') {
224
+ console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
225
+ console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
226
+ } else if (result.error === 'feature-folder-not-found') {
201
227
  console.error(chalk.red(`\n❌ Feature folder not found: .morph/features/${featureName}\n`));
202
228
  } else if (result.error === 'review-not-approved') {
203
229
  console.error(chalk.red(`\n❌ Gate 3 (review) is not approved for "${featureName}".`));
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { existsSync, rmSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, resolve, sep } from 'path';
4
4
  import { execSync } from 'child_process';
5
+ import { isValidFeatureName } from '../project/worktree.js';
5
6
 
6
7
  /**
7
8
  * `morph-spec delete <feature>` — remove a feature. Hard by default (folder is
@@ -17,8 +18,27 @@ import { execSync } from 'child_process';
17
18
  *
18
19
  * Refuses when any gate is approved (real work, not a dead shell), unless
19
20
  * `--force`.
21
+ *
22
+ * SECURITY: `featureName` is an unsanitized CLI argument that flows straight
23
+ * into `rmSync(..., { recursive: true })` — the same class of bug the worktree
24
+ * teardown guards against, but here the danger is path TRAVERSAL rather than a
25
+ * followed symlink. `join(cwd, '.morph', 'features', '../../.claude')`
26
+ * resolves to `cwd/.claude`, so an unvalidated name can point the hard-delete
27
+ * at the shared `.claude/` install (or anything else relative to cwd). Gated
28
+ * by `isValidFeatureName` (same kebab-slug rule `create` stamps at birth) PLUS
29
+ * a resolved-path containment assert — validated by path, not by the
30
+ * convention that a real feature name never contains `..`.
20
31
  */
21
32
 
33
+ /** Refuse a delete/archive whose resolved target escapes `.morph/features/{scope}/{name}`. */
34
+ function assertContained(scopeDir, name, target) {
35
+ const scopeAbs = resolve(scopeDir) + sep;
36
+ const targetAbs = resolve(target);
37
+ if (targetAbs !== resolve(scopeDir) && !(targetAbs + sep).startsWith(scopeAbs)) {
38
+ throw new Error(`recusado: "${name}" resolve para fora de ${scopeDir}`);
39
+ }
40
+ }
41
+
22
42
  function isGitRepo(cwd) {
23
43
  try {
24
44
  execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'pipe' });
@@ -53,7 +73,21 @@ function dropFromIndex(cwd, featureName) {
53
73
  export async function deleteCommand(featureName, options = {}) {
54
74
  try {
55
75
  const cwd = options.cwd || process.cwd();
56
- const featuresPath = join(cwd, '.morph', 'features', featureName);
76
+
77
+ // Gate 0: valid kebab name — refuses BEFORE any path is built from it, so a
78
+ // traversal attempt (`../../.claude`) never reaches existsSync/rmSync. Same
79
+ // gate `create` stamps at birth and `finish` re-checks; unlike those two,
80
+ // delete/archive take a FRESH argument every invocation, so this cannot rely
81
+ // on "every real feature folder is already a valid slug".
82
+ if (!isValidFeatureName(featureName)) {
83
+ console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
84
+ console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
85
+ process.exit(1);
86
+ }
87
+
88
+ const featuresDir = join(cwd, '.morph', 'features');
89
+ const featuresPath = join(featuresDir, featureName);
90
+ assertContained(featuresDir, featureName, featuresPath);
57
91
  const fjPath = join(featuresPath, 'feature.json');
58
92
 
59
93
  if (!existsSync(featuresPath)) {