@polymorphism-tech/morph-spec 8.5.0 → 8.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 (55) hide show
  1. package/README.md +4 -4
  2. package/bin/morph-spec.js +23 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +2 -2
  5. package/docs/QUICKSTART.md +2 -2
  6. package/framework/CLAUDE.md +12 -7
  7. package/framework/MORPH.md +49 -6
  8. package/framework/evals/rubrics/test-coverage.md +4 -1
  9. package/framework/hooks/claude-code/core/telemetry-log.js +15 -0
  10. package/framework/hooks/claude-code/core/worktree-context.js +41 -5
  11. package/framework/hooks/shared/claim-registry.js +175 -0
  12. package/framework/hooks/shared/phase-utils.js +1 -0
  13. package/framework/hooks/shared/state-reader.js +68 -25
  14. package/framework/rules/morph-task-tracking.md +14 -8
  15. package/framework/rules/morph-workflow.md +36 -3
  16. package/framework/schemas/feature.schema.json +2 -1
  17. package/framework/schemas/tasks.schema.json +6 -1
  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 +1 -1
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +4 -3
  22. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  23. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  24. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  25. package/framework/skills/core/morph-plan/SKILL.md +83 -13
  26. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  27. package/framework/skills/core/morph-review/SKILL.md +7 -1
  28. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  29. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  30. package/package.json +1 -1
  31. package/src/commands/project/claim.js +91 -0
  32. package/src/commands/project/doctor.js +65 -0
  33. package/src/commands/project/fleet.js +37 -2
  34. package/src/commands/project/worktree.js +70 -7
  35. package/src/commands/state/advance.js +60 -1
  36. package/src/commands/state/approve.js +125 -2
  37. package/src/commands/state/archive.js +44 -5
  38. package/src/commands/state/create.js +3 -3
  39. package/src/commands/state/finish.js +103 -7
  40. package/src/commands/state/list.js +9 -1
  41. package/src/commands/state/report-check.js +202 -0
  42. package/src/commands/tasks/dag.js +25 -2
  43. package/src/commands/validation/validate-feature.js +9 -2
  44. package/src/core/paths/output-schema.js +66 -6
  45. package/src/core/state/lifecycle-audit.js +57 -0
  46. package/src/dashboard/scanner/state-scanner.js +11 -3
  47. package/src/lib/agents/output-collector.js +5 -0
  48. package/src/lib/generators/architecture-scanner.js +1 -1
  49. package/src/lib/generators/handoff-generator.js +4 -2
  50. package/src/lib/specs/usecase-check.js +173 -0
  51. package/src/lib/specs/usecase-promoter.js +120 -0
  52. package/src/lib/tasks/tasks-json-parser.js +16 -0
  53. package/src/lib/worktree/fleet-scanner.js +224 -15
  54. package/src/lib/worktree/worktree-audit.js +181 -0
  55. package/src/utils/file-copier.js +5 -1
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.5.0
6
+ **Version:** 8.7.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -125,7 +125,7 @@ morph-spec approval-status {feature}
125
125
 
126
126
  Not every change deserves the full pipeline. Every feature is born with a **work type** — `feature`, `bug`, `chore`, or `hotfix` — set via `morph-spec create {feature} --type <type>` or classified from free text with `--request "<text>"`. The type dimensions the pipeline: `feature` runs the full proposal → [uiux] → plan → implement → review sequence; `bug` runs a lean proposal/plan; `chore` skips straight to implement (Gates 1/2 stamped by policy at birth); `hotfix` runs scout (read-only root-cause) → surgical fix → test loop, and its Gate 3 always pauses for a human even under `trust: auto`. Change the type mid-flight with `morph-spec retype {feature} {newType}` (undoes only policy-stamped gates, preserves human approvals, blocked after Gate 3).
127
127
 
128
- Independent features can also be developed **in parallel, each isolated in its own git worktree** — one feature, one worktree, one Claude Code instance. `morph-spec create {feature} --worktree` (or `morph-spec worktree setup {feature}`) creates `.worktrees/{feature}/` on branch `morph/{feature}`. The authoritative `feature.json` travels committed on the branch; shared infra (`node_modules`, `.claude`, `.morph/framework`) is linked via junctions (`morph-spec worktree link --all` repairs it if missing). Close out with `morph-spec worktree finish {feature}` (merges `--no-ff` back into the clean primary root) followed by `morph-spec archive {feature}` at the root; `morph-spec worktree list` shows every active worktree with its phase and gate status.
128
+ Independent features can also be developed **in parallel, each isolated in its own git worktree** — one feature, one worktree, one Claude Code instance. `morph-spec create {feature} --worktree` (or `morph-spec worktree setup {feature}`) creates `worktrees/{feature}/` on branch `morph/{feature}`. The authoritative `feature.json` travels committed on the branch; shared infra (`node_modules`, `.claude`, `.morph/framework`) is linked via junctions (`morph-spec worktree link --all` repairs it if missing). Close out with `morph-spec worktree finish {feature}` (merges `--no-ff` back into the clean primary root) followed by `morph-spec archive {feature}` at the root; `morph-spec worktree list` shows every active worktree with its phase and gate status.
129
129
 
130
130
  ---
131
131
 
@@ -197,7 +197,7 @@ morph-spec dag {feature} [--mermaid] # DAG dispatch d
197
197
 
