@polymorphism-tech/morph-spec 8.6.0 → 8.8.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 (47) 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 +6 -3
  7. package/framework/MORPH.md +28 -3
  8. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  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/claude-code/statusline.py +545 -521
  12. package/framework/hooks/shared/claim-registry.js +175 -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 +34 -1
  16. package/framework/schemas/feature.schema.json +2 -1
  17. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  18. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  19. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  20. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  21. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  22. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  23. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +7 -1
  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/package.json +1 -1
  30. package/src/commands/project/claim.js +91 -0
  31. package/src/commands/project/doctor.js +65 -0
  32. package/src/commands/project/fleet.js +37 -2
  33. package/src/commands/project/worktree.js +70 -7
  34. package/src/commands/state/advance.js +60 -1
  35. package/src/commands/state/approve.js +125 -2
  36. package/src/commands/state/create.js +3 -3
  37. package/src/commands/state/finish.js +88 -3
  38. package/src/commands/state/list.js +9 -1
  39. package/src/commands/state/report-check.js +202 -0
  40. package/src/core/state/lifecycle-audit.js +57 -0
  41. package/src/dashboard/scanner/state-scanner.js +11 -3
  42. package/src/lib/generators/architecture-scanner.js +1 -1
  43. package/src/lib/tasks/tasks-json-parser.js +8 -0
  44. package/src/lib/worktree/fleet-scanner.js +224 -15
  45. package/src/lib/worktree/worktree-audit.js +181 -0
  46. package/src/utils/file-copier.js +5 -1
  47. package/src/utils/hooks-installer.js +7 -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.6.0
6
+ **Version:** 8.8.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.6.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.8.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.6.0",
3
+ "version": "8.8.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.6.0 by Polymorphism Tech*
188
+ *morph-spec v8.8.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.6.0 by Polymorphism Tech*
252
+ *morph-spec v8.8.0 by Polymorphism Tech*
@@ -43,7 +43,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
43
43
  | `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
44
44
 
45
45
  **Comandos CLI de gate e de estado** (a LLM os usa em vez de editar o estado à mão):
46
- - `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).
47
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).
48
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.
49
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.
@@ -55,9 +55,12 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
55
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).
56
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.
57
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.
58
- - `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).
59
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`.
60
- - `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`.
61
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.
62
65
 
63
66
  ## Skills core (em `.claude/skills/`)
@@ -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.
@@ -307,6 +307,31 @@ O orquestrador **lê o arquivo** ao retomar controle após o dispatch — nunca
307
307
  mensagem de retorno da tool. Se a mensagem de texto final chegou inteira, ótimo (economiza uma
308
308
  leitura); se não chegou, o arquivo já está lá.
309
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
+
310
335
  **O que receber de volta (no arquivo, e na mensagem final quando ela chega):**
311
336
  - O que foi feito (2-3 frases)
312
337
  - Arquivos modificados (lista)
@@ -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
  }