@polymorphism-tech/morph-spec 8.19.0 → 8.21.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 +2 -2
- package/bin/morph-spec.js +2 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -11
- package/framework/MORPH.md +53 -55
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +3 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +23 -2
- package/framework/hooks/claude-code/core/state-sync.js +5 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +34 -3
- package/framework/hooks/claude-code/core/validate-completion.js +116 -4
- package/framework/hooks/claude-code/core/worktree-context.js +9 -4
- package/framework/hooks/shared/telemetry-logger.js +64 -1
- package/framework/phases.json +1 -1
- package/framework/schemas/tasks.schema.json +1 -1
- 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 +6 -15
- package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
- 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 +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +6 -4
- package/framework/skills/core/morph-proposal/SKILL.md +10 -1
- package/framework/skills/core/morph-review/SKILL.md +4 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +29 -15
- package/framework/skills/core/morph-uiux/SKILL.md +135 -0
- package/package.json +1 -1
- package/src/commands/observability/cost.js +93 -5
- package/src/commands/project/update.js +2 -63
- package/src/commands/state/gate-check.js +10 -3
- package/src/core/templates/template-registry.js +2 -30
- package/src/lib/git/default-branch.js +44 -8
- package/src/lib/installers/templates-reconcile.js +81 -0
- package/src/lib/risk-detector.js +20 -1
- package/src/lib/telemetry/telemetry-reader.js +80 -4
- package/src/scripts/setup-infra.js +23 -5
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
|
+
**Version:** 8.21.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
368
368
|
|
|
369
369
|
---
|
|
370
370
|
|
|
371
|
-
*morph-spec v8.
|
|
371
|
+
*morph-spec v8.21.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -428,7 +428,8 @@ program
|
|
|
428
428
|
program
|
|
429
429
|
.command('cost [feature]')
|
|
430
430
|
.description('Cost-facing view over .morph/logs/events.jsonl: task duration, bytes re-ingested by kind (proxy, not tokens), dispatch counts')
|
|
431
|
-
.option('--by <mode>', 'Group by: task|phase (default: feature-wide summary)')
|
|
431
|
+
.option('--by <mode>', 'Group by: task|phase (default: feature-wide summary; with --all-trees, applies to the combined aggregate)')
|
|
432
|
+
.option('--all-trees', 'Aggregate .morph/logs/events.jsonl across every git worktree that has one, not just the current tree — per-tree breakdown + combined total')
|
|
432
433
|
.option('--json', 'Output the aggregate as JSON')
|
|
433
434
|
.action((feature, options) => costCommand(feature, options));
|
|
434
435
|
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -79,6 +79,7 @@ Prefixo de todo comando: `morph-spec`. Flags/exemplos: `.morph/framework/CLI.md`
|
|
|
79
79
|
|-------|-------------|
|
|
80
80
|
| `morph-brainstorming` | Entender negócio, escanear stack, produzir proposal.md |
|
|
81
81
|
| `morph-stack-scan` | Detectar desvios de stack em projeto brownfield |
|
|
82
|
+
| `morph-uiux` | Ramo opcional uiux — design system, mockups, componentes, fluxos |
|
|
82
83
|
| `morph-plan` | Destilar use cases e produzir spec.md + mandate.md + tasks.json |
|
|
83
84
|
| `morph-implement` | Executar uma tarefa com auto-advance entre tasks |
|
|
84
85
|
| `morph-eval` | Aplicar rubricas e produzir score 0-10 |
|
|
@@ -88,7 +89,7 @@ Prefixo de todo comando: `morph-spec`. Flags/exemplos: `.morph/framework/CLI.md`
|
|
|
88
89
|
|
|
89
90
|
## Roteamento por tipo de trabalho
|
|
90
91
|
|
|
91
|
-
`workType` (`create --type <tipo>` ou classificado de `--request`) dimensiona o pipeline: `feature` completo, `bug` enxuto, `chore` fast-track (Gates 1/2 por política), `hotfix` scout→fix→gate humano→ship. Canônico: `MORPH.md` §2.1/§5.
|
|
92
|
+
`workType` (`create --type <tipo>` ou classificado de `--request`) dimensiona o pipeline: `feature` completo (+ ramo uiux quando há superfície de UI), `bug` enxuto, `chore` fast-track (Gates 1/2 por política), `hotfix` scout→fix→gate humano→ship. Canônico: `MORPH.md` §2.1/§5.
|
|
92
93
|
|
|
93
94
|
Features independentes rodam em paralelo, uma por git worktree (`create --worktree`/`worktree setup`), bloco de portas próprio + handoff.md. Fechamento: `finish <f> --pr|--merge`; agregado: `fleet`. Canônico: `MORPH.md` §2.2.
|
|
94
95
|
|
|
@@ -104,15 +105,6 @@ Drift entre `agents.json` e os `.md` gerados é detectado por `morph-spec doctor
|
|
|
104
105
|
|
|
105
106
|
## Regra de precedência de skills
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
| Em vez de | Use |
|
|
110
|
-
|-----------|-----|
|
|
111
|
-
| `superpowers:brainstorming` | `morph-brainstorming` |
|
|
112
|
-
| `superpowers:writing-plans` | `morph-plan` |
|
|
113
|
-
| `superpowers:executing-plans` | `morph-apply` |
|
|
114
|
-
| `superpowers:test-driven-development` | seguir `mandate.md` (TDD está embutido) |
|
|
115
|
-
|
|
116
|
-
As skills `morph-*` aplicam Gates e leem/produzem outputs nos paths corretos de `.morph/`. As `superpowers:*` são genéricas e não conhecem o contexto morph-spec.
|
|
108
|
+
Prefira `morph-*` a `superpowers:*` equivalentes (`brainstorming`→`morph-brainstorming`, `writing-plans`→`morph-plan`, `executing-plans`→`morph-apply`, `test-driven-development`→seguir `mandate.md`, TDD embutido): `morph-*` aplica Gates e paths corretos de `.morph/`; `superpowers:*` é genérico, sem esse contexto.
|
|
117
109
|
|
|
118
110
|
*MORPH-SPEC v5 by Polymorphism Tech*
|
package/framework/MORPH.md
CHANGED
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
Entre gates, você opera com autonomia total. O dev valida. Você executa.
|
|
14
14
|
|
|
15
|
-
| Situação
|
|
16
|
-
|
|
17
|
-
| Gate 1 / 2 / 3 aprovado
|
|
18
|
-
| Tarefa completada na fase implement
|
|
19
|
-
| Ambiguidade de escopo coberta pelo mandate
|
|
20
|
-
| Ambiguidade bloqueante (requisito ausente ou contraditório)
|
|
21
|
-
| Sub-agent retornou relatório (qualquer auto-score)
|
|
22
|
-
| Sub-agent com auto-score < 7
|
|
23
|
-
| Build/typecheck quebra
|
|
24
|
-
| Task implementada (código escrito)
|
|
25
|
-
| Score morph-eval ≥ 9
|
|
26
|
-
| Score morph-eval < 9 numa task
|
|
27
|
-
| Score morph-eval < 9 após 3 tentativas
|
|
15
|
+
| Situação | O que você faz |
|
|
16
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
17
|
+
| Gate 1 / 2 / 3 aprovado | Avançar para próxima fase imediatamente |
|
|
18
|
+
| Tarefa completada na fase implement | Iniciar a próxima task do tasks.json sem aguardar instrução |
|
|
19
|
+
| Ambiguidade de escopo coberta pelo mandate | Resolver conservadoramente e registrar em decisions.md |
|
|
20
|
+
| Ambiguidade bloqueante (requisito ausente ou contraditório) | UMA pergunta via AskUserQuestion |
|
|
21
|
+
| Sub-agent retornou relatório (qualquer auto-score) | Auto-score é triagem, não aprovação: a task só avança com morph-eval ≥ 9 |
|
|
22
|
+
| Sub-agent com auto-score < 7 | Re-despachar com feedback específico (ou corrigir inline) antes do morph-eval |
|
|
23
|
+
| Build/typecheck quebra | Corrigir antes de marcar tarefa como done |
|
|
24
|
+
| Task implementada (código escrito) | `morph-spec verify <feature> <task>` ANTES do juiz: vermelho = feedback ao loop sem gastar o morph-eval; verde → morph-eval avalia o que o código não pega |
|
|
25
|
+
| Score morph-eval ≥ 9 | Silêncio (aprovação implícita), próxima tarefa |
|
|
26
|
+
| Score morph-eval < 9 numa task | Loop de correção autônomo: re-implementar com feedback priorizado, até 3 tentativas, sem pausa humana |
|
|
27
|
+
| Score morph-eval < 9 após 3 tentativas | Escalar ao usuário (aceitar/continuar/pular) — único ponto de pausa do loop |
|
|
28
28
|
|
|
29
29
|
**Regra de ouro:** dúvida que não bloqueia → resolver e registrar; dúvida que bloqueia → uma pergunta, máximo. Vale **entre gates** (plan/implement/review) — **não** vale na abertura da feature: a fase proposal pode rodar em **modo entrevista** (escolhido na pergunta de abertura, `morph-brainstorming` Parte 2), onde perguntar até fechar o entendimento é o comportamento desejado.
|
|
30
30
|
|
|
@@ -36,16 +36,16 @@ Entre gates, você opera com autonomia total. O dev valida. Você executa.
|
|
|
36
36
|
|
|
37
37
|
O harness conhece o stack por padrão. Não pergunte sobre tecnologia a menos que haja ambiguidade real.
|
|
38
38
|
|
|
39
|
-
| Camada
|
|
40
|
-
|
|
41
|
-
| Backend
|
|
42
|
-
| ORM/DB
|
|
43
|
-
| IDs
|
|
44
|
-
| Classes
|
|
45
|
-
| Erros
|
|
46
|
-
| IA
|
|
47
|
-
| Frontend
|
|
48
|
-
| Jobs async | Hangfire — apenas quando há razão real
|
|
39
|
+
| Camada | Stack |
|
|
40
|
+
| ------------ | -------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Backend | .NET 10 + VSA (feature-folder: handler + validator + endpoint + errors + tests) |
|
|
42
|
+
| ORM/DB | EF Core + Neon (Postgres) |
|
|
43
|
+
| IDs | `Guid.CreateVersion7()` (nunca `Guid.NewGuid()`) |
|
|
44
|
+
| Classes | `sealed` por padrão |
|
|
45
|
+
| Erros | Result pattern com `result.Match()` (nunca exceptions como fluxo) |
|
|
46
|
+
| IA | Microsoft Agent Framework (MAF) — quando houver agentes |
|
|
47
|
+
| Frontend | Next.js App Router + Tailwind + Framer Motion + Shadcn |
|
|
48
|
+
| Jobs async | Hangfire — apenas quando há razão real |
|
|
49
49
|
|
|
50
50
|
Padrões proibidos automaticamente injetados no mandate:
|
|
51
51
|
- `Guid.NewGuid()` → `Guid.CreateVersion7()`
|
|
@@ -77,16 +77,16 @@ Os papéis não são exclusivos — numa sessão longa você transita entre eles
|
|
|
77
77
|
|
|
78
78
|
O fluxo padrão de uma feature tem quatro fases, cada uma com pasta dedicada em `.morph/features/{feature}/`. Estado autoritativo e commitado: `.morph/features/{feature}/feature.json` (status, approvalGates, outputs, tasks, gateDecisions, taskScores, archivedAt); `.morph/state.json` na raiz é só índice fino gitignored derivado dele; identidade do projeto em `.morph/config/config.json` (commitado).
|
|
79
79
|
|
|
80
|
-
| Fase
|
|
81
|
-
|
|
82
|
-
| proposal
|
|
83
|
-
| plan
|
|
84
|
-
| implement
|
|
85
|
-
| review
|
|
80
|
+
| Fase | Pasta | Gate | O que termina a fase |
|
|
81
|
+
| ------------- | ---------------- | ----------------------- | ------------------------------------------------- |
|
|
82
|
+
| proposal | `0-proposal/` | Gate 1 (trust-aware) | `proposal.md` aprovado e commitado |
|
|
83
|
+
| plan | `2-plan/` | Gate 2 (trust-aware) | `spec.md`, `mandate.md`, `tasks.json` aprovados |
|
|
84
|
+
| implement | `3-implement/` | Nenhum | Todas as tasks do tasks.json com status `done` |
|
|
85
|
+
| review | `4-review/` | Gate 3 (trust-aware) | `review-report.md` aprovado e commitado |
|
|
86
86
|
|
|
87
87
|
A fase `proposal` tem dois **modos de descoberta**, escolhidos pelo usuário na abertura (só `workType: feature`/`bug` — `chore`/`hotfix` nascem em `implement`): **entrevista** (uma pergunta por vez até a árvore de decisões abertas zerar; Gate 1 pausa sempre) e **automático** (até 4 perguntas em lote; Gate 1 trust-aware). Nos dois modos a varredura do ambiente roda **antes** de qualquer pergunta — fato que `morph-stack-scan` ou o acervo de use cases responde não vira pergunta. Detalhes em `morph-brainstorming`.
|
|
88
88
|
|
|
89
|
-
A fase `1-design/`
|
|
89
|
+
A fase `1-design/` é opcional — critério objetivo de entrada em §2.1.
|
|
90
90
|
|
|
91
91
|
Não avance de fase sem o gate correspondente ter sido confirmado.
|
|
92
92
|
|
|
@@ -95,12 +95,14 @@ Não avance de fase sem o gate correspondente ter sido confirmado.
|
|
|
95
95
|
Nem todo trabalho merece o pipeline completo. Cada feature nasce com **identidade** (`name` = slug + `description` obrigatória) e um **workType** (`morph-spec create <f> --description "<o quê/porquê>" --type <tipo>`, ou classificado de `--request "<pedido>"`, que também semeia a descrição) que dimensiona o pipeline:
|
|
96
96
|
|
|
97
97
|
| Tipo | Pipeline | Gates |
|
|
98
|
-
|
|
98
|
+
| ------ | ---------- | ------- |
|
|
99
99
|
| `feature` | proposal → [uiux] → plan → implement → review (completo) | 1, 2, 3 |
|
|
100
100
|
| `bug` | repro+diagnóstico enxuto (proposal lean) → plan enxuto → implement → review | 1, 2, 3 |
|
|
101
101
|
| `chore` | fast-track: direto para implement (Gates 1/2 carimbados por política no nascimento) → review leve | só 3 |
|
|
102
102
|
| `hotfix` | scout (diagnóstico read-only) → fix cirúrgico → **aprovação humana obrigatória** → test loop → ship | 3 (humano sempre) |
|
|
103
103
|
|
|
104
|
+
**Ramo uiux (critério objetivo):** feature/bug que cria ou altera superfície de UI (componente, página, tela, estilo, fluxo de navegação) toma o ramo `1-design/` via `morph-spec advance {feature} --design`; sem superfície de UI, direto para plan. Skill: `morph-uiux`. Avaliado por `morph-proposal`/`morph-brainstorming` no fecho do Gate 1 — não redecida em nenhum outro ponto do pipeline.
|
|
105
|
+
|
|
104
106
|
**Como classificar (router):** sinais determinísticos primeiro — `morph-spec create --request "<texto>"` roda o detector por keywords (hotfix: "produção caiu/urgente/incident/p0"; bug: "bug/corrigir/exception/regressão"; chore: "typo/rename/bump/config/docs/cleanup"). As keywords casam como **palavra inteira** ("group0" não é `p0`, "prefix" não é `fix`, "debug" não é `bug`) — casar demais pularia gates num pedido inócuo; por isso plural e flexão são listados explicitamente ("bugs", "incidentes", "configs"). Confiança `high` → usa o tipo. Empate (`tie`) ou nenhum sinal (`none`) → o comando sai com código 2 e os candidatos; **você decide como roteador** e re-roda com `--type`. Só vale uma `AskUserQuestion` quando o pedido é genuinamente indeterminado.
|
|
105
107
|
|
|
106
108
|
**Como o skip funciona:** `chore`/`hotfix` têm os gates `proposal`/`plan` carimbados no nascimento com `approvedBy: "policy:{tipo}"`. Como `derivePhase` é gate-aware, o trabalho já nasce em `implement` — sem tocar `phases.json`. Artefatos reutilizam os nomes canônicos (hotfix escreve brief curto em `proposal.md`; ambos escrevem `2-plan/tasks.json` mínimo de 1-3 tasks para ligar trace/score/dag).
|
|
@@ -212,7 +214,7 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), auto-pas
|
|
|
212
214
|
**Sinais de risco que forçam pausa em auto** (`gate-check` aplica esta tabela via `shouldPauseGate` — fonte única; sinal que só a sessão conhece escala via `gate-decision` manual):
|
|
213
215
|
|
|
214
216
|
| Sinal | Threshold | Quando aplica |
|
|
215
|
-
|
|
217
|
+
| ------- | ----------- | --------------- |
|
|
216
218
|
| `designSystemDivergence` | true | Brainstorming detecta fontes conflitantes (spec antigo vs bundle local) |
|
|
217
219
|
| `standardsViolations` | array.length > 0 | Mandate ou implementation viola padrões proibidos |
|
|
218
220
|
| `evalScore` | < 9 | score composto da feature (via morph-review) abaixo do mínimo |
|
|
@@ -220,7 +222,7 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), auto-pas
|
|
|
220
222
|
| `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (detalhe abaixo, "Gates por tipo de trabalho") |
|
|
221
223
|
|
|
222
224
|
| Gate | Sinais | Skill responsável |
|
|
223
|
-
|
|
225
|
+
| ------ | -------- | -------------------- |
|
|
224
226
|
| 1 — Proposta | `designSystemDivergence`, `standardsViolations` (improvável) | `morph-brainstorming` |
|
|
225
227
|
| 2 — Plano | Gate 1 + `taskCount`, `standardsViolations` em mandate | `morph-plan` |
|
|
226
228
|
| 3 — Review | `evalScore` composite, `standardsViolations` em implementation | `morph-review` |
|
|
@@ -287,7 +289,7 @@ morph-spec report-check <feature> [task] --json
|
|
|
287
289
|
```
|
|
288
290
|
|
|
289
291
|
| veredito | o que significa | ação |
|
|
290
|
-
|
|
292
|
+
| --- | --- | --- |
|
|
291
293
|
| `ok` | relatório presente e com os 4 itens | siga o fluxo |
|
|
292
294
|
| `report-incomplete` | relatório existe, faltam itens (ele lista quais) | complete a partir da mensagem de retorno, carimbe **DEGRADADO** |
|
|
293
295
|
| `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** |
|
|
@@ -330,27 +332,23 @@ Não invoque uma skill fora do momento indicado.
|
|
|
330
332
|
|
|
331
333
|
Path de cada skill: `framework/skills/core/{nome}/SKILL.md`.
|
|
332
334
|
|
|
333
|
-
| Skill
|
|
334
|
-
|
|
335
|
-
| **morph-proposal**
|
|
336
|
-
| **morph-apply**
|
|
337
|
-
| morph-brainstorming
|
|
338
|
-
| morph-stack-scan
|
|
339
|
-
| morph-
|
|
340
|
-
| morph-
|
|
341
|
-
| morph-
|
|
342
|
-
| morph-
|
|
343
|
-
| morph-
|
|
344
|
-
| morph-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
Slash commands são atalhos: o agente pode chamar `Skill(morph-proposal)` diretamente quando o usuário pede "use /morph-proposal".
|
|
352
|
-
|
|
353
|
-
Cada skill contém instruções passo a passo. Leia, siga, não desvie sem justificativa no recap.md. Apenas `framework/skills/core/` é instalado. Nenhuma skill deve ser invocada proativamente por um hook ou script — skills são instrumentos do modelo, você decide quando a situação pede o rigor que a skill fornece.
|
|
335
|
+
| Skill | Quando invocar |
|
|
336
|
+
| ----------------------- | --------------------------------------------------------------------------- |
|
|
337
|
+
| **morph-proposal** | **Orquestrador**: brainstorming → Gate 1 → plan → Gate 2 (entry point) |
|
|
338
|
+
| **morph-apply** | **Orquestrador**: DAG-adaptive implement loop → review → Gate 3 |
|
|
339
|
+
| morph-brainstorming | Invocada pela morph-proposal — entendimento de negócio + stack-scan |
|
|
340
|
+
| morph-stack-scan | Detecta stack atual; curto-circuita em greenfield |
|
|
341
|
+
| morph-uiux | Ramo opcional uiux (§2.1) — design system, mockups, componentes, fluxos |
|
|
342
|
+
| morph-plan | Invocada pela morph-proposal — produz spec/mandate/tasks.json |
|
|
343
|
+
| morph-implement | Invocada pela morph-apply — executa cada task |
|
|
344
|
+
| morph-eval | Após concluir uma task ou fase |
|
|
345
|
+
| morph-review | Invocada pela morph-apply — produz review-report + Gate 3 |
|
|
346
|
+
| morph-hotfix | **Orquestrador**: ADW cirúrgico de hotfix (scout → fix → gate humano → ship) |
|
|
347
|
+
| morph-standards | Quando precisar localizar ou aplicar um standard específico |
|
|
348
|
+
|
|
349
|
+
Slash commands (`/morph-proposal`, `/morph-apply`, `/morph-hotfix` — linhas acima) são atalhos que invocam a `Skill()` correspondente; o agente pode chamá-la direto quando o usuário pede "use /morph-proposal".
|
|
350
|
+
|
|
351
|
+
Cada skill contém instruções passo a passo — leia, siga, não desvie sem justificativa no recap.md. Só `framework/skills/core/` é instalado. Nenhuma skill é invocada proativamente por hook ou script — você decide quando a situação pede o rigor dela.
|
|
354
352
|
|
|
355
353
|
---
|
|
356
354
|
|
package/framework/agents.json
CHANGED
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"design", "ui", "ux", "design system", "tokens", "wcag",
|
|
103
103
|
"accessibility", "mockup", "flow", "shadcn", "tailwind"
|
|
104
104
|
],
|
|
105
|
-
"stack": ["
|
|
105
|
+
"stack": ["*"],
|
|
106
106
|
"standards": [
|
|
107
107
|
"frontend-design-system-naming",
|
|
108
108
|
"frontend-design-system-animations",
|
package/framework/docs/CLI.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
## Gates e estado
|
|
8
8
|
|
|
9
|
-
- **`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). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
|
|
9
|
+
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **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). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
|
|
10
10
|
- **`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).
|
|
11
11
|
- **`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.
|
|
12
|
-
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato); sem o token, cai no proxy (média das `taskScores`); `--signals` vence ambos. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate.
|
|
12
|
+
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato); sem o token, cai no proxy (média das `taskScores`); `--signals` vence ambos. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
13
13
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — 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.
|
|
14
14
|
|
|
15
15
|
## Features
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
## Observabilidade
|
|
39
39
|
|
|
40
40
|
- **`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.
|
|
41
|
-
- **`morph-spec cost [feature] [--by task|phase] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não
|
|
41
|
+
- **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não eram observáveis por hook até o evento `SessionUsage` — quando presente no stream, `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens` são somados e exibidos à parte como "tokens reais", nunca misturados com a proxy de bytes). `--all-trees` agrega `.morph/logs/events.jsonl` de **toda árvore git** que tiver um (não só a corrente) — reusa a mesma enumeração de worktree do `doctor --worktrees`/`fleet`; mostra um resumo por árvore + o total combinado (`--by` aplica ao agregado combinado). Use antes/depois de mudar skill ou fluxo para medir o efeito.
|
|
@@ -102,8 +102,29 @@ export async function run(payload) {
|
|
|
102
102
|
// advances once that file exists) — the deadlock this hook used to create.
|
|
103
103
|
const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
|
|
104
104
|
if (entryGate && isGateApproved(featureName, entryGate, root)) {
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
// uiux is OPTIONAL — proposal approval alone is enough to enter
|
|
106
|
+
// 2-plan/ for a feature that never took the design branch (no
|
|
107
|
+
// 1-design/ dir), exactly as before this check. But once a feature
|
|
108
|
+
// HAS opted into uiux (1-design/ exists), that gate is not
|
|
109
|
+
// decorative: 2-plan/ must wait for it too, or a feature can skip
|
|
110
|
+
// straight from an approved proposal into planning with its design
|
|
111
|
+
// gate silently left pending.
|
|
112
|
+
const tookDesignBranch = targetDir === '2-plan'
|
|
113
|
+
&& existsSync(resolveTargetPath(`.morph/features/${featureName}/1-design`, root));
|
|
114
|
+
|
|
115
|
+
if (!tookDesignBranch || isGateApproved(featureName, 'uiux', root)) {
|
|
116
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
|
|
121
|
+
return blockResult(
|
|
122
|
+
`MORPH-SPEC: Writing to '2-plan/' is not allowed yet.\n` +
|
|
123
|
+
`This feature entered the uiux branch (1-design/ exists) and the 'uiux' gate is still pending.\n\n` +
|
|
124
|
+
`Approve the design gate first, then re-enter plan:\n` +
|
|
125
|
+
` morph-spec approve ${featureName} uiux\n` +
|
|
126
|
+
` morph-spec advance ${featureName}`
|
|
127
|
+
);
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
const phaseLabel = phase.toUpperCase();
|
|
@@ -94,6 +94,11 @@ function isApprovalAnswer(answer) {
|
|
|
94
94
|
|
|
95
95
|
function gateFromQuestion(question) {
|
|
96
96
|
if (typeof question !== 'string') return null;
|
|
97
|
+
// Checked BEFORE the generic patterns below: the pinned uiux template is
|
|
98
|
+
// "Gate uiux — Design de {feature} aprovado?" (morph-uiux/SKILL.md) — it
|
|
99
|
+
// must never fall through to 'plan' via a stray "plan"/"plano" substring in
|
|
100
|
+
// the feature name. `/gate\s+uiux/i` alone is unambiguous either way.
|
|
101
|
+
if (/gate\s+uiux/i.test(question)) return 'uiux';
|
|
97
102
|
const lower = question.toLowerCase();
|
|
98
103
|
if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
|
|
99
104
|
if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
|
|
@@ -41,6 +41,32 @@ function normalize(p) {
|
|
|
41
41
|
return (p || '').replace(/\\/g, '/');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// events.jsonl grows forever across a project's lifetime (R2 rotation caps
|
|
45
|
+
// the FILE, not any single line) — a multi-KB sub-agent prompt or a long
|
|
46
|
+
// Bash command written verbatim on every dispatch/call is the dominant per-
|
|
47
|
+
// line cost. Capped AT THE WRITE SITE (not in the shared logger, so every
|
|
48
|
+
// OTHER event type stays exactly as-is) to `MAX_FIELD_CHARS`, with the
|
|
49
|
+
// ORIGINAL length preserved in a sibling `<field>Chars` key — `morph-spec
|
|
50
|
+
// cost` sums prompt chars for its dispatch-cost view, and must prefer that
|
|
51
|
+
// field over `prompt.length` once it's present (a truncated string's own
|
|
52
|
+
// length would silently undercount every dispatch over the cap). This shape
|
|
53
|
+
// is a CONTRACT with the telemetry reader — do not rename either half.
|
|
54
|
+
const MAX_FIELD_CHARS = 2000;
|
|
55
|
+
const TRUNCATE_SUFFIX = '…[truncado]';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cap a free-text field (prompt/command) at MAX_FIELD_CHARS, always
|
|
59
|
+
* reporting the field's true original length alongside it.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} value
|
|
62
|
+
* @returns {{ value: string, chars: number }}
|
|
63
|
+
*/
|
|
64
|
+
function capField(value) {
|
|
65
|
+
const str = typeof value === 'string' ? value : '';
|
|
66
|
+
if (str.length <= MAX_FIELD_CHARS) return { value: str, chars: str.length };
|
|
67
|
+
return { value: str.slice(0, MAX_FIELD_CHARS) + TRUNCATE_SUFFIX, chars: str.length };
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
/**
|
|
45
71
|
* Classify a Read target into the telemetry `kind` taxonomy — a contract with
|
|
46
72
|
* the observability side (src/lib/telemetry/telemetry-reader.js), so these
|
|
@@ -144,7 +170,8 @@ export async function run(payload) {
|
|
|
144
170
|
|
|
145
171
|
// UserPromptSubmit carries the user's prompt and no tool_name.
|
|
146
172
|
if (payload.hook_event_name === 'UserPromptSubmit' || (!payload.tool_name && typeof payload.prompt === 'string')) {
|
|
147
|
-
|
|
173
|
+
const userPrompt = capField(payload.prompt || '');
|
|
174
|
+
event = { event: 'UserPromptSubmit', tool: 'UserPrompt', prompt: userPrompt.value, promptChars: userPrompt.chars };
|
|
148
175
|
} else {
|
|
149
176
|
const tool = payload.tool_name;
|
|
150
177
|
if (!tool) return null;
|
|
@@ -156,7 +183,9 @@ export async function run(payload) {
|
|
|
156
183
|
} else if (tool === 'Agent' || tool === 'Task') {
|
|
157
184
|
event.persona = input.subagent_type || '';
|
|
158
185
|
if (input.description) event.desc = input.description;
|
|
159
|
-
|
|
186
|
+
const dispatchPrompt = capField(input.prompt || '');
|
|
187
|
+
event.prompt = dispatchPrompt.value;
|
|
188
|
+
event.promptChars = dispatchPrompt.chars;
|
|
160
189
|
} else if (tool === 'Read') {
|
|
161
190
|
event.target = targetFilePath || normalize(input.file_path || input.path);
|
|
162
191
|
event.kind = classifyReadKind(event.target);
|
|
@@ -169,7 +198,9 @@ export async function run(payload) {
|
|
|
169
198
|
// omit bytes
|
|
170
199
|
}
|
|
171
200
|
} else if (tool === 'Bash') {
|
|
172
|
-
|
|
201
|
+
const bashCommand = capField(input.command || '');
|
|
202
|
+
event.command = bashCommand.value;
|
|
203
|
+
event.commandChars = bashCommand.chars;
|
|
173
204
|
} else if (tool === 'Write' || tool === 'Edit') {
|
|
174
205
|
event.target = normalize(input.file_path || input.path);
|
|
175
206
|
} else {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Fail-open: exits 0 on any error.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, statSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { createHash } from 'crypto';
|
|
27
27
|
import { runBuildCheck } from '../../shared/build-check.js';
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '../../shared/state-reader.js';
|
|
31
31
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
32
32
|
import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
|
|
33
|
+
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Audit recap.md against tasks.json + standard-citation rule.
|
|
@@ -121,6 +122,80 @@ function auditRecapAndTasks(featurePath) {
|
|
|
121
122
|
return warnings;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// Safety valve for a pathological outlier transcript — NOT needed for the
|
|
126
|
+
// normal case: a real 45 MB / ~16k-line transcript (inspected directly while
|
|
127
|
+
// building this) parses+sums in ~350ms locally, well inside a Stop hook's
|
|
128
|
+
// budget. Above this, skip rather than risk an unbounded read.
|
|
129
|
+
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sum real token usage from a Claude Code session transcript (JSONL, the
|
|
133
|
+
* `transcript_path` the Stop payload carries).
|
|
134
|
+
*
|
|
135
|
+
* Each assistant TURN is written to the transcript as MULTIPLE JSONL lines —
|
|
136
|
+
* one per content block (thinking/text/tool_use) — every one of them
|
|
137
|
+
* repeating the exact SAME turn-level `message.usage` object under the same
|
|
138
|
+
* `message.id`. Verified against a real transcript: 5982 assistant lines but
|
|
139
|
+
* only 3315 unique message ids, with byte-identical usage across duplicates
|
|
140
|
+
* of the same id — summing every line naively double/triple-counts (measured
|
|
141
|
+
* ~2x on output_tokens on that file). This is why a per-line sum is wrong and
|
|
142
|
+
* a dedup-by-id sum is required; there is no running/cumulative total field
|
|
143
|
+
* to lean on instead — each turn's `usage` is that turn's own cost.
|
|
144
|
+
*
|
|
145
|
+
* Tolerant to shape: any missing usage field reads as 0. A corrupt/truncated
|
|
146
|
+
* line is skipped, not fatal — same discipline as telemetry-logger's reader.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} [transcriptPath]
|
|
149
|
+
* @returns {{inputTokens:number,outputTokens:number,cacheReadTokens:number,cacheCreationTokens:number}|null}
|
|
150
|
+
* null when there is nothing to report (no path, missing/oversized/
|
|
151
|
+
* unreadable file, or zero assistant messages carrying usage).
|
|
152
|
+
*/
|
|
153
|
+
function computeSessionUsage(transcriptPath) {
|
|
154
|
+
if (!transcriptPath || typeof transcriptPath !== 'string') return null;
|
|
155
|
+
try {
|
|
156
|
+
if (!existsSync(transcriptPath)) return null;
|
|
157
|
+
if (statSync(transcriptPath).size > MAX_TRANSCRIPT_BYTES) return null;
|
|
158
|
+
|
|
159
|
+
const raw = readFileSync(transcriptPath, 'utf-8');
|
|
160
|
+
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheCreationTokens = 0;
|
|
161
|
+
const seenMessageIds = new Set();
|
|
162
|
+
let sawAny = false;
|
|
163
|
+
|
|
164
|
+
for (const line of raw.split('\n')) {
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (!trimmed) continue;
|
|
167
|
+
let entry;
|
|
168
|
+
try {
|
|
169
|
+
entry = JSON.parse(trimmed);
|
|
170
|
+
} catch {
|
|
171
|
+
continue; // one corrupt/truncated line must not lose the whole transcript
|
|
172
|
+
}
|
|
173
|
+
if (entry?.type !== 'assistant') continue;
|
|
174
|
+
const usage = entry?.message?.usage;
|
|
175
|
+
if (!usage || typeof usage !== 'object') continue;
|
|
176
|
+
|
|
177
|
+
// Dedup by message.id (fallback to the line's own uuid when a message
|
|
178
|
+
// carries none) — first occurrence wins, duplicates carry identical
|
|
179
|
+
// values so which one wins never matters.
|
|
180
|
+
const dedupKey = entry?.message?.id || entry?.uuid;
|
|
181
|
+
if (dedupKey) {
|
|
182
|
+
if (seenMessageIds.has(dedupKey)) continue;
|
|
183
|
+
seenMessageIds.add(dedupKey);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
sawAny = true;
|
|
187
|
+
inputTokens += Number(usage.input_tokens) || 0;
|
|
188
|
+
outputTokens += Number(usage.output_tokens) || 0;
|
|
189
|
+
cacheReadTokens += Number(usage.cache_read_input_tokens) || 0;
|
|
190
|
+
cacheCreationTokens += Number(usage.cache_creation_input_tokens) || 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return sawAny ? { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } : null;
|
|
194
|
+
} catch {
|
|
195
|
+
return null; // fail-open
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
124
199
|
// Phases where compaction is strongly recommended before starting
|
|
125
200
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
126
201
|
|
|
@@ -128,12 +203,15 @@ const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
|
128
203
|
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
129
204
|
|
|
130
205
|
/**
|
|
131
|
-
* @param {object}
|
|
132
|
-
* reads its own state from disk
|
|
206
|
+
* @param {object} payload - parsed Stop stdin payload. Most of this hook's
|
|
207
|
+
* logic reads its own state from disk rather than the payload, EXCEPT the
|
|
208
|
+
* session-usage capture below, which needs `payload.transcript_path` (and
|
|
209
|
+
* `payload.session_id` for the throttle key) — the one thing only the
|
|
210
|
+
* payload carries.
|
|
133
211
|
* @returns {Promise<object|null>} a hookSpecificOutput advisory (phase
|
|
134
212
|
* transition / build failure / recap drift), or null (pass/silent).
|
|
135
213
|
*/
|
|
136
|
-
export async function run(
|
|
214
|
+
export async function run(payload) {
|
|
137
215
|
try {
|
|
138
216
|
// Prevent infinite loop
|
|
139
217
|
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
|
|
@@ -264,6 +342,40 @@ export async function run(_payload) {
|
|
|
264
342
|
}
|
|
265
343
|
// ────────────────────────────────────────────────────────────────────────
|
|
266
344
|
|
|
345
|
+
// ── Session usage (real tokens, from the transcript) ──────────────────────
|
|
346
|
+
// Silent side effect — never contributes to `warnings`/additionalContext,
|
|
347
|
+
// same as telemetry-log.js's capture. Throttled: only re-emitted when the
|
|
348
|
+
// computed totals actually changed since the last SessionUsage recorded
|
|
349
|
+
// for this session_id, so an unchanged transcript across consecutive
|
|
350
|
+
// Stops (no new assistant turn) does not re-append an identical event.
|
|
351
|
+
try {
|
|
352
|
+
const usage = computeSessionUsage(payload?.transcript_path);
|
|
353
|
+
if (usage) {
|
|
354
|
+
const usagePath = join(memDir, 'session-usage.json');
|
|
355
|
+
let stored = {};
|
|
356
|
+
try {
|
|
357
|
+
if (existsSync(usagePath)) stored = JSON.parse(readFileSync(usagePath, 'utf-8'));
|
|
358
|
+
} catch { /* corrupt file — start fresh */ }
|
|
359
|
+
|
|
360
|
+
const sessionKey = payload?.session_id || 'default';
|
|
361
|
+
const last = stored[sessionKey];
|
|
362
|
+
const unchanged = last
|
|
363
|
+
&& last.inputTokens === usage.inputTokens
|
|
364
|
+
&& last.outputTokens === usage.outputTokens
|
|
365
|
+
&& last.cacheReadTokens === usage.cacheReadTokens
|
|
366
|
+
&& last.cacheCreationTokens === usage.cacheCreationTokens;
|
|
367
|
+
|
|
368
|
+
if (!unchanged) {
|
|
369
|
+
appendTelemetryEvent({ event: 'SessionUsage', ...usage }, cwd);
|
|
370
|
+
stored[sessionKey] = usage;
|
|
371
|
+
try { writeFileSync(usagePath, JSON.stringify(stored, null, 2), 'utf-8'); } catch { /* non-blocking */ }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
// Fail-open — usage capture must never block Stop
|
|
376
|
+
}
|
|
377
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
267
379
|
if (warnings.length === 0) return null;
|
|
268
380
|
|
|
269
381
|
const message = [
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
* Also the real call site for activity-log rotation (R1.2 of the performance
|
|
28
28
|
* plan): SessionStart is the only reliable "between sessions" checkpoint the
|
|
29
29
|
* harness gets, so this is where `.morph/logs/activity.jsonl` gets archived
|
|
30
|
-
* to `.morph/logs/sessions/` once it crosses the size threshold.
|
|
30
|
+
* to `.morph/logs/sessions/` once it crosses the size threshold. The same
|
|
31
|
+
* checkpoint now also rotates `.morph/logs/events.jsonl` (telemetry-logger.js)
|
|
32
|
+
* once IT crosses its own threshold — events.jsonl grew unbounded even after
|
|
33
|
+
* R1.2, since that rotation only ever covered activity.jsonl.
|
|
31
34
|
*
|
|
32
35
|
* Fail-open: any error, or nothing to report, → pass() silently. This hook
|
|
33
36
|
* never blocks a session from starting.
|
|
@@ -46,6 +49,7 @@ import {
|
|
|
46
49
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
47
50
|
import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
|
|
48
51
|
import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
|
|
52
|
+
import { rotateEventsLogIfLarge } from '../../shared/telemetry-logger.js';
|
|
49
53
|
|
|
50
54
|
function projectDir() {
|
|
51
55
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
@@ -223,10 +227,11 @@ export async function run(payload) {
|
|
|
223
227
|
return null;
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
// Rotate the activity
|
|
227
|
-
// Best-effort, never fatal to session start —
|
|
228
|
-
//
|
|
230
|
+
// Rotate the activity + events logs before anything else touches them
|
|
231
|
+
// this session. Best-effort, never fatal to session start — both rotate*
|
|
232
|
+
// helpers are themselves fail-open (return false on any error).
|
|
229
233
|
try { rotateActivityLogIfLarge(dir); } catch { /* non-blocking */ }
|
|
234
|
+
try { rotateEventsLogIfLarge(dir); } catch { /* non-blocking */ }
|
|
230
235
|
|
|
231
236
|
if (isSecondaryWorktree(dir)) {
|
|
232
237
|
const feature = featureFromBranch(dir) || featureFromSingleDir(dir);
|