198
198
  ```bash
199
199
  morph-spec create {feature} --description "<what/why>" --worktree # Birth the feature already isolated in its own worktree
200
- morph-spec worktree setup {feature} # Create .worktrees/{feature}/ on branch morph/{feature}
200
+ morph-spec worktree setup {feature} # Create worktrees/{feature}/ on branch morph/{feature}
201
201
  morph-spec worktree link [--all] # Relink shared infra (node_modules, .claude, .morph/framework)
202
202
  morph-spec worktree list [--json] # List active feature worktrees with phase/gate status
203
203
  # Close a worktree feature with `morph-spec finish {feature} --pr|--merge` (see Feature workflow) — it absorbed `worktree finish`.
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
368
368
 
369
369
  ---
370
370
 
371
- *morph-spec v8.5.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.7.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -25,6 +25,8 @@ import { doctorCommand } from '../src/commands/project/doctor.js';
25
25
  import { statusCommand } from '../src/commands/project/status.js';
26
26
  import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand } from '../src/commands/project/worktree.js';
27
27
  import { fleetCommand } from '../src/commands/project/fleet.js';
28
+ import { claimCommand } from '../src/commands/project/claim.js';
29
+ import { reportCheckCommand } from '../src/commands/state/report-check.js';
28
30
 
29
31
  // === Approval gates (human-only) ===
30
32
  import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
@@ -118,6 +120,9 @@ program
118
120
  .description('Check MORPH installation health')
119
121
  .option('--full', 'Run full health check')
120
122
  .option('--mcp', 'Check MCP server configuration')
123
+ .option('--worktrees', "Reconcile git's worktree registry × the directories on disk × known features")
124
+ .option('--prune', 'With --worktrees: drop git records for worktree directories that are already gone')
125
+ .option('--json', 'With --worktrees: output as JSON')
121
126
  .option('--reset', 'Remove morph-managed entries from .claude/settings.local.json')
122
127
  .action(doctorCommand);
123
128
 
@@ -145,6 +150,7 @@ program
145
150
  .option('--mode <mode>', 'Approval mode: "user" or "auto" (default: detected from TTY)')
146
151
  .option('--notes <notes>', 'Approval notes')
147
152
  .option('--force', 'Approve out of order (bypass the gate-monotonicity check)')
153
+ .option('--self-assessed', 'Gate 3 only: approve without 4-review/evaluator-report.md, recording the degradation in feature.json')
148
154
  .action((feature, gate, options) => approveCommand(feature, gate, options));
149
155
 
150
156
  program
@@ -341,7 +347,7 @@ const worktreeCommand = program
341
347
 
342
348
  worktreeCommand
343
349
  .command('setup <feature>')
344
- .description('Create a worktree at .worktrees/{feature}/ on branch morph/{feature}')
350
+ .description('Create a worktree at worktrees/{feature}/ on branch morph/{feature}')
345
351
  .option('--fresh', 'Create a new dated branch even if one already exists')
346
352
  .action((feature, options) => worktreeSetupCommand(feature, options));
347
353
 
@@ -365,6 +371,22 @@ program
365
371
  .option('--json', 'Output as JSON')
366
372
  .action((options) => fleetCommand(options));
367
373
 
374
+ // ─── Claims (advisory feature ownership) ──────────────────────────────────
375
+ program
376
+ .command('claim [feature]')
377
+ .description('Show which session holds each feature (claims are advisory warnings, never locks). --release frees an orphan claim left by a dead session')
378
+ .option('--release', 'Release the claim on <feature> (recovery: the owning session died)')
379
+ .option('--list', 'List every claim on record (the default)')
380
+ .option('--json', 'Output as JSON')
381
+ .action((feature, options) => claimCommand(feature, options));
382
+
383
+ // ─── Sub-agent report protocol ────────────────────────────────────────────
384
+ program
385
+ .command('report-check <feature> [task]')
386
+ .description('Audit sub-agent report files against the work that actually landed. Answers "did the WORK land even though the REPORT did not" — exits 0 only when everything is intact')
387
+ .option('--json', 'Output as JSON')
388
+ .action((feature, task, options) => reportCheckCommand(feature, task, options));
389
+
368
390
  // ─── Dashboard ────────────────────────────────────────────────────────────
369
391
  program
370
392
  .command('dashboard')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.5.0",
3
+ "version": "8.7.0",
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",
@@ -117,7 +117,7 @@ One feature = one git worktree = one Claude Code instance. Each worktree gets it
117
117
 
118
118
  ```bash
119
119
  morph-spec create {feature} --description "<what/why>" --worktree # birth the feature already isolated in its own worktree
120
- morph-spec worktree setup {feature} # create .worktrees/{feature}/ on branch morph/{feature}
120
+ morph-spec worktree setup {feature} # create worktrees/{feature}/ on branch morph/{feature}
121
121
  morph-spec worktree link [--all] # relink shared infra (node_modules[, .claude, .morph/framework])
122
122
  morph-spec worktree list [--json] # active feature worktrees with phase/gate status
123
123
  # Close a worktree feature with `morph-spec finish {feature} --pr|--merge` (see above) — it absorbed `worktree finish`.
@@ -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.5.0 by Polymorphism Tech*
188
+ *morph-spec v8.7.0 by Polymorphism Tech*
@@ -165,7 +165,7 @@ Independent features can be developed in parallel, each isolated in its own git
165
165
  ```bash
166
166
  morph-spec create my-feature --description "what & why" --worktree # birth the feature already isolated
167
167
  # or, for an existing feature:
168
- morph-spec worktree setup my-feature # creates .worktrees/my-feature/ on branch morph/my-feature
168
+ morph-spec worktree setup my-feature # creates worktrees/my-feature/ on branch morph/my-feature
169
169
  ```
170
170
 
171
171
  Open the new worktree directory in its own Claude Code instance and work normally — a SessionStart hook injects the worktree's feature identity into context automatically. When the feature is done (Gate 3 approved), close it in one step:
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.5.0 by Polymorphism Tech*
252
+ *morph-spec v8.7.0 by Polymorphism Tech*
@@ -15,7 +15,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
15
15
 
16
16
  **NUNCA:**
17
17
  - Pular para código sem proposal aprovado (Gate 1) e plano aprovado (Gate 2).
