@polymorphism-tech/morph-spec 8.2.0 → 8.3.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.
- package/README.md +70 -99
- package/bin/morph-spec.js +13 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +76 -135
- package/docs/QUICKSTART.md +35 -51
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +20 -2
- package/framework/agents.json +3 -1
- package/framework/hooks/README.md +3 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +102 -0
- package/framework/hooks/shared/build-check.js +8 -1
- package/framework/hooks/shared/payload-utils.js +16 -2
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/telemetry-logger.js +75 -0
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/schemas/feature.schema.json +0 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +12 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +22 -10
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +19 -12
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +43 -1
- package/framework/standards/ai-agents/observability-patterns.md +156 -0
- package/framework/standards/ai-agents/production.md +3 -47
- package/framework/templates/REGISTRY.json +0 -93
- package/package.json +1 -1
- package/src/commands/observability/telemetry.js +75 -0
- package/src/commands/project/worktree.js +150 -36
- package/src/commands/state/score.js +13 -2
- package/src/commands/tasks/dag.js +11 -3
- package/src/core/paths/output-schema.js +28 -0
- package/src/core/state/state-manager.js +0 -128
- package/src/dashboard/app/pages/LiveState.jsx +0 -43
- package/src/lib/detectors/design-system-detector.js +0 -43
- package/src/lib/generators/index.js +0 -1
- package/src/lib/stack/stack-profile.js +40 -9
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/tasks/test-runner.js +26 -4
- package/src/lib/telemetry/telemetry-reader.js +103 -0
- package/src/lib/validators/validation-runner.js +13 -74
- package/src/utils/hooks-installer.js +27 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +0 -269
- package/framework/templates/context/CONTEXT.md +0 -181
- package/src/lib/agents/metrics-aggregator.js +0 -56
- package/src/lib/agents/metrics-calculator.js +0 -125
- package/src/lib/generators/metadata-extractor.js +0 -385
package/docs/QUICKSTART.md
CHANGED
|
@@ -19,11 +19,11 @@ morph-spec init
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
`init` does the following:
|
|
22
|
-
- Auto-detects your stack (
|
|
22
|
+
- Auto-detects your stack (.NET, Next.js, Node.js, monorepo)
|
|
23
23
|
- Creates `.morph/` with framework files, standards, and templates
|
|
24
24
|
- Creates `.morph/context/` with inferred project context
|
|
25
25
|
- Installs `.claude/skills/`, `.claude/agents/`, `.claude/rules/`
|
|
26
|
-
- Writes `.claude/settings.local.json` with
|
|
26
|
+
- Writes `.claude/settings.local.json` with the fail-open Claude Code hooks (see `docs/ARCHITECTURE.md`)
|
|
27
27
|
- Copies `CLAUDE.md` with runtime instructions
|
|
28
28
|
|
|
29
29
|
Verify the installation:
|
|
@@ -44,42 +44,31 @@ Open your project in Claude Code and run:
|
|
|
44
44
|
|
|
45
45
|
Replace `my-feature` with a short kebab-case name for your feature (e.g., `user-notifications`, `payment-flow`).
|
|
46
46
|
|
|
47
|
-
The proposal pipeline runs phases
|
|
47
|
+
The proposal pipeline runs the `proposal` and `plan` phases (plus the optional `uiux` phase), with an approval gate at each:
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### Proposal (Gate 1)
|
|
50
50
|
|
|
51
|
-
Claude analyzes your request, detects
|
|
51
|
+
Claude analyzes your request, detects the relevant stack and standards, and produces `0-proposal/proposal.md`. You review it and approve (or request changes).
|
|
52
52
|
|
|
53
|
-
###
|
|
53
|
+
### UI/UX — optional (Gate)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Only runs for UI-heavy features. Claude generates, under `1-design/`:
|
|
56
|
+
- `design-system.md` — colors, typography, spacing
|
|
57
|
+
- `mockups.md` — ASCII wireframes
|
|
58
|
+
- `components.md` — component specs
|
|
59
|
+
- `flows.md` — user flows and edge cases
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
You approve the UI design before moving on.
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
- `1-design/design-system.md` — colors, typography, spacing
|
|
61
|
-
- `1-design/mockups.md` — ASCII wireframes
|
|
62
|
-
- `1-design/components.md` — component specs
|
|
63
|
-
- `1-design/flows.md` — user flows and edge cases
|
|
63
|
+
### Plan (Gate 2)
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Claude produces, under `2-plan/`:
|
|
66
|
+
- `spec.md` — full requirements, use cases, validations
|
|
67
|
+
- `mandate.md` — the implementation contract (stack defaults, forbidden patterns)
|
|
68
|
+
- `tasks.json` — ordered task breakdown (`T001`, `T002`, …)
|
|
69
|
+
- `decisions.md` — architectural decision records (optional)
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Claude produces the technical specification:
|
|
70
|
-
- `2-design/spec.md` — full requirements, use cases, validations
|
|
71
|
-
- `2-design/contracts.cs` — C# interfaces, DTOs, enums
|
|
72
|
-
- `2-design/decisions.md` — architectural decision records (ADRs)
|
|
73
|
-
|
|
74
|
-
You review and approve the design.
|
|
75
|
-
|
|
76
|
-
### Phase 4 — Clarify
|
|
77
|
-
|
|
78
|
-
Claude identifies 3–7 ambiguities in the spec, asks you to resolve them, then updates the spec. No gate.
|
|
79
|
-
|
|
80
|
-
### Phase 6 — Tasks
|
|
81
|
-
|
|
82
|
-
Claude breaks the spec into atomic tasks (`T001`, `T002`, …) saved to `3-tasks/tasks.md`. You approve the task list.
|
|
71
|
+
Claude identifies ambiguities in the spec and resolves them with you before finalizing. You review and approve the spec + mandate + task list together.
|
|
83
72
|
|
|
84
73
|
At this point, planning is complete:
|
|
85
74
|
|
|
@@ -95,11 +84,11 @@ At this point, planning is complete:
|
|
|
95
84
|
/morph-apply my-feature
|
|
96
85
|
```
|
|
97
86
|
|
|
98
|
-
Claude reads `spec.md`, `
|
|
99
|
-
-
|
|
100
|
-
- Runs
|
|
101
|
-
-
|
|
102
|
-
- Generates `
|
|
87
|
+
Claude reads `spec.md`, `mandate.md`, and `tasks.json`, then implements task by task with auto-advance (no pause between tasks unless there's a blocking ambiguity):
|
|
88
|
+
- Tracks task progress with the native `TaskCreate`/`TaskUpdate` tools, persisted to `tasks.json`
|
|
89
|
+
- Runs the validators relevant to your stack after each task (e.g. `architecture`/`packages`/`contract-compliance` for .NET, `nextjs-component`/`css`/`design-system` for Next.js) via `morph-spec verify`
|
|
90
|
+
- Applies `morph-eval` after each task — a score below 9 triggers an autonomous correction loop, not a pause
|
|
91
|
+
- Generates `3-implement/recap.md` on completion, then Gate 3 review (`4-review/`)
|
|
103
92
|
|
|
104
93
|
---
|
|
105
94
|
|
|
@@ -108,21 +97,16 @@ Claude reads `spec.md`, `contracts.cs`, and `tasks.md`, then implements task by
|
|
|
108
97
|
Check feature state:
|
|
109
98
|
|
|
110
99
|
```bash
|
|
111
|
-
morph-spec state get my-feature
|
|
112
100
|
morph-spec status my-feature
|
|
113
101
|
```
|
|
114
102
|
|
|
115
|
-
|
|
103
|
+
Approve the current gate and scaffold the next phase folder in one step:
|
|
116
104
|
|
|
117
105
|
```bash
|
|
118
|
-
morph-spec
|
|
106
|
+
morph-spec advance my-feature
|
|
119
107
|
```
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
morph-spec task done my-feature T001
|
|
125
|
-
```
|
|
109
|
+
Task progress is tracked with the native `TaskCreate`/`TaskUpdate` tools and persisted to `tasks.json` — there is no separate CLI command for marking a task done.
|
|
126
110
|
|
|
127
111
|
Run validators at any time:
|
|
128
112
|
|
|
@@ -195,23 +179,23 @@ morph-spec worktree list
|
|
|
195
179
|
|
|
196
180
|
## Multiple features
|
|
197
181
|
|
|
198
|
-
You can have multiple features in progress simultaneously. Each has its own directory under `.morph/features
|
|
182
|
+
You can have multiple features in progress simultaneously. Each has its own directory under `.morph/features/`, with its own committed `feature.json`:
|
|
199
183
|
|
|
200
184
|
```
|
|
201
185
|
.morph/features/
|
|
202
186
|
├── user-notifications/
|
|
187
|
+
│ ├── feature.json
|
|
203
188
|
│ ├── 0-proposal/
|
|
204
|
-
│
|
|
205
|
-
│ └── 3-tasks/
|
|
189
|
+
│ └── 2-plan/
|
|
206
190
|
└── payment-flow/
|
|
207
|
-
├──
|
|
208
|
-
└──
|
|
191
|
+
├── feature.json
|
|
192
|
+
└── 0-proposal/
|
|
209
193
|
```
|
|
210
194
|
|
|
211
|
-
See
|
|
195
|
+
See every active feature — root and worktrees — with phase, gates, and activity:
|
|
212
196
|
|
|
213
197
|
```bash
|
|
214
|
-
morph-spec
|
|
198
|
+
morph-spec fleet
|
|
215
199
|
```
|
|
216
200
|
|
|
217
201
|
---
|
|
@@ -256,4 +240,4 @@ morph-spec doctor
|
|
|
256
240
|
|
|
257
241
|
---
|
|
258
242
|
|
|
259
|
-
*morph-spec v8.
|
|
243
|
+
*morph-spec v8.3.0 by Polymorphism Tech*
|
package/framework/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
26
26
|
- Apresentar gates com o template fixo definido em MORPH.md / morph-plan.
|
|
27
27
|
- Registrar decisões não triviais em `decisions.md`.
|
|
28
28
|
- Aplicar `morph-eval` após cada tarefa; silêncio em score ≥ 9 é o sinal correto. Score < 9 dispara o loop de correção autônomo (re-implementa, sem pausa), não escala ao humano entre tarefas. No Gate 3, o score composto da feature vem do **avaliador independente** despachado pela `morph-review` (persona `evaluator`, contexto fresco) — nunca de auto-avaliação de quem implementou.
|
|
29
|
-
- Incluir as 3 cláusulas do contrato do relatório final em todo dispatch de sub-agent (**
|
|
29
|
+
- Incluir as 3 cláusulas do contrato do relatório final em todo dispatch de sub-agent (**o sub-agent GRAVA o relatório em arquivo canônico ANTES de encerrar — `.morph/features/{f}/3-implement/reports/{taskId}.md`, ou `4-review/evaluator-report.md` para o avaliador — com os 4 itens obrigatórios (o quê / arquivos / problemas / auto-score 0-10); esse arquivo é o valor de retorno autoritativo, a última mensagem de texto é cortesia best-effort e pode não chegar inteira; ainda assim não chamar `SendMessage`, que gera um evento a mais e custa um turno inútil**; não tocar tasks.json/feature.json/board; sem build/test da suíte compartilhada em batch paralelo) — templates em `morph-implement` §4 e `morph-apply` §3b. **O orquestrador lê o arquivo ao retomar controle** — nunca confia só na mensagem de retorno da tool.
|
|
30
30
|
|
|
31
31
|
## Slash commands
|
|
32
32
|
|
|
@@ -45,7 +45,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
45
45
|
- `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
46
|
- `morph-spec archive <feature>` — arquiva a feature (atômico, git-aware); é o que `/morph-archive` chama.
|
|
47
47
|
- `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).
|
|
48
|
-
- `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**.
|
|
48
|
+
- `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.
|
|
49
49
|
- `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.
|
|
50
50
|
- `morph-spec create <feature> --type <feature|bug|chore|hotfix> [--request "<txt>"] [--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). `--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`); `--worktree` já nasce a feature no seu worktree próprio.
|
|
51
51
|
- `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.
|
|
@@ -53,6 +53,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
53
53
|
- `morph-spec worktree setup|link|finish|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; `finish` faz merge `--no-ff`, limpa e libera o bloco; `list` mostra fase/gate de cada worktree ativo).
|
|
54
54
|
- `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`.
|
|
55
55
|
- `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.
|
|
56
|
+
- `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.
|
|
56
57
|
|
|
57
58
|
## Skills core (em `.claude/skills/`)
|
|
58
59
|
|
package/framework/MORPH.md
CHANGED
|
@@ -269,14 +269,32 @@ Inline (sem dispatch) é aceitável para tasks curtas (effort S/M) **dentro do d
|
|
|
269
269
|
- Contexto de outras tarefas não relacionadas
|
|
270
270
|
- Outputs de ferramentas não pertinentes à tarefa
|
|
271
271
|
|
|
272
|
-
**O
|
|
272
|
+
**O ARQUIVO é o retorno, a última mensagem é cortesia.** Em uso real (feature `config-tenant-split`,
|
|
273
|
+
MORPH_GHLBrain, 2026-07-22 — overnight com dispatch de sub-agents), o evento recebido de volta do
|
|
274
|
+
sub-agent foi só um `idle_notification`, sem o texto — o orquestrador teve que reconstruir o
|
|
275
|
+
relatório do disco e pedir via `SendMessage` que o avaliador o persistisse. A última mensagem de
|
|
276
|
+
texto **pode não chegar** inteira ao orquestrador; por isso ela deixou de ser o canal
|
|
277
|
+
autoritativo. O sub-agent **sempre** grava o relatório também em um arquivo canônico:
|
|
278
|
+
|
|
279
|
+
- Task de implementação: `.morph/features/{feature}/3-implement/reports/{taskId}.md` (um arquivo
|
|
280
|
+
por task concluída, mesmo quando uma única sessão de sub-agent cobre várias tasks de um fluxo).
|
|
281
|
+
- Avaliador do Gate 3 (`morph-evaluator`): `.morph/features/{feature}/4-review/evaluator-report.md`
|
|
282
|
+
— o relatório BRUTO do avaliador; a `morph-review` o lê e sintetiza o `review-report.md`
|
|
283
|
+
apresentado no Gate 3 (os dois nunca colidem: um é saída crua do sub-agent, o outro é a curadoria
|
|
284
|
+
do orquestrador).
|
|
285
|
+
|
|
286
|
+
O orquestrador **lê o arquivo** ao retomar controle após o dispatch — nunca confia apenas na
|
|
287
|
+
mensagem de retorno da tool. Se a mensagem de texto final chegou inteira, ótimo (economiza uma
|
|
288
|
+
leitura); se não chegou, o arquivo já está lá.
|
|
289
|
+
|
|
290
|
+
**O que receber de volta (no arquivo, e na mensagem final quando ela chega):**
|
|
273
291
|
- O que foi feito (2-3 frases)
|
|
274
292
|
- Arquivos modificados (lista)
|
|
275
293
|
- Problemas encontrados e como foram resolvidos
|
|
276
294
|
- Auto-score 0-10 com justificativa em uma linha
|
|
277
295
|
|
|
278
296
|
**Contrato do relatório final** (cláusulas que todo prompt de dispatch inclui — ver templates em `morph-implement` §4 e `morph-apply` §3b):
|
|
279
|
-
1. **
|
|
297
|
+
1. **GRAVE o relatório no arquivo canônico do seu dispatch ANTES de encerrar** — é o valor de retorno autoritativo desta sessão, não a sua última mensagem de texto (que é cortesia best-effort e pode não chegar inteira ao orquestrador). Conteúdo obrigatório, idêntico no arquivo: (1) o que foi feito em 2-3 frases, (2) arquivos modificados, (3) problemas encontrados e como foram resolvidos, (4) auto-score 0-10 com justificativa em uma linha. Relatório ausente, oco, ou sem os 4 itens é **falha de protocolo** — registrada no recap. Ainda assim **não chame `SendMessage`**: ele gera um evento a mais (o teammate-message) além da notificação de término, custando um turno inútil ao orquestrador — a mensagem de texto final (cortesia) já é entregue automaticamente.
|
|
280
298
|
2. Não tocar em `tasks.json`, `feature.json`, `state.json` nem no board de sessão (TaskCreate/TaskUpdate) — todo estado é gravado exclusivamente pelo orquestrador.
|
|
281
299
|
3. **Verificação em batch paralelo — condicional ao isolamento:** em batch **sem isolamento** (agentes na mesma working tree), não rode build/test da suíte compartilhada — a verificação (`morph-spec verify`) é do orquestrador, uma vez, pós-batch. Em batch **com `isolation: "worktree"`** (quando o `morph-spec dag` recomenda, por colisão de `outputs` entre fluxos), a regra se inverte: rode build/test **dentro do seu worktree**, commite o trabalho na branch do worktree como último ato antes do relatório, reporte a branch + status de build, e **nunca** faça merge nem toque na árvore principal — a integração é do orquestrador.
|
|
282
300
|
|
package/framework/agents.json
CHANGED
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"ai-agents-prompts-in-db",
|
|
65
65
|
"ai-agents-rag-custom-pgvector",
|
|
66
66
|
"ai-agents-middleware-patterns",
|
|
67
|
+
"ai-agents-observability-patterns",
|
|
67
68
|
"ai-agents-multi-agent-patterns"
|
|
68
69
|
],
|
|
69
70
|
"mcp": ["context7"],
|
|
@@ -125,7 +126,8 @@
|
|
|
125
126
|
"standards": [
|
|
126
127
|
"backend-integrations-hangfire-hangfire-jobs",
|
|
127
128
|
"infrastructure-docker-coolify-deploy",
|
|
128
|
-
"backend-dotnet-scrutor"
|
|
129
|
+
"backend-dotnet-scrutor",
|
|
130
|
+
"ai-agents-observability-patterns"
|
|
129
131
|
],
|
|
130
132
|
"mcp": ["context7"],
|
|
131
133
|
"model": "sonnet",
|
|
@@ -51,7 +51,9 @@ framework/hooks/
|
|
|
51
51
|
| **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md + taskScores on every tasks.json write — the skills never write traces by hand |
|
|
52
52
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
53
53
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions; logs one `coherent`/`divergent` line per check in `.morph/logs/activity.json` (issue #63) |
|
|
54
|
+
| **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec telemetry` |
|
|
54
55
|
| **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
|
|
56
|
+
| **UserPromptSubmit** | telemetry-log.js | Telemetry | Appends the user's prompt to the same `events.jsonl` stream; silent, fail-open |
|
|
55
57
|
| **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift; build check; session snapshot |
|
|
56
58
|
|
|
57
59
|
## Design Principles
|
|
@@ -112,6 +114,7 @@ All Claude Code hooks import from `framework/hooks/shared/`:
|
|
|
112
114
|
| `phase-utils.js` | Phase constants, path extraction, file classification |
|
|
113
115
|
| `hook-response.js` | `block(reason)`, `approve(context)`, `injectContext(text)`, `pass()` |
|
|
114
116
|
| `stdin-reader.js` | `readStdin()` — Promise-based stdin JSON reader |
|
|
117
|
+
| `telemetry-logger.js` | `appendTelemetryEvent()`, `readTelemetryEvents()` — append-only JSONL event stream (`.morph/logs/events.jsonl`) |
|
|
115
118
|
|
|
116
119
|
`injectContext(text)` is how `worktree-context.js` (and any other SessionStart hook) delivers context: it emits `{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": text } }` on stdout. Claude Code only honors additional context nested under `hookSpecificOutput` — a bare top-level field is ignored.
|
|
117
120
|
|
|
@@ -52,7 +52,13 @@ function diffPlanContract(oldDoc, newDoc, mutableFields) {
|
|
|
52
52
|
const violations = [];
|
|
53
53
|
const mutable = new Set(mutableFields);
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// Backfilling a previously-absent `version` (undefined → any value) is a
|
|
56
|
+
// benign schema-tooling update, not a plan-contract change — it's what
|
|
57
|
+
// lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
|
|
58
|
+
// it without a lenient fallback. An actual change between two SET
|
|
59
|
+
// versions is still a real contract change and stays blocked.
|
|
60
|
+
const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
|
|
61
|
+
if (!versionBackfilled && oldDoc.version !== newDoc.version) {
|
|
56
62
|
violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
|
|
57
63
|
}
|
|
58
64
|
if (oldDoc.feature !== newDoc.feature) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry Hook: harness execution-path capture
|
|
5
|
+
*
|
|
6
|
+
* Events: PostToolUse (all tools) + UserPromptSubmit
|
|
7
|
+
*
|
|
8
|
+
* morph-spec is a Natural Language Harness: there is no central runtime to
|
|
9
|
+
* instrument — Claude Code executes the skills and sub-agents. The only
|
|
10
|
+
* vantage points are hooks. This one is the generic capture point: every tool
|
|
11
|
+
* call and every user prompt is appended (one JSON line) to
|
|
12
|
+
* .morph/logs/events.jsonl, from which `morph-spec telemetry` reconstructs the
|
|
13
|
+
* whole path — which skills ran, which personas were dispatched (with their
|
|
14
|
+
* prompts), which standards/knowledge-base files were read.
|
|
15
|
+
*
|
|
16
|
+
* Correlation is by an EXPLICIT key (feature + phase) written into each event,
|
|
17
|
+
* not by ambient span context — hooks fire as separate node processes with no
|
|
18
|
+
* shared trace context (the enrichment lesson from the observability standard:
|
|
19
|
+
* propagate a correlation key, a TraceId does not survive the boundary).
|
|
20
|
+
*
|
|
21
|
+
* SILENT: never blocks, never injects context — only appends. Fail-open on any
|
|
22
|
+
* error (exit 0). Only active inside a morph project (stateExists gate).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readStdin } from '../../shared/stdin-reader.js';
|
|
26
|
+
import { stateExists, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
27
|
+
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
28
|
+
import { pass } from '../../shared/hook-response.js';
|
|
29
|
+
|
|
30
|
+
function normalize(p) {
|
|
31
|
+
return (p || '').replace(/\\/g, '/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (!stateExists()) pass();
|
|
36
|
+
|
|
37
|
+
const payload = await readStdin();
|
|
38
|
+
if (!payload) pass();
|
|
39
|
+
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
|
|
42
|
+
// Correlation key: active feature + its phase (explicit, not ambient).
|
|
43
|
+
// NOTE (perf): getActiveFeature → loadState hydrates every .morph/features/*/
|
|
44
|
+
// feature.json, so enrichment is O(active-feature-count) per tool call. That
|
|
45
|
+
// is fine for realistic projects (archive removes finished features, so the
|
|
46
|
+
// dir holds only the few in-flight ones) but would grow on a project that
|
|
47
|
+
// keeps many un-archived features. If that ever bites, resolve the active
|
|
48
|
+
// feature from the thin state.json index alone. Kept accurate-over-cheap for
|
|
49
|
+
// now because phase is not stored in feature.json — it needs derivation.
|
|
50
|
+
let feature = null;
|
|
51
|
+
let phase = null;
|
|
52
|
+
try {
|
|
53
|
+
const active = getActiveFeature(cwd);
|
|
54
|
+
if (active) {
|
|
55
|
+
feature = active.name;
|
|
56
|
+
phase = active.feature?.phase || derivePhaseForFeature(active.name, cwd);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// enrichment is best-effort — a missing feature never blocks capture
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let event;
|
|
63
|
+
|
|
64
|
+
// UserPromptSubmit carries the user's prompt and no tool_name.
|
|
65
|
+
if (payload.hook_event_name === 'UserPromptSubmit' || (!payload.tool_name && typeof payload.prompt === 'string')) {
|
|
66
|
+
event = { event: 'UserPromptSubmit', tool: 'UserPrompt', prompt: payload.prompt || '' };
|
|
67
|
+
} else {
|
|
68
|
+
const tool = payload.tool_name;
|
|
69
|
+
if (!tool) pass();
|
|
70
|
+
const input = payload.tool_input || {};
|
|
71
|
+
event = { event: 'PostToolUse', tool };
|
|
72
|
+
|
|
73
|
+
if (tool === 'Skill') {
|
|
74
|
+
event.skill = input.skill || '';
|
|
75
|
+
} else if (tool === 'Agent' || tool === 'Task') {
|
|
76
|
+
event.persona = input.subagent_type || '';
|
|
77
|
+
if (input.description) event.desc = input.description;
|
|
78
|
+
event.prompt = input.prompt || '';
|
|
79
|
+
} else if (tool === 'Read') {
|
|
80
|
+
event.target = normalize(input.file_path || input.path);
|
|
81
|
+
if (/\/standards\//.test(event.target)) event.kind = 'standard';
|
|
82
|
+
} else if (tool === 'Bash') {
|
|
83
|
+
event.command = input.command || '';
|
|
84
|
+
} else if (tool === 'Write' || tool === 'Edit') {
|
|
85
|
+
event.target = normalize(input.file_path || input.path);
|
|
86
|
+
} else {
|
|
87
|
+
// Every other tool still counts toward "the whole path" — record a
|
|
88
|
+
// compact target so Grep/Glob/WebFetch bursts remain distinguishable.
|
|
89
|
+
const target = input.file_path || input.path || input.pattern || input.query || input.url || '';
|
|
90
|
+
if (target) event.target = normalize(String(target));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
event.feature = feature;
|
|
95
|
+
event.phase = phase;
|
|
96
|
+
|
|
97
|
+
appendTelemetryEvent(event, cwd);
|
|
98
|
+
pass();
|
|
99
|
+
} catch {
|
|
100
|
+
// Fail-open
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
@@ -84,7 +84,14 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
|
|
|
84
84
|
|
|
85
85
|
if (isFrontendStack(s)) {
|
|
86
86
|
const frontendDir = frontendPath ? join(cwd, frontendPath) : cwd;
|
|
87
|
-
|
|
87
|
+
// node_modules guard (same fail-open as post-edit-typecheck.js): a fresh worktree before
|
|
88
|
+
// `morph-spec worktree link` has tsconfig.json (versioned) but no toolchain — running
|
|
89
|
+
// `npx tsc` there fetches an unrelated "tsc" package and fails noisily. No toolchain →
|
|
90
|
+
// no tsc step; the real typecheck runs wherever node_modules exists (root/CI).
|
|
91
|
+
if (
|
|
92
|
+
existsSync(join(frontendDir, 'tsconfig.json')) &&
|
|
93
|
+
existsSync(join(frontendDir, 'node_modules'))
|
|
94
|
+
) {
|
|
88
95
|
steps.push({ command: TSC_BUILD_CMD, cwd: frontendDir });
|
|
89
96
|
}
|
|
90
97
|
}
|
|
@@ -93,7 +93,16 @@ export function getToolName(payload) {
|
|
|
93
93
|
* Build a normalized signature for loop detection.
|
|
94
94
|
* - Edit/Write: "Edit:normalized/file/path"
|
|
95
95
|
* - Bash: "Bash:first two words" (e.g., "Bash:npm test")
|
|
96
|
-
* -
|
|
96
|
+
* - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
|
|
97
|
+
* - No tool_input at all: just the tool name
|
|
98
|
+
*
|
|
99
|
+
* The general (non-Edit/Write/Bash) case matters: a burst of `Read`/`Skill`/
|
|
100
|
+
* `Grep`/`Agent` calls against DIFFERENT targets (phase-context loading,
|
|
101
|
+
* post-task evaluation reads, parallel subagent dispatch) is normal work, not
|
|
102
|
+
* a loop. Collapsing every one of them to the bare tool name is what made
|
|
103
|
+
* loop-detect fire on exactly that legitimate traffic — a real, reported
|
|
104
|
+
* false positive. Folding in the actual input (rather than hand-listing a
|
|
105
|
+
* target field per tool) fixes it uniformly for every tool at once.
|
|
97
106
|
*
|
|
98
107
|
* @param {Object} payload - Hook payload from stdin
|
|
99
108
|
* @returns {string} Signature string
|
|
@@ -116,5 +125,10 @@ export function getCommandSignature(payload) {
|
|
|
116
125
|
return words ? `Bash:${words}` : 'Bash';
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
|
|
128
|
+
try {
|
|
129
|
+
const json = JSON.stringify(input).replace(/\\/g, '/');
|
|
130
|
+
return json ? `${tool}:${json.slice(0, 200)}` : tool;
|
|
131
|
+
} catch {
|
|
132
|
+
return tool;
|
|
133
|
+
}
|
|
120
134
|
}
|
|
@@ -35,6 +35,7 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
35
35
|
decisions: 'plan',
|
|
36
36
|
recap: 'implement',
|
|
37
37
|
reviewReport: 'review',
|
|
38
|
+
evaluatorReport: 'review',
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
/** Map filename → output type (camelCase) */
|
|
@@ -52,6 +53,7 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
52
53
|
'decisions.md': 'decisions',
|
|
53
54
|
'recap.md': 'recap',
|
|
54
55
|
'review-report.md': 'reviewReport',
|
|
56
|
+
'evaluator-report.md': 'evaluatorReport',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
/** Protected spec files and the approval gate that locks them */
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Logger — append-only harness event stream
|
|
3
|
+
*
|
|
4
|
+
* Writes one JSON line per event to .morph/logs/events.jsonl. This is the sink
|
|
5
|
+
* for the `telemetry-log` hook, which fires on every tool call (PostToolUse,
|
|
6
|
+
* all tools) and on every user prompt (UserPromptSubmit) — the only vantage
|
|
7
|
+
* points an NLH exposes. The whole execution path (skills, sub-agent dispatch,
|
|
8
|
+
* knowledge-base reads, prompts) is reconstructable by reading this file.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints (same discipline as activity-logger, plus one):
|
|
11
|
+
* - Synchronous only (hooks run in process, no async overhead)
|
|
12
|
+
* - Fail-silent (never throws — hooks must not be blocked by the logger)
|
|
13
|
+
* - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
|
|
14
|
+
* read-modify-write of a whole JSON doc (activity-logger) is O(n²) across a
|
|
15
|
+
* session and blows the ≤5ms budget once tool calls number in the hundreds.
|
|
16
|
+
* - Windows-safe paths via path.join
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
|
|
20
|
+
import { join, dirname } from 'path';
|
|
21
|
+
|
|
22
|
+
const LOGS_DIR = '.morph/logs';
|
|
23
|
+
export const EVENTS_FILE = join(LOGS_DIR, 'events.jsonl');
|
|
24
|
+
|
|
25
|
+
function getEventsPath(projectPath) {
|
|
26
|
+
return join(projectPath || process.cwd(), EVENTS_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Append a single telemetry event as one JSON line.
|
|
31
|
+
* Stamps an ISO `ts` when the caller omits one.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} event - Event payload (tool, skill/persona/target/prompt, feature, phase…)
|
|
34
|
+
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
35
|
+
*/
|
|
36
|
+
export function appendTelemetryEvent(event, projectPath) {
|
|
37
|
+
try {
|
|
38
|
+
const eventsPath = getEventsPath(projectPath);
|
|
39
|
+
const dir = dirname(eventsPath);
|
|
40
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
41
|
+
|
|
42
|
+
const stamped = { ts: event?.ts || new Date().toISOString(), ...event };
|
|
43
|
+
appendFileSync(eventsPath, JSON.stringify(stamped) + '\n', 'utf-8');
|
|
44
|
+
} catch {
|
|
45
|
+
// Fail-silent — never block hooks
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the full event stream back into an array. A corrupt/truncated line
|
|
51
|
+
* (e.g. a killed process mid-write) is skipped, not fatal.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} [projectPath]
|
|
54
|
+
* @returns {Object[]} events in write order (may be empty)
|
|
55
|
+
*/
|
|
56
|
+
export function readTelemetryEvents(projectPath) {
|
|
57
|
+
try {
|
|
58
|
+
const eventsPath = getEventsPath(projectPath);
|
|
59
|
+
if (!existsSync(eventsPath)) return [];
|
|
60
|
+
const raw = readFileSync(eventsPath, 'utf-8');
|
|
61
|
+
const events = [];
|
|
62
|
+
for (const line of raw.split('\n')) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed) continue;
|
|
65
|
+
try {
|
|
66
|
+
events.push(JSON.parse(trimmed));
|
|
67
|
+
} catch {
|
|
68
|
+
// skip a single corrupt line — don't lose the whole stream
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return events;
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -20,6 +20,7 @@ stacks:
|
|
|
20
20
|
@.morph/framework/standards/ai-agents/prompts-in-db.md
|
|
21
21
|
@.morph/framework/standards/ai-agents/rag-custom-pgvector.md
|
|
22
22
|
@.morph/framework/standards/ai-agents/middleware-patterns.md
|
|
23
|
+
@.morph/framework/standards/ai-agents/observability-patterns.md
|
|
23
24
|
@.morph/framework/standards/ai-agents/workflows.md
|
|
24
25
|
@.morph/framework/standards/ai-agents/production.md
|
|
25
26
|
|
|
@@ -195,12 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
"additionalProperties": true
|
|
197
197
|
},
|
|
198
|
-
"activeAgents": { "type": "array", "items": { "type": "string" } },
|
|
199
198
|
"checkpoints": { "type": "array" },
|
|
200
|
-
"dispatchLog": {
|
|
201
|
-
"type": "array",
|
|
202
|
-
"description": "Dispatch events (capped at 50). Entries: { timestamp, phase, mode, agents: [{ id, subagentType, group, tasks }] }."
|
|
203
|
-
},
|
|
204
199
|
"trustConfig": { "type": "object" },
|
|
205
200
|
"contextBundles": { "type": "array" },
|
|
206
201
|
"fileChanges": { "type": "array" },
|
|
@@ -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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -135,11 +135,16 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
|
|
|
135
135
|
Não modifique spec.md, mandate.md.
|
|
136
136
|
|
|
137
137
|
Contrato do relatório final:
|
|
138
|
-
1.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
1. GRAVE um relatório por task concluída em
|
|
139
|
+
`.morph/features/{feature}/3-implement/reports/{taskId}.md` ANTES de
|
|
140
|
+
encerrar — mesmo quando esta sessão cobre várias tasks do fluxo, um
|
|
141
|
+
arquivo por task. Esse arquivo é o valor de retorno autoritativo; a sua
|
|
142
|
+
última mensagem de texto é cortesia best-effort e pode não chegar
|
|
143
|
+
inteira ao orquestrador (o evento recebido às vezes é só um
|
|
144
|
+
idle_notification, sem texto). Ainda assim NÃO chame SendMessage: ele
|
|
145
|
+
gera um evento a mais além da notificação de término, custando um turno
|
|
146
|
+
inútil ao orquestrador. Relatório ausente, oco, ou sem os itens abaixo,
|
|
147
|
+
é falha de protocolo.
|
|
143
148
|
2. NÃO toque em tasks.json, feature.json, state.json nem no board de sessão
|
|
144
149
|
(TaskCreate/TaskUpdate) — todo estado (status de task, scores, gates) é
|
|
145
150
|
gravado exclusivamente pelo orquestrador.
|
|
@@ -163,7 +168,7 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
|
|
|
163
168
|
|
|
164
169
|
> **Alternativa quando os fluxos tocam os mesmos projetos/arquivos** (ex.: o mesmo csproj de testes compartilhado): siga a §3b-w abaixo em vez desta seção — despache com `isolation: "worktree"`, cada agente trabalha numa working tree própria e o orquestrador integra ao final.
|
|
165
170
|
|
|
166
|
-
Aguardar todos os agentes terminarem
|
|
171
|
+
Aguardar todos os agentes terminarem, depois **ler primeiro** `.morph/features/{feature}/3-implement/reports/{taskId}.md` de cada task — é o valor de retorno autoritativo; a mensagem de texto do dispatch, se chegou inteira, é só um atalho que economiza essa leitura. Se um arquivo vier ausente, oco, ou sem os itens da cláusula 1, registre a falha de protocolo no recap. Depois, sequencialmente:
|
|
167
172
|
- **Rodar `morph-spec verify {feature} --json` uma única vez, pós-batch** (escopo feature) — os agentes não a rodaram (cláusula 3). Vermelho: use `verify.feedback` para corrigir/redespachar ANTES de qualquer morph-eval. (Com isolamento por worktree, cada agente já rodou o verify dentro do seu worktree; o orquestrador roda o verify compartilhado UMA vez após o merge — ver §3b-w.)
|
|
168
173
|
- Para cada relatório retornado:
|
|
169
174
|
- Aplicar `Skill(morph-eval)`.
|
|
@@ -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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
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.
|
|
7
|
+
cliVersion: "8.3.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|