18
- - Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) estão congelados.
18
+ - Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) estão congelados. **Os use cases (`2-plan/usecases/UC-*.md`) NÃO estão** — são editáveis por design, é assim que se muda o comportamento e se re-roda `implement`; a auditoria fica no git.
19
+ - Escrever critérios de aceite no `proposal.md`. O comportamento verificável mora nos use cases, cuja pós-condição precisa ser **observável em teste** e chega **literal** ao `doneCriteria` da task.
20
+ - Editar `.morph/specs/usecases/` à mão. O acervo só se escreve pela promoção do `finish`, que lê de `2-plan/usecases/` — é ela que carimba `status: implemented` e sobrescreve por ID. Para mudar um UC existente (o caso `bug`), copie-o para o `2-plan/usecases/` da feature e corrija lá.
19
21
  - Tocar os campos de contrato do plano em `tasks.json` após o Gate 2 (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) — o hook `protect-spec-files` faz diff semântico e bloqueia mudanças neles, seja via `Write` ou `Edit`. (Os campos mutáveis `status`/`outputs`/`notes` continuam graváveis por `Write` ou `Edit` — ver SEMPRE.)
20
22
  - Listar perguntas em texto simples — sempre use a tool `AskUserQuestion` (1-4 perguntas por chamada).
21
23
  - Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), ou SQL cru sem justificativa.
@@ -41,11 +43,11 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
41
43
  | `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
42
44
 
43
45
  **Comandos CLI de gate e de estado** (a LLM os usa em vez de editar o estado à mão):
44
- - `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`.
46
+ - `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate).
45
47
  - `morph-spec advance <feature>` — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
46
48
  - `morph-spec archive <feature>` — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
47
- - `morph-spec finish <feature> --pr|--merge` — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}`, depois integra. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
48
- - `morph-spec dag <feature>` — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio).
49
+ - `morph-spec finish <feature> --pr|--merge` — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
50
+ - `morph-spec dag <feature>` — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `--mermaid` nunca é bloqueado.
49
51
  - `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
50
52
  - `morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
51
53
  - `morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]` — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
@@ -53,9 +55,12 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
53
55
  - `morph-spec delete <feature> [--archive] [--force]` — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
54
56
  - `morph-spec retype <feature> <newType>` — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
55
57
  - `morph-spec verify <feature> [task] [--json]` — cadeia determinística build+testes+validadores (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira.
56
- - `morph-spec worktree setup|link|list` — gerencia o ciclo do worktree por feature (`setup` cria `.worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). O fechamento saiu daqui — use `morph-spec finish` (acima).
58
+ - `morph-spec doctor --worktrees [--prune] [--json]` — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda o único reparo seguro (`git worktree prune` — registros de pastas que já sumiram); apagar pasta que ainda existe é reportado com o comando exato, nunca executado.
59
+ - `morph-spec worktree setup|link|list` — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). O fechamento saiu daqui — use `morph-spec finish` (acima).
57
60
  - `morph-spec handoff <feature>` — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`.
58
- - `morph-spec fleet` — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, portas, handoff e atividade. Use entre sessões para decidir onde atuar.
61
+ - `morph-spec fleet` — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
62
+ - `morph-spec report-check <feature> [task] [--json]` — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez). Protocolo completo em MORPH.md §6.
63
+ - `morph-spec claim [feature] [--release] [--json]` — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`.
59
64
  - `morph-spec telemetry [--json] [--feature <f>] [--limit <n>]` — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log` em todo tool call e todo UserPromptSubmit.
60
65
 
61
66
  ## Skills core (em `.claude/skills/`)
@@ -64,7 +69,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
64
69
  |-------|-------------|
65
70
  | `morph-brainstorming` | Entender negócio, escanear stack, produzir proposal.md |
66
71
  | `morph-stack-scan` | Detectar desvios de stack em projeto brownfield |
67
- | `morph-plan` | Produzir spec.md + mandate.md + tasks.json |
72
+ | `morph-plan` | Destilar os use cases (comportamento) e então produzir spec.md + mandate.md + tasks.json |
68
73
  | `morph-implement` | Executar uma tarefa com auto-advance entre tasks |
69
74
  | `morph-eval` | Aplicar rubricas e produzir score 0-10 |
70
75
  | `morph-review` | Produzir review-report.md (Gate 3) |
@@ -73,7 +73,7 @@ Anti-padrão: o Specialist não cria novas tarefas. Descobertas de escopo durant
73
73
 
74
74
  **Evaluator** — ativo após uma tarefa ou fase ser concluída.
75
75
  Responsabilidades: aplicar as rubricas em `.morph/framework/evals/rubrics/`, produzir score 0-10, emitir feedback priorizado quando score < 9 (que alimenta o loop de correção autônomo do morph-apply), escalar para revisão humana apenas quando o loop de correção se esgotar (3 tentativas sem atingir 9).
76
- No **Gate 3** o Evaluator é obrigatoriamente um sub-agent independente: a `morph-review` despacha o subagent registrado `morph-evaluator` (gerado de `agents.json`, tools read-only) com contexto fresco — mandate + diff da implementação + saída real dos testes — e o score composto da feature vem desse retorno, nunca de auto-avaliação de quem implementou.
76
+ No **Gate 3** o Evaluator é obrigatoriamente um sub-agent independente: a `morph-review` despacha o subagent registrado `morph-evaluator` (gerado de `agents.json`, tools read-only) com contexto fresco — mandate + diff da implementação + saída real dos testes — e o score composto da feature vem desse retorno, nunca de auto-avaliação de quem implementou. **A regra é enforced server-side:** `morph-spec approve {feature} review` recusa (exit 1) enquanto `4-review/evaluator-report.md` não existir. Se o dispatch for genuinamente impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` não dispensa a regra (é só para ordem de gate).
77
77
  Anti-padrão: o Evaluator não reescreve o código que avaliou. Se o score está abaixo de 6, o relatório vai para o humano — não para uma segunda rodada de tentativa automática.
78
78
 
79
79
  Os papéis não são exclusivos — numa sessão longa você transita entre Orchestrator e Specialist conforme as tarefas avançam. A transição deve ser explícita: antes de mudar de papel, conclua o que estava fazendo ou registre o estado corrente no recap.md.
@@ -112,11 +112,11 @@ Nem todo trabalho merece o pipeline completo. Cada feature nasce com **identidad
112
112
 
113
113
  **Compat:** uma feature sem `workType` (criada antes do v8) é tratada como `feature`. Mudança de tipo mid-flight: `morph-spec retype <f> <tipo>` (desfaz só carimbos de política, preserva aprovações humanas; bloqueado após o Gate 3).
114
114
 
115
- **Nascimento numa branch:** toda feature — **com ou sem `--worktree`** — nasce numa branch `morph/{f}` ramificada da default. O fluxo comum (sem worktree) desenvolve direto em `morph/{f}` na raiz; o fluxo worktree isola em `.worktrees/{f}`. Por construção o fechamento é simétrico: `morph-spec finish <f> --pr|--merge` (§2.2) fecha os dois do mesmo jeito. Features criadas **antes** deste modelo (sem branch `morph/{f}`) fecham por `morph-spec archive <f>` (arquivar-sem-integrar), não por `finish`.
115
+ **Nascimento numa branch:** toda feature — **com ou sem `--worktree`** — nasce numa branch `morph/{f}` ramificada da default. O fluxo comum (sem worktree) desenvolve direto em `morph/{f}` na raiz; o fluxo worktree isola em `worktrees/{f}`. Por construção o fechamento é simétrico: `morph-spec finish <f> --pr|--merge` (§2.2) fecha os dois do mesmo jeito. Features criadas **antes** deste modelo (sem branch `morph/{f}`) fecham por `morph-spec archive <f>` (arquivar-sem-integrar), não por `finish`.
116
116
 
117
117
  ### 2.2 Desenvolvimento paralelo por worktree (frota)
118
118
 
119
- Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, com uma instância de Claude Code por worktree. `morph-spec create <f> --description "<o quê/porquê>" --worktree` (ou `morph-spec worktree setup <f>`) cria `.worktrees/{f}/` na branch `morph/{f}`. Contrato:
119
+ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu git worktree**, com uma instância de Claude Code por worktree. `morph-spec create <f> --description "<o quê/porquê>" --worktree` (ou `morph-spec worktree setup <f>`) cria `worktrees/{f}/` na branch `morph/{f}`. Contrato:
120
120
 
121
121
  - **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (o setup recusa com exit 3).
122
122
  - 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.
@@ -138,15 +138,21 @@ Não invente caminhos alternativos.
138
138
 
139
139
  ### Proposal (`0-proposal/`)
140
140
 
141
- - `proposal.md` — descrição da feature, problema que resolve, escopo, critérios de aceite, riscos. Um bom proposal.md tem no máximo 2 páginas: se está crescendo além disso, o escopo provavelmente precisa ser cortado antes de avançar.
141
+ - `proposal.md` — descrição da feature, problema que resolve, escopo, riscos. **Sem critérios de aceite:** o comportamento verificável mora nos use cases (`2-plan/usecases/`), destilados na fase plan. Duplicar aqui produz duas fontes que divergem no primeiro ajuste. Um bom proposal.md tem no máximo 2 páginas: se está crescendo além disso, o escopo provavelmente precisa ser cortado antes de avançar.
142
142
 
143
143
  ### Plan (`2-plan/`)
144
144
 
145
- - `spec.md` — especificação técnica detalhada: arquitetura, componentes, integrações, fluxos de dados, edge cases identificados. Um bom spec.md permite que qualquer desenvolvedor comece a implementar sem fazer perguntas básicas. Um spec.md fraco lista tecnologias mas não explica como os componentes se conectam.
145
+ - `usecases/UC-{nn}-{slug}.md` — **um arquivo por use case**, o artefato de comportamento da feature. Frontmatter (`id`, `title`, `actor`, `status`, `entities`) mais as seções `Pré-condições`, `Cenário principal`, `Fluxos alternativos` (A1, A2… — cada alternativa aproxima uma user story), `Pós-condições` (P1, P2…) e `Delta de domínio` (**só o que muda** no modelo; o domínio completo continua sendo o código EF Core mais as migrations).
146
+
147
+ **Pós-condição precisa ser observável em teste** — esta é a regra que sustenta a camada. "O sistema fica consistente" não passa; "a resposta contém todos os veterinários não arquivados" passa. A pós-condição chega **literal** ao `doneCriteria` da task que a realiza: é a tradução manual nesse ponto que hoje perde o requisito.
148
+
149
+ O `id` é sequencial **no projeto**, não na feature — atribuído lendo o acervo `.morph/specs/usecases/` mais os use cases das features ativas, tomando o maior mais um. Ciclo de status: `draft` (nasce) → `approved` (Gate 2) → `implemented` (promoção no `finish`); é descritivo, quem trava fase é o gate. O use case fica **editável depois do Gate 2**, por design — é o que permite mudar o comportamento e re-rodar `implement`, com a auditoria no git. Obrigatoriedade por tipo de trabalho: ver `morph-plan`.
150
+
151
+ - `spec.md` — especificação técnica detalhada: arquitetura, componentes, integrações, fluxos de dados, edge cases **técnicos**. Responde **como** os use cases serão realizados — não repete o comportamento. Um bom spec.md permite que qualquer desenvolvedor comece a implementar sem fazer perguntas básicas. Um spec.md fraco lista tecnologias mas não explica como os componentes se conectam.
146
152
 
147
153
  - `mandate.md` — contrato de implementação: stack exata, standards obrigatórios (com links), patterns explicitamente proibidos, checklist de done com critérios verificáveis. O mandate.md é lido no início de cada tarefa de implementação — deve ser específico o suficiente para resolver ambiguidades sem consultar o Orchestrator.
148
154
 
149
- - `tasks.json` — task list estruturada validada contra `framework/schemas/tasks.schema.json`. Cada task tem `id` (T1, T2...), `title`, `description`, `dependencies` (array de IDs), `effort` (S/M/L/XL), `doneCriteria`, `status` (pending/in_progress/done/skipped/blocked) e `outputs` (paths esperados). Cada entrada no recap.md deve referenciar um ID do tasks.json.
155
+ - `tasks.json` — task list estruturada validada contra `framework/schemas/tasks.schema.json`. Cada task tem `id` (T1, T2...), `title`, `description`, `dependencies` (array de IDs), `effort` (S/M/L/XL), `doneCriteria`, `status` (pending/in_progress/done/skipped/blocked), `outputs` (paths esperados) e `usecase` (opcional — o **ID** do use case que a task realiza, `UC-07`, nunca o nome do arquivo). O `doneCriteria` de uma task com `usecase` carrega a **pós-condição literal**, com o rótulo (`"P2 — cada veterinário traz..."`). Cada entrada no recap.md deve referenciar um ID do tasks.json.
150
156
 
151
157
  - `decisions.md` — log de decisões técnicas: qual era a alternativa, por que foi descartada, quem decidiu. Criado quando surgir a primeira decisão não trivial. Não é obrigatório — só existe se houver decisões que precisam de rastreabilidade.
152
158
 
@@ -158,6 +164,18 @@ Não invente caminhos alternativos.
158
164
 
159
165
  - `review-report.md` — relatório de revisão: scores por dimensão (arquitetura, contratos, qualidade, cobertura), feedback priorizado (bloqueantes primeiro, melhorias depois), aprovação ou rejeição com critérios explícitos. Um review-report.md que só diz "aprovado" sem scores não satisfaz o gate.
160
166
 
167
+ ### Acervo de use cases (`.morph/specs/usecases/`)
168
+
169
+ O acervo permanente do projeto: `UC-{nn}-{slug}.md`, **commitado** (nenhuma regra do `.gitignore` o cobre). É a descrição viva do comportamento do sistema e sobrevive ao arquivamento das features — diferente dos traces, ele **faz parte da entrega**.
170
+
171
+ - **Quem escreve:** `morph-spec finish` promove os use cases da feature para cá **dentro do commit de arquivamento**, na branch `morph/{f}`, com `status: implemented`. O acervo viaja na própria integração, como o estado arquivado (§2.2) — não é um passo pós-merge.
172
+ - **O que é cobrado mecanicamente:** `morph-spec dag {feature}` cruza `tasks.json` com os use cases — referência a um `UC-{nn}` inexistente é **erro** (não despacha), pós-condição órfã e `doneCriteria` parafraseado são avisos. Isso cobre o *encanamento*. Se a pós-condição é **observável em teste** nenhum checker decide: continua sendo critério do Gate 2 e da rubrica `test-coverage`.
173
+ - **Via única de escrita:** a promoção lê **só** de `{feature}/2-plan/usecases/`. Nunca edite `.morph/specs/usecases/` à mão — a edição direta pula o `status: implemented`, a sobrescrita por ID e o commit escopado, e o acervo passa a ter duas verdades. Para mudar um UC existente (o caso `bug`), copie-o para o `2-plan/usecases/` da feature, corrija lá, e deixe o `finish` sobrescrever.
174
+ - **Quando chega na default:** em `--merge`, no merge `--no-ff`; em `--pr`, só quando o PR for mergeado. Correto por construção: o acervo descreve o que foi **integrado**, não o que foi proposto.
175
+ - **Sobrescrita por ID:** um `UC-07` que já existe é substituído pela versão que está sendo integrada, mesmo que o slug tenha mudado. Exatamente um arquivo por ID.
176
+ - **Fail-open:** feature sem `2-plan/usecases/` promove nada, em silêncio. A promoção nunca pode impedir o fechamento de uma feature.
177
+ - **Colisão entre features paralelas:** dois worktrees podem alocar `UC-08` sem se enxergar, porque cada `feature.json` viaja na sua branch. A colisão é **aceita e resolvida no merge**, como conflito de arquivo — coerente com o ownership de §2.2 ("conflito ali é o sinal honesto de escopo sobreposto"). Renumere o UC perdedor e atualize as referências `usecase` no `tasks.json` arquivado daquela feature. Não há alocador central, e não deve haver: um lock global de id reintroduziria entre worktrees a coordenação que a arquitetura evita de propósito.
178
+
161
179
  ### Traces (`.morph/traces/`)
162
180
 
163
181
  Cada fase de implementação produz um trace JSON em `.morph/traces/YYYY-MM-DD-{feature}-{phase}.json`.
@@ -289,6 +307,31 @@ O orquestrador **lê o arquivo** ao retomar controle após o dispatch — nunca
289
307
  mensagem de retorno da tool. Se a mensagem de texto final chegou inteira, ótimo (economiza uma
290
308
  leitura); se não chegou, o arquivo já está lá.
291
309
 
310
+ ### Protocolo de ausência de relatório
311
+
312
+ **"Relatório ausente = o agente falhou" é a leitura errada.** O sub-agent pode ter feito todo o
313
+ trabalho e perdido o meio de gravá-lo — foi o que aconteceu quando um worktree sumiu debaixo de um
314
+ avaliador em execução. Tratar isso como falha joga fora trabalho real.
315
+
316
+ A pergunta que decide não é "o relatório chegou?", é **"o TRABALHO entrou?"** — e ela se responde
317
+ por evidência, não pela palavra do agente:
318
+
319
+ ```
320
+ morph-spec report-check <feature> [task] --json
321
+ ```
322
+
323
+ | veredito | o que significa | ação |
324
+ |---|---|---|
325
+ | `ok` | relatório presente e com os 4 itens | siga o fluxo |
326
+ | `report-incomplete` | relatório existe, faltam itens (ele lista quais) | complete a partir da mensagem de retorno, carimbe **DEGRADADO** |
327
+ | `work-landed-report-missing` | sem relatório, mas os `outputs` da task estão no disco | **não re-despache** — grave você mesmo o relatório canônico a partir da mensagem de retorno, carimbado **DEGRADADO** |
328
+ | `no-work-no-report` | sem relatório e sem output | re-despache **uma** vez com o mesmo prompt |
329
+ | `report-missing-evidence-unknown` | sem relatório e a task não declarou `outputs` | re-despache (sem evidência, o conservador é o certo) |
330
+
331
+ Sai 0 só em `ok`. Em qualquer caminho degradado a degradação é **registrada** — no recap e nas
332
+ `notes` da task —, nunca aceita em silêncio. Segunda ausência sem trabalho entregue é falha real:
333
+ escale ao humano.
334
+
292
335
  **O que receber de volta (no arquivo, e na mensagem final quando ela chega):**
293
336
  - O que foi feito (2-3 frases)
294
337
  - Arquivos modificados (lista)
@@ -4,7 +4,9 @@ Avalia se os testes cobrem os comportamentos esperados, são isolados e têm nom
4
4
 
5
5
  ## Score 9-10 — Exemplar
6
6
 
7
- - Happy path coberto para todos os casos de uso relevantes.
7
+ - **Cada pós-condição do use case referenciado tem teste correspondente.** Se a task traz `usecase: "UC-07"`, abra `2-plan/usecases/UC-07-*.md` e percorra as pós-condições (P1, P2…): cada uma precisa de ao menos um teste que a verifique. Pós-condição sem teste **impede a faixa 9-10**, por mais completa que a suíte pareça no resto.
8
+ - Happy path coberto para todos os casos de uso relevantes — "relevantes" são os use cases referenciados pelas tasks da feature, não uma lista implícita.
9
+ - Fluxos alternativos do use case (A1, A2…) cobertos: são os edge cases de **comportamento** e foram declarados justamente para serem testados.
8
10
  - Edge cases críticos cobertos: null/undefined, string vazia, boundary values, coleção vazia.
9
11
  - Casos de erro cobertos: o que acontece quando a operação falha, quando a dependência retorna erro.
10
12
  - Testes completamente isolados — cada teste pode rodar sozinho em qualquer ordem.
@@ -43,6 +45,7 @@ Avalia se os testes cobrem os comportamentos esperados, são isolados e têm nom
43
45
 
44
46
  ## Perguntas de avaliação
45
47
 
48
+ 0. A task referencia um `usecase`? Se sim: **toda** pós-condição daquele use case tem teste correspondente? (Sem isso, o teto é 8.)
46
49
  1. O happy path está coberto para todos os casos de uso da tarefa?
47
50
  2. Os edge cases críticos (null, vazio, boundary) estão cobertos?
48
51
  3. O que acontece quando uma dependência falha — isso está testado?
@@ -59,6 +59,21 @@ try {
59
59
  // enrichment is best-effort — a missing feature never blocks capture
60
60
  }
61
61
 
62
+ // Claim heartbeat (B1): this hook already fires on EVERY tool call, so it is
63
+ // the natural proof-of-life for the advisory feature claim — no extra hook,
64
+ // no discipline asked of the operator. The registry throttles the actual
65
+ // write (HEARTBEAT_THROTTLE_MS), so this is a small read on most calls.
66
+ // Silent and fail-open, like everything else here.
67
+ if (feature && payload.session_id) {
68
+ try {
69
+ const { execSync } = await import('child_process');
70
+ const { resolve } = await import('path');
71
+ const { heartbeatClaim } = await import('../../shared/claim-registry.js');
72
+ const common = execSync('git rev-parse --git-common-dir', { cwd, stdio: 'pipe' }).toString().trim();
73
+ if (common) heartbeatClaim(feature, { gitCommonDir: resolve(cwd, common), sessionId: payload.session_id });
74
+ } catch { /* fail-open — a missed heartbeat only shortens a claim's life */ }
75
+ }
76
+
62
77
  let event;
63
78
 
64
79
  // UserPromptSubmit carries the user's prompt and no tool_name.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Event: SessionStart
6
6
  *
7
- * v8 runs each feature in its own git worktree (`.worktrees/{feature}/` on
7
+ * v8 runs each feature in its own git worktree (`worktrees/{feature}/` on
8
8
  * branch `morph/{feature}`), each with its own Claude Code instance. When a
9
9
  * session starts inside one of those worktrees, Claude has no built-in way to
10
10
  * know which feature it belongs to — this hook injects that identity so the
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { readFileSync, existsSync, readdirSync } from 'fs';
32
- import { join } from 'path';
32
+ import { join, resolve } from 'path';
33
33
  import { execSync } from 'child_process';
34
34
  import {
35
35
  stateExists,
@@ -39,6 +39,7 @@ import {
39
39
  getTaskStats,
40
40
  } from '../../shared/state-reader.js';
41
41
  import { injectContext, pass } from '../../shared/hook-response.js';
42
+ import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
42
43
 
43
44
  function readStdin() {
44
45
  try {
@@ -168,10 +169,44 @@ function buildActiveFeatureSummary(dir) {
168
169
  return text;
169
170
  }
170
171
 
172
+ /**
173
+ * Advisory feature ownership (B1): claim the feature this session is about to
174
+ * work on, and — if another session already holds a LIVE claim — say so loudly
175
+ * before any work starts.
176
+ *
177
+ * Never blocks: the claim is a warning, not a lock (see claim-registry.js).
178
+ * The claim is taken regardless, so the registry reflects reality; the operator
179
+ * decides what to do with the collision.
180
+ *
181
+ * Fail-open per FIELD: no git common dir, or any registry error, just means no
182
+ * claim line — never a failed session start.
183
+ *
184
+ * @returns {string} a block to append to the injected context ('' when quiet)
185
+ */
186
+ function claimBlock(dir, feature, sessionId) {
187
+ try {
188
+ const gitCommonDir = resolve(dir, gitRevParse('--git-common-dir', dir));
189
+ const other = foreignClaim(feature, { gitCommonDir, sessionId });
190
+ acquireClaim(feature, { gitCommonDir, sessionId, pid: process.pid, path: dir });
191
+ if (!other) return '';
192
+ return `\n⚠️ ATENÇÃO — a feature ${feature} tem claim VIVO de OUTRA sessão`
193
+ + `\n pid ${other.pid}${other.path ? ` · ${other.path}` : ''} · visto há ${claimAge(other)}`
194
+ + '\n Duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent'
195
+ + '\n (o worktree sumiu debaixo dele). Confirme com a outra sessão ANTES de agir —'
196
+ + '\n principalmente antes de `finish`, `archive` ou `delete`.'
197
+ + `\n Se aquela sessão morreu: morph-spec claim ${feature} --release`;
198
+ } catch {
199
+ return '';
200
+ }
201
+ }
202
+
171
203
  async function main() {
172
204
  try {
173
205
  const dir = projectDir();
174
- readStdin(); // consume the SessionStart payload (source/session_id) — unused here
206
+ // The SessionStart payload carries session_id — the identity the claim
207
+ // registry uses to tell "me" from "another session".
208
+ const payload = readStdin();
209
+ const sessionId = payload?.session_id || `pid-${process.pid}`;
175
210
 
176
211
  if (!stateExists(dir)) {
177
212
  pass();
@@ -184,7 +219,7 @@ async function main() {
184
219
  pass();
185
220
  return;
186
221
  }
187
- injectContext(buildWorktreeContext(dir, feature), 'SessionStart');
222
+ injectContext(buildWorktreeContext(dir, feature) + claimBlock(dir, feature, sessionId), 'SessionStart');
188
223
  return;
189
224
  }
190
225
 
@@ -194,7 +229,8 @@ async function main() {
194
229
  pass();
195
230
  return;
196
231
  }
197
- injectContext(summary, 'SessionStart');
232
+ const active = getActiveFeature(dir);
233
+ injectContext(summary + (active?.name ? claimBlock(dir, active.name, sessionId) : ''), 'SessionStart');
198
234
  } catch {
199
235
  pass();
200
236
  }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * claim-registry.js — advisory feature ownership across parallel sessions.
3
+ *
4
+ * Two Claude Code sessions worked the SAME feature at the same time without
5
+ * seeing each other: one finished it and removed the worktree out from under
6
+ * the other's running sub-agent. A complete recap.md was discarded, a sub-agent
7
+ * wasted, and a decision taken on state that was already obsolete. There was no
8
+ * lock, no claim, no heartbeat — not even a warning.
9
+ *
10
+ * DELIBERATELY ADVISORY. This is not a lock: nothing here ever blocks a
11
+ * command. The framework is fail-open by principle, and a strong lock left
12
+ * behind by a crashed session would strand a feature for no gain. Being
13
+ * VISIBLE is the whole fix.
14
+ *
15
+ * Location: `<git-common-dir>/morph-spec/claims.json`, beside the fleet's
16
+ * env-registry, for the same three reasons — it can never dirty the `git
17
+ * status` the finish gates read, it survives `git clean -fdx`, and the COMMON
18
+ * dir is the one place every linked worktree of the repo shares. That last one
19
+ * is decisive here: in the observed incident one session sat inside the
20
+ * worktree and the other in the primary root, so a claim written to the
21
+ * branch-local `feature.json` would have been invisible to exactly the session
22
+ * that needed to read it.
23
+ *
24
+ * Writes are tmp+rename (torn file impossible); a corrupt registry reads as
25
+ * empty — the cost is a lost warning, never a crash.
26
+ */
27
+
28
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
29
+ import { join } from 'path';
30
+
31
+ /**
32
+ * A claim with no heartbeat for this long is considered dead. The heartbeat
33
+ * fires on tool activity, so this is really "how long a session may sit idle
34
+ * before its claim stops warning others".
35
+ */
36
+ export const CLAIM_TTL_MS = 30 * 60 * 1000;
37
+
38
+ /** Minimum gap between heartbeat WRITES. The heartbeat runs on every tool call. */
39
+ export const HEARTBEAT_THROTTLE_MS = 60 * 1000;
40
+
41
+ /** Claim file path for a repo's common git dir. */
42
+ export function claimsPath(gitCommonDir) {
43
+ return join(gitCommonDir, 'morph-spec', 'claims.json');
44
+ }
45
+
46
+ function readRegistry(gitCommonDir) {
47
+ try {
48
+ const raw = readFileSync(claimsPath(gitCommonDir), 'utf8').replace(/^/, '');
49
+ const parsed = JSON.parse(raw);
50
+ if (parsed && typeof parsed.claims === 'object' && parsed.claims !== null) return parsed;
51
+ } catch { /* fail-open: absent or corrupt registry restarts empty */ }
52
+ return { version: 1, claims: {} };
53
+ }
54
+
55
+ function writeRegistry(gitCommonDir, registry) {
56
+ const path = claimsPath(gitCommonDir);
57
+ mkdirSync(join(gitCommonDir, 'morph-spec'), { recursive: true });
58
+ const tmp = `${path}.tmp.${process.pid}`;
59
+ writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
60
+ renameSync(tmp, path);
61
+ }
62
+
63
+ /** Is this claim still within its TTL? A missing/unparseable heartbeat is dead. */
64
+ export function isLive(claim, now = Date.now()) {
65
+ const beat = Date.parse(claim?.heartbeatAt ?? '');
66
+ return Number.isFinite(beat) && now - beat < CLAIM_TTL_MS;
67
+ }
68
+
69
+ /**
70
+ * Claim a feature for this session, or refresh an existing claim of ours.
71
+ *
72
+ * A claim held by ANOTHER session is TAKEN OVER, not refused — advisory by
73
+ * design, and the caller has already been warned via `foreignClaim`. The
74
+ * takeover is recorded in `previousSessionId` so it is never silent.
75
+ *
76
+ * @param {string} feature
77
+ * @param {{ gitCommonDir: string, sessionId: string, pid?: number, path?: string }} opts
78
+ * @returns {object} the stored claim
79
+ */
80
+ export function acquireClaim(feature, { gitCommonDir, sessionId, pid = process.pid, path = null }) {
81
+ const registry = readRegistry(gitCommonDir);
82
+ const previous = registry.claims[feature];
83
+ const now = new Date().toISOString();
84
+ const claim = {
85
+ feature,
86
+ sessionId,
87
+ pid,
88
+ path: path ? String(path).replace(/\\/g, '/') : null,
89
+ claimedAt: previous?.sessionId === sessionId ? (previous.claimedAt || now) : now,
90
+ heartbeatAt: now,
91
+ ...(previous && previous.sessionId !== sessionId ? { previousSessionId: previous.sessionId } : {}),
92
+ };
93
+ registry.claims[feature] = claim;
94
+ writeRegistry(gitCommonDir, registry);
95
+ return claim;
96
+ }
97
+
98
+ /**
99
+ * Refresh our own claim's heartbeat. Throttled — this runs on every tool call,
100
+ * and a write per call would be pure I/O waste.
101
+ *
102
+ * Never touches a claim owned by another session: a heartbeat is proof of life,
103
+ * and forging someone else's would defeat the TTL.
104
+ *
105
+ * @param {string} feature
106
+ * @param {{ gitCommonDir: string, sessionId: string }} opts
107
+ * @returns {boolean} true when a write actually happened
108
+ */
109
+ export function heartbeatClaim(feature, { gitCommonDir, sessionId }) {
110
+ const registry = readRegistry(gitCommonDir);
111
+ const claim = registry.claims[feature];
112
+ if (!claim || claim.sessionId !== sessionId) return false;
113
+
114
+ const beat = Date.parse(claim.heartbeatAt ?? '');
115
+ if (Number.isFinite(beat) && Date.now() - beat < HEARTBEAT_THROTTLE_MS) return false;
116
+
117
+ claim.heartbeatAt = new Date().toISOString();
118
+ writeRegistry(gitCommonDir, registry);
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * The LIVE claim on this feature held by a session that is not us — the thing
124
+ * worth warning about. Returns null when the feature is unclaimed, claimed by
125
+ * us, or claimed by a session whose heartbeat went stale.
126
+ *
127
+ * @param {string} feature
128
+ * @param {{ gitCommonDir: string, sessionId?: string }} opts
129
+ * @returns {object|null}
130
+ */
131
+ export function foreignClaim(feature, { gitCommonDir, sessionId }) {
132
+ const claim = readRegistry(gitCommonDir).claims[feature];
133
+ if (!claim) return null;
134
+ if (sessionId && claim.sessionId === sessionId) return null;
135
+ return isLive(claim) ? claim : null;
136
+ }
137
+
138
+ /**
139
+ * Drop a feature's claim. Called by the manual `morph-spec claim <f> --release`
140
+ * (recovering from a session that died without releasing) and by `finish`.
141
+ *
142
+ * @param {string} feature
143
+ * @param {{ gitCommonDir: string }} opts
144
+ * @returns {boolean} false when there was nothing to release
145
+ */
146
+ export function releaseClaim(feature, { gitCommonDir }) {
147
+ const registry = readRegistry(gitCommonDir);
148
+ if (!registry.claims[feature]) return false;
149
+ delete registry.claims[feature];
150
+ writeRegistry(gitCommonDir, registry);
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Every claim on record, live and stale, newest heartbeat first. `live` is
156
+ * computed, never stored — a stored flag would go wrong the moment nobody
157
+ * rewrites the file.
158
+ *
159
+ * @param {string} gitCommonDir
160
+ * @returns {object[]}
161
+ */
162
+ export function listClaims(gitCommonDir) {
163
+ const now = Date.now();
164
+ return Object.values(readRegistry(gitCommonDir).claims)
165
+ .map((c) => ({ ...c, live: isLive(c, now) }))
166
+ .sort((a, b) => String(b.heartbeatAt).localeCompare(String(a.heartbeatAt)));
167
+ }
168
+
169
+ /** Human-readable age of a claim's last heartbeat ("3 min", "2 h", "—"). */
170
+ export function claimAge(claim, now = Date.now()) {
171
+ const beat = Date.parse(claim?.heartbeatAt ?? '');
172
+ if (!Number.isFinite(beat)) return '—';
173
+ const min = Math.max(0, Math.round((now - beat) / 60000));
174
+ return min < 60 ? `${min} min` : `${Math.round(min / 60)} h`;
175
+ }
@@ -33,6 +33,7 @@ export const OUTPUT_PHASE_MAP = {
33
33
  mandate: 'plan',
34
34
  tasks: 'plan',
35
35
  decisions: 'plan',
36
+ usecases: 'plan',
36
37
  recap: 'implement',
37
38
  reviewReport: 'review',
38
39
  evaluatorReport: 'review',