@polymorphism-tech/morph-spec 10.5.4 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +9 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/docs/CLI.md +7 -2
- package/framework/hooks/shared/phase-utils.js +0 -2
- package/framework/rules/plan-grammar.md +6 -2
- package/framework/schemas/config.schema.json +10 -0
- package/framework/schemas/feature.schema.json +12 -2
- 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 +2 -2
- package/framework/skills/core/morph-apply/references/review.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +5 -4
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/docs/plan.md +3 -3
- package/package.json +1 -1
- package/src/commands/state/finish.js +99 -11
- package/src/commands/state/recap.js +6 -5
- package/src/commands/validation/plan-check.js +4 -7
- package/src/commands/validation/verify.js +13 -4
- package/src/core/paths/output-schema.js +3 -1
- package/src/lib/ci/ci-verify.js +16 -0
- package/src/lib/ci/pr-checks.js +76 -0
- package/src/lib/plan/checks.js +6 -6
- package/src/lib/plan/plan-parser.js +3 -0
- package/src/lib/recap/recap.js +6 -5
- package/src/lib/validators/verify-runner.js +6 -3
- package/src/lib/view/feature-html.js +307 -0
- package/src/lib/view/feature-page.client.js +116 -0
- package/src/lib/view/feature-page.css +169 -0
- package/src/lib/view/feature-view.js +104 -0
- package/src/lib/view/flow-svg.js +63 -0
- package/src/lib/view/tokens.css +18 -0
- package/src/lib/plan/plan-html.js +0 -125
package/CLAUDE.md
CHANGED
|
@@ -21,6 +21,7 @@ cd src/dashboard && npm install
|
|
|
21
21
|
npm run test:changed # add -- --list to see the selection and why
|
|
22
22
|
|
|
23
23
|
# Run all tests (files in parallel, cpus-1 at a time; ~6 min on a 16-core Windows box, bound by process/git contention)
|
|
24
|
+
# Not a step before a PR or release: the PR runs test:changed and the tag runs the whole wall before publishing
|
|
24
25
|
npm test
|
|
25
26
|
|
|
26
27
|
# A subtree or file (still parallel); --test-concurrency=1 to debug serially
|
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:** 10.
|
|
6
|
+
**Version:** 10.7.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
*morph-spec v10.
|
|
379
|
+
*morph-spec v10.7.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -580,4 +580,13 @@ program
|
|
|
580
580
|
.option('--json', 'Output the aggregate as JSON')
|
|
581
581
|
.action((feature, options) => costCommand(feature, options));
|
|
582
582
|
|
|
583
|
+
// The page follows every command that moves the feature; 'exit' also covers their process.exit().
|
|
584
|
+
const VIEW_COMMANDS = new Set(['plan-check', 'gate-check', 'gate-decision', 'approve', 'unapprove', 'advance', 'verify', 'report-check']);
|
|
585
|
+
program.hook('preAction', async (_, action) => {
|
|
586
|
+
const [feature] = action.args;
|
|
587
|
+
if (!VIEW_COMMANDS.has(action.name()) || !/^[a-z0-9][\w-]*$/i.test(feature ?? '')) return;
|
|
588
|
+
const { refreshFeatureView } = await import('../src/lib/view/feature-view.js');
|
|
589
|
+
process.on('exit', () => refreshFeatureView(process.cwd(), feature));
|
|
590
|
+
});
|
|
591
|
+
|
|
583
592
|
await program.parseAsync();
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -127,6 +127,8 @@ Feature sem `workType` (pré-v8) é tratada como `feature`.
|
|
|
127
127
|
|
|
128
128
|
**`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum; a sessão escolhe por `finish.mode` do `config.json`, padrão `pr`): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
|
|
129
129
|
|
|
130
|
+
**Stamp com `pendingPr`** (`verify --ci` sob `project.prChecks`): o `--pr` espera os checks declarados no commit do archive, antes do desmonte, porque é esse o run que o merge reaproveita; provar antes do archive pagaria o CI duas vezes. Rode em `run_in_background`. `pass` → `checks: { status: "pass" }`. Check vermelho → `error: "pr-checks-fail"`: desfaz o archive com `git revert`, reabre o Gate 3, carimba `status: "fail"`, `source: "pr"`, devolve a PR a rascunho (o revert fica local, e rascunho não mergeia) e sai 1. Check que morreu em menos de 30 s (cota do Actions), cancelado, ou nenhum check no commit em 90 s → `error: "pr-checks-no-verdict"`: desfaz igual, apaga o stamp, e a saída é a completa local, `verify {f}` sem `--ci`. O próximo `finish` reaproveita a PR aberta. Sem resposta em 50 min: `finished: true`, `checks: { status: "pending" }`, sem desfazer nada. Processo morto na espera: o mesmo `finish --pr` retoma. `--merge` recusa esse stamp (`pr-proof-pending`).
|
|
131
|
+
|
|
130
132
|
**Índice de ADRs:** no mesmo commit do archive, o `finish` lê o `2-plan/plan.md` da feature: os ADRs de `## Decisões` com `**Alcance:** projeto` entram em `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo), e a tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` retira (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os ADRs revogados — forma em `morph-spec plan-check --help`; o `plan-check` avisa, enquanto o plano ainda muda, a linha que o `finish` não aplicaria. O payload traz `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`.
|
|
131
133
|
|
|
132
134
|
`--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). Saída de teste não rastreada (`test-results/`, `TestResults/`, `playwright-report/`, `blob-report/`) não conta como árvore suja: na raiz só é tolerada, na worktree sai junto com o desmonte; qualquer outro arquivo não commitado recusa. Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; derruba a stack e2e com `down --remove-orphans --rmi local -v` (`--keep-volumes` tira o `-v`) e os `e2e.hostServices` — `--rmi local` leva só as imagens que o compose construiu, nunca a puxada (`postgres`), que a próxima feature baixaria de novo; `docker builder prune` nunca roda (o cache é global da máquina). No `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`.
|
|
@@ -147,7 +149,7 @@ Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed:
|
|
|
147
149
|
|
|
148
150
|
### archive
|
|
149
151
|
|
|
150
|
-
**`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. O arquivo guarda só `feature.json`, `2-plan/plan.md` e o recap (`
|
|
152
|
+
**`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. O arquivo guarda só `feature.json`, `2-plan/plan.md` e a página da feature com o recap dentro (`feature.html` + `recap.json`, gerados antes da poda, enquanto os relatórios do Gate 3 ainda existem): o plano sobrevive até a um squash merge, e `feature.json.archivedFrom.sha` é o topo da branch antes da poda (`git show <sha>:.morph/features/<f>/3-implement/report.md`; num squash, o `(#N)` do commit leva ao PR e a `git fetch origin pull/N/head`).
|
|
151
153
|
|
|
152
154
|
### recap
|
|
153
155
|
|
|
@@ -347,6 +349,8 @@ Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rod
|
|
|
347
349
|
|
|
348
350
|
**Por quê.** Só o **artefato** vira prova, nunca o "verde" do run: workflow de lint, deploy ou de terceiro não compra o Gate 3.
|
|
349
351
|
|
|
352
|
+
**Prova na PR (`project.prChecks`):** projeto cujo CI de PR já roda a suíte declara `"prChecks": { "workflows": ["Backend", "Frontend"], "covers": ["dotnet", "vitest"] }`, e o `--ci` não cria `morph-verify.yml` nem paga a suíte duas vezes: roda local só o que a PR não prova (build, validadores, e2e aplicável, nós fora de `covers`) e carimba com `pendingPr`. Quem espera é o `finish --pr` (`### finish`). `workflows` é o `name:` do workflow; workflow fora da lista não prova nada. `covers` vale pelo que o workflow roda de fato: Vitest só com `--changed` na PR é cobertura menor aceita pelo projeto. Roda a completa local, com a razão, o que a rodada afetada subiria (migration, contrato, arquivo que alcança o projeto inteiro, código compartilhado) e o que não alcança o CI (`ciAvailability`). Ignorado com `project.ci: false` ou `finish.mode: "merge"`.
|
|
353
|
+
|
|
350
354
|
### Validador code-budget
|
|
351
355
|
|
|
352
356
|
**Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`).
|
|
@@ -526,7 +530,7 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
526
530
|
|
|
527
531
|
### plan-check
|
|
528
532
|
|
|
529
|
-
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `
|
|
533
|
+
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e abre a vida de `feature.html`, a página única da feature (um arquivo só, na raiz dela): o que precisa de decisão, cenários, decisões ligadas aos cenários que citam, gráficos de prova, risco e reuso e, com `## Fluxo` e `## Estados` (opcionais, `- origem → destino: gatilho` por linha), os diagramas. Todo `plan-check`, `gate-check`, `gate-decision`, `approve`, `unapprove`, `advance`, `verify` e `report-check` reescreve o mesmo arquivo a partir do disco ao sair, sem custo de token; o `archive` a congela com o recap dentro. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; `risco:` com `prova: leitura`; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`, ADR sem `**Alcance:** feature|projeto`, `## Fatias` que sobrou da 9.x (não é lida). Com o gate `plan` aprovado e o corpo mudado fora da `## Errata`, recusa (`frozen`, exit 1) — o `planHash` ignora a errata; mudança de escopo é `unapprove <f> plan`, `plan-check` e o gate de novo. Plano extraído antes das regras de `risco:` e de mídia não passa a falhar por elas. Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a. Com o plano válido, acrescenta ao `config.json` a stack que os arquivos mostram e instala em `.claude/rules/` a regra de stack que falta (uma linha por regra, em stderr; `rulesAdded` no `--json`).
|
|
530
534
|
|
|
531
535
|
### stack-set
|
|
532
536
|
|
|
@@ -746,6 +750,7 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
|
|
|
746
750
|
| Sessão que implementa | à vontade — filtro ou suíte —, pela fila `heavy` (`heavy.gateBash` ligado por padrão) | Implementar e conferir é dela; a prova do gate continua sendo o `verify` sem filtro |
|
|
747
751
|
| Gate 3 | `verify {f} --ci --json` (sem filtro; sem CI disponível, roda local) | A prova tem de cobrir **todos** os nós; no CI ela não ocupa a máquina e roda em paralelo com o avaliador |
|
|
748
752
|
| CI | a suíte inteira, todos os nós (`morph-verify.yml` roda o mesmo `verify {f}`) | idem — e o artefato dele é o stamp do Gate 3 |
|
|
753
|
+
| PR, com `project.prChecks` | os workflows do projeto, esperados pelo `finish --pr` | a suíte que a PR já paga não roda de novo na máquina nem no `morph-verify.yml` |
|
|
749
754
|
| Evidência de eval (cenário `Cn` com `prova: eval`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals`, sem stack e sem carimbo — é a evidência de runtime do cenário, não a verificação da feature |
|
|
750
755
|
|
|
751
756
|
> **Nenhum relógio do `verify` é de 600 s.** Os 600 s que existem são a espera da fila
|
|
@@ -13,7 +13,6 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
13
13
|
plan: 'plan',
|
|
14
14
|
checks: 'plan',
|
|
15
15
|
planCritique: 'plan',
|
|
16
|
-
planView: 'plan',
|
|
17
16
|
uiMockups: 'uiux',
|
|
18
17
|
implementReport: 'implement',
|
|
19
18
|
evidencias: 'implement',
|
|
@@ -26,7 +25,6 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
26
25
|
'plan.md': 'plan',
|
|
27
26
|
'checks.json': 'checks',
|
|
28
27
|
'critica.md': 'planCritique',
|
|
29
|
-
'plan.html': 'planView',
|
|
30
28
|
'mockups.md': 'uiMockups',
|
|
31
29
|
'report.md': 'implementReport',
|
|
32
30
|
'verifier.md': 'verifierReport',
|
|
@@ -41,7 +41,11 @@ Todo valor de baixo risco que você decidiu sem perguntar (limite, intervalo, re
|
|
|
41
41
|
|
|
42
42
|
## Estados (opcional)
|
|
43
43
|
|
|
44
|
-
Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatura): uma transição por linha, `- origem → destino: gatilho` (`->` também vale).
|
|
44
|
+
Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatura): uma transição por linha, `- origem → destino: gatilho` (`->` também vale). A página da feature desenha a máquina; linha que não é transição, ou seção vazia, é erro. Sem estado, não escreva a seção.
|
|
45
|
+
|
|
46
|
+
## Fluxo (opcional)
|
|
47
|
+
|
|
48
|
+
O caminho de um pedido ou dado pelo sistema, na mesma forma e com as mesmas recusas de `## Estados`: `- origem → destino: o que passa`. A página desenha o diagrama.
|
|
45
49
|
|
|
46
50
|
## Reuso
|
|
47
51
|
|
|
@@ -49,7 +53,7 @@ Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatur
|
|
|
49
53
|
|
|
50
54
|
## Decisões
|
|
51
55
|
|
|
52
|
-
`### ADR-001 — título` por decisão não trivial (alternativa descartada, motivo, quem decidiu) com `**Alcance:** feature` ou `projeto
|
|
56
|
+
`### ADR-001 — título` por decisão não trivial (alternativa descartada, motivo, quem decidiu) com `**Alcance:** feature` ou `projeto`, citando os `Cn` que sustenta: a citação vira link na página da feature. Greenfield: decisão que fixa stack, layout, infraestrutura ou test runner é `projeto`. `### Conflitos com o acervo` sempre (`nenhum` vale). Proibição além das do standard `architecture-stack-baseline` é ADR.
|
|
53
57
|
|
|
54
58
|
## O que o plan-check recusa
|
|
55
59
|
|
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
"type": "boolean",
|
|
26
26
|
"description": "false turns `verify {f} --ci` into the local verify (with the reason printed). Absent or true: the Gate 3 proof comes from the GitHub Actions workflow that runs `morph-spec verify` (created on first use), whenever origin is GitHub and gh is authenticated."
|
|
27
27
|
},
|
|
28
|
+
"prChecks": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "The project's own PR workflows as the proof of suites they already run. With it, `verify {f} --ci` leaves the `covers` nodes out of the local run, and `finish --pr` waits for the `workflows` checks of the PR commit; red or unjudged, it undoes the archive and reopens Gate 3. Ignored with project.ci: false or finish.mode: merge, and by a change the affected run would escalate to the complete one.",
|
|
31
|
+
"required": ["workflows", "covers"],
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"workflows": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Workflow `name:` values whose checks count. Any other workflow proves nothing." },
|
|
35
|
+
"covers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "project.tests[].id of the nodes these workflows prove. A node left out (e2e, evals, what the PR does not run) still runs locally." }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
28
38
|
"tests": {
|
|
29
39
|
"type": "array",
|
|
30
40
|
"description": "One entry per test suite. Absent (not empty) means the four-step fallback of resolveTestPlan() decides — every project created before this key existed keeps working. An empty array is an explicit declaration of 'no test suites'.",
|
|
@@ -149,8 +149,18 @@
|
|
|
149
149
|
"e2eReason": { "type": ["string", "null"], "description": "The LITERAL reason the e2e node skipped (e2e-check.js's own wording), or null when it did not skip. risk-detector.js's classifyE2eSkip() reads it: 'project does not containerize' / 'does not touch runtime' are legitimate dispensations and pass; 'docker is not available' / 'requested' (--skip-e2e) / anything unclassifiable — null included — are an absence of proof and pause Gate 3. A skip recorded WITHOUT a reason is refused on purpose: a nameless dispensation is a silent no-op." },
|
|
150
150
|
"sha": { "type": ["string", "null"], "description": "HEAD commit SHA at verify time (git rev-parse HEAD); null when not in a git repo or no commits yet." },
|
|
151
151
|
"ts": { "type": "string", "format": "date-time", "description": "Server-side timestamp, same instant as the corresponding verifications entry's updatedAt." },
|
|
152
|
-
"source": { "type": "string", "enum": ["ci"], "description": "
|
|
153
|
-
"ciRun": { "type": "string", "description": "URL of the CI run whose artifact wrote this stamp (with source: ci). `sha` is that run's headSha, so verificationStale compares the code CI saw with HEAD." }
|
|
152
|
+
"source": { "type": "string", "enum": ["ci", "pr"], "description": "`ci`: the stamp came from `verify --ci`, the verify --json the project CI uploaded for the pushed commit, judged by the same rules as a local run. `pr`: `finish --pr` wrote it when a project.prChecks workflow came back red. Absent = local run." },
|
|
153
|
+
"ciRun": { "type": "string", "description": "URL of the CI run whose artifact wrote this stamp (with source: ci). `sha` is that run's headSha, so verificationStale compares the code CI saw with HEAD." },
|
|
154
|
+
"pendingPr": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"description": "Local run of `verify --ci` under project.prChecks: the `covers` nodes were left out and `finish --pr` waits for the `workflows` checks. Refused by `finish --merge`.",
|
|
157
|
+
"required": ["workflows", "covers"],
|
|
158
|
+
"additionalProperties": false,
|
|
159
|
+
"properties": {
|
|
160
|
+
"workflows": { "type": "array", "items": { "type": "string" } },
|
|
161
|
+
"covers": { "type": "array", "items": { "type": "string" } }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
154
164
|
},
|
|
155
165
|
"additionalProperties": false
|
|
156
166
|
},
|
|
@@ -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_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -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_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Implementa o plano aprovado de uma feature morph-spec inteiro nesta
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado ("/morph-apply {feature}", "implementar feature"), em "/morph-review {feature}" (entra no §5), em "/morph-hotfix {feature}" ou incidente de produção (references/hotfix.md), ou quando `morph-spec next` mandar implement, verify, fix, review ou finish
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -58,7 +58,7 @@ Verify verde: siga `references/review.md` — foto `--before-eval`, verificador
|
|
|
58
58
|
|
|
59
59
|
## 6. Fechar
|
|
60
60
|
|
|
61
|
-
`morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/
|
|
61
|
+
`morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. Stamp com `pendingPr`: `--pr` em `run_in_background`, porque ele espera os checks da PR; `pr-checks-fail` ou `pr-checks-no-verdict` desfaz o archive e reabre o Gate 3: siga o `hint`. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/feature.html#recap`, abra no navegador; `morph-spec recap {feature} --piper` narra com o Piper local).
|
|
62
62
|
|
|
63
63
|
## Arquivo de teste
|
|
64
64
|
|
|
@@ -49,7 +49,7 @@ Confirmação = **dispatch novo** só dos papéis em `gate3.redispatch`, com o `
|
|
|
49
49
|
|
|
50
50
|
## 6. Gate 3
|
|
51
51
|
|
|
52
|
-
**A rodada completa:** com os relatórios sem bloqueador, `morph-spec verify {feature} --ci --json --detach` em `run_in_background` (sem CI, roda local e diz por quê; repetir o comando retoma a rodada). É a única. Sem `--filter`, sempre, e sem `--affected`: verde filtrado é verde sobre código não testado, e é ela que o gate lê. `reason: "stamp-reused"` = nada andou desde o último verde: vale. Saída `CI rodando` → espere a notificação. `fail` → corrija (CI: `gh run view <id> --log-failed`) e siga o §5, porque a correção é código que ninguém revisou. Nó `e2e` em `skip` → o `gate-check` decide se dispensa (`classifyE2eSkip`, em `risk-detector.js`: docker indisponível pausa sozinho).
|
|
52
|
+
**A rodada completa:** com os relatórios sem bloqueador, `morph-spec verify {feature} --ci --json --detach` em `run_in_background` (sem CI, roda local e diz por quê; repetir o comando retoma a rodada). É a única. Sem `--filter`, sempre, e sem `--affected`: verde filtrado é verde sobre código não testado, e é ela que o gate lê. `reason: "stamp-reused"` = nada andou desde o último verde: vale. Saída `CI rodando` → espere a notificação. `fail` → corrija (CI: `gh run view <id> --log-failed`) e siga o §5, porque a correção é código que ninguém revisou. Nó `e2e` em `skip` → o `gate-check` decide se dispensa (`classifyE2eSkip`, em `risk-detector.js`: docker indisponível pausa sozinho). Com `project.prChecks`, o que a PR prova fica de fora e o `finish --pr` espera os checks (`SKILL.md` §6).
|
|
53
53
|
|
|
54
54
|
`morph-spec gate-check {feature} review [--json]` lê os relatórios, a cobertura dos `Cn` e os demais sinais. Forçar passagem apesar da pausa: `morph-spec gate-decision {feature} review --pause false --reason "…"`.
|
|
55
55
|
|
|
@@ -4,7 +4,7 @@ description: Leva um pedido ou o link de uma issue ao plano aprovado — roteia
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "/morph-plan <link da issue>", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -54,9 +54,10 @@ O `create` escreveu o esqueleto; a gramática completa chega pela rule `plan-gra
|
|
|
54
54
|
- **`## Cenários` é a definição de pronto.** Cada `Cn` é um comportamento observável que o verificador do Gate 3 vai **executar** contra a app — escreva-o como um fluxo de usuário ou uma regra que dá para disparar. `prova:` `teste` (com `risco:`) | `e2e` | `eval` | `manual` | `leitura`. Sem risco, `leitura`.
|
|
55
55
|
- **`degrau:`** quando o `Cn` não se desfaz com revert: `migrar` (toda migration, aditiva ou não: drop, rename, backfill, coluna nova) ou `irreversível` (apaga dado que alguém lê, quebra contrato externo). Sem a linha, `refazer`. Esses dois degraus pausam o gate do plano para o usuário, e o Gate 3 pausa em migration sem degrau, em arquivo de contrato (`openapi`, `.proto`) sem `irreversível`, e no bloqueador `irreversivel` do revisor.
|
|
56
56
|
- **`## Padrões assumidos`**: todo valor de baixo risco que você decidiu sem perguntar, numerado `1.`…`N.`, com o valor. É o que tira a pergunta trivial da entrevista: o usuário veta por número. Auth, credencial, permissão, pagamento e apagar dado nunca entram: são pergunta.
|
|
57
|
-
- **`## Estados`** (opcional): algo muda de estado (pedido, aprovação) → `- origem → destino: gatilho`, uma por linha;
|
|
57
|
+
- **`## Estados`** (opcional): algo muda de estado (pedido, aprovação) → `- origem → destino: gatilho`, uma por linha; a página da feature desenha a máquina.
|
|
58
|
+
- **`## Fluxo`** (opcional): o caminho de um pedido ou dado pelo sistema, na mesma forma; vira o desenho que o usuário olha em vez de ler `## Arquitetura`. Escreva-o sempre que houver mais de duas peças conversando.
|
|
58
59
|
- **`## Reuso`**: `REUSAR` | `ESTENDER` | `CRIAR`; `CRIAR` exige `why` (a consulta que respondeu "não existe").
|
|
59
|
-
- **`## Decisões`**: ADR por decisão não trivial, `**Alcance:** feature|projeto
|
|
60
|
+
- **`## Decisões`**: ADR por decisão não trivial, `**Alcance:** feature|projeto`, citando os `Cn` que sustenta ("Sustenta C1, C4"): a citação vira o link e a matriz decisão × cenário da página; `### Conflitos com o acervo` sempre. Serviço novo de dados ou plataforma (cache, fila, vetor, busca, grafo, banco de documento, arquivo, gateway de LLM): o ADR diz por que o Postgres e o Neon do projeto não cobrem (`morph-spec standards --show architecture-stack-baseline#postgres-primeiro`).
|
|
60
61
|
- **`## Arquitetura`**: onde entra, o que conversa com o quê, edge cases técnicos. Gera imagem ou vídeo por IA → cite aqui o standard (`ai-agents-modalities-image-gen`, `ai-agents-media-video`, `ai-agents-media-pipeline`): é a citação que arma as provas de mídia do Gate 3, e o `plan-check` recusa o plano que fala em mídia gerada sem ela (falso positivo: a linha `Mídia gerada: não`).
|
|
61
62
|
|
|
62
63
|
**Contrato de implementação**: o que quem implementa precisa e o código não diz.
|
|
@@ -77,7 +78,7 @@ Antes do gate, em `feature` e `bug`: a crítica em contexto fresco de `reference
|
|
|
77
78
|
`morph-spec gate-check {feature} plan [--json]`. Entrevista que zerou a árvore já é a aprovação do usuário: não repause, salvo a pausa de `signal: "rung"`, que vence e pergunta nomeando cada `Cn` e o seu degrau. Encerrada antes, com nós em `## Fora de escopo`: `morph-spec gate-decision {feature} plan --pause true --reason "entrevista encerrada com nós abertos" --signals '{"discoveryMode":"interview"}'`, e a pergunta do gate lista esses nós.
|
|
78
79
|
|
|
79
80
|
- `resolver === "session"` (`plan-check` com erro, crítica ausente ou de outra versão) → corrija o plano ou despache a crítica, e rode `plan-check` e `gate-check` de novo, sem perguntar.
|
|
80
|
-
- `resolver === "human"` → abra `
|
|
81
|
+
- `resolver === "human"` → abra `feature.html`, na raiz da feature, no navegador do usuário (`start`/`open`/`xdg-open`; a CLI a mantém, nunca escreva HTML de plano à mão; o usuário pode colar a resposta que ela monta), o entendimento (problema, fora de escopo, cenários, riscos) e `AskUserQuestion`, header "Gate plano": `"Gate do plano — Plano de {feature} aprovado?"` com `"aprovado, prosseguir" | "preciso ajustar" | "cancelar feature"`; a pergunta lista `## Padrões assumidos` para veto por número; só a decisão sensível que você tomou sozinho vira pergunta antes, na forma de `references/interview.md` (`## Forma da pergunta`). Headless: `morph-spec approve {feature} plan --approver <quem>`.
|
|
81
82
|
- `pause === false` → uma linha e `morph-spec advance {feature}` (ou `--design`).
|
|
82
83
|
|
|
83
84
|
Aprovado: "Plano aprovado.", com `## Padrões assumidos` numerado, e siga direto com `Skill(morph-apply)`, sem esperar resposta. Veto, a qualquer momento: `## Errata` com "Padrão N vetado: <valor novo>"; a implementação segue a errata. Rodou `stack-set` nesta sessão: pare em "Plano aprovado. Rode `/morph-apply {feature}` numa sessão nova", porque as rules de domínio só carregam no início de uma sessão.
|
|
@@ -4,14 +4,14 @@ description: Conduz a fase de UI/UX de uma feature com plano aprovado — render
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design), antes de /morph-apply
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
|
11
11
|
|
|
12
12
|
Entra depois do gate do plano, quando a feature cria ou altera UI. Sai com `1-design/` aprovado; próximo passo `/morph-apply {feature}`. Você desenha nesta sessão — o que decidir aqui é o que vai implementar depois.
|
|
13
13
|
|
|
14
|
-
A régua de sistema e de estética chega sozinha: a rule `ui-design` carrega quando você abre um arquivo de `1-design/` ou de UI.
|
|
14
|
+
A régua de sistema e de estética chega sozinha: a rule `ui-design` carrega quando você abre um arquivo de `1-design/` ou de UI. Antes do primeiro HTML, carregue `frontend-design:frontend-design` e, para gráfico ou indicador, `dataviz`, se a sessão os tiver. As telas aparecem sozinhas na página da feature (`feature.html`), com link e print.
|
|
15
15
|
|
|
16
16
|
## 1. Insumos
|
|
17
17
|
|
|
@@ -24,8 +24,8 @@ Resultado buscado: <o estado depois: o que muda, para quem, como se percebe.>
|
|
|
24
24
|
|
|
25
25
|
1. <padrão e o valor escolhido>
|
|
26
26
|
|
|
27
|
-
<!--
|
|
28
|
-
"- origem → destino: gatilho".
|
|
27
|
+
<!-- Opcionais, viram desenho em feature.html: "Fluxo" (h2), o caminho pelo sistema, e "Estados" (h2), se algo muda de
|
|
28
|
+
estado. Uma transição por linha: "- origem → destino: gatilho". -->
|
|
29
29
|
|
|
30
30
|
## Cenários
|
|
31
31
|
|
|
@@ -43,7 +43,7 @@ Resultado buscado: <o estado depois: o que muda, para quem, como se percebe.>
|
|
|
43
43
|
### ADR-001 — <título>
|
|
44
44
|
**Alcance:** feature
|
|
45
45
|
|
|
46
|
-
<Contexto, decisão e consequência. `projeto` quando a decisão vale além desta feature.>
|
|
46
|
+
<Contexto, decisão e consequência, citando os Cn que ela sustenta. `projeto` quando a decisão vale além desta feature.>
|
|
47
47
|
|
|
48
48
|
### Conflitos com o acervo
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -24,6 +24,9 @@ import { runArchive } from './archive.js';
|
|
|
24
24
|
import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
25
25
|
import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
|
|
26
26
|
import { isTestArtifact } from '../../lib/worktree/ignored-triage.js';
|
|
27
|
+
import { waitForPrChecks } from '../../lib/ci/pr-checks.js';
|
|
28
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
29
|
+
import { recordGateEvent } from '../../../framework/hooks/shared/gate-event.js';
|
|
27
30
|
|
|
28
31
|
const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
|
|
29
32
|
|
|
@@ -253,7 +256,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
253
256
|
});
|
|
254
257
|
process.exit(1);
|
|
255
258
|
}
|
|
256
|
-
|
|
259
|
+
// A run killed while waiting left the PR open and the proof unread: this run picks the wait up.
|
|
260
|
+
if (openPr && !readFeatureJson(featureCwd, feature, 'archive')?.verification?.pendingPr) {
|
|
257
261
|
emit({
|
|
258
262
|
finished: false,
|
|
259
263
|
error: 'already-finished',
|
|
@@ -278,6 +282,16 @@ export async function finishCommand(feature, options = {}) {
|
|
|
278
282
|
emit({ finished: false, error: 'review-not-approved', feature });
|
|
279
283
|
process.exit(1);
|
|
280
284
|
}
|
|
285
|
+
const pendingPr = fj?.verification?.pendingPr ?? null;
|
|
286
|
+
if (mode === 'merge' && pendingPr && !options.force) {
|
|
287
|
+
emit({
|
|
288
|
+
finished: false,
|
|
289
|
+
error: 'pr-proof-pending',
|
|
290
|
+
feature,
|
|
291
|
+
hint: `${pendingPr.covers.join(', ')} só seriam provadas pelo CI da PR, e --merge não abre PR. Rode \`morph-spec verify ${feature}\` (sem --ci), \`gate-check ${feature} review\` e o finish de novo`,
|
|
292
|
+
});
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
281
295
|
const prTitle = buildPrTitle(feature, fj);
|
|
282
296
|
const prBody = (fj?.description && String(fj.description).trim())
|
|
283
297
|
? String(fj.description).trim()
|
|
@@ -364,7 +378,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
364
378
|
if (mode === 'merge') {
|
|
365
379
|
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
|
|
366
380
|
}
|
|
367
|
-
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
|
|
381
|
+
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp, pendingPr, featureTree: cleanCwd });
|
|
368
382
|
}
|
|
369
383
|
|
|
370
384
|
// A junction/symlink in the worktree points at content in the ROOT, and `rm -rf` or
|
|
@@ -600,7 +614,43 @@ export function prNumberFromUrl(url) {
|
|
|
600
614
|
return m ? Number(m[1]) : null;
|
|
601
615
|
}
|
|
602
616
|
|
|
603
|
-
|
|
617
|
+
// The checks of the archive commit are the proof: a second push of the same code would pay the CI twice,
|
|
618
|
+
// so a red or unjudged run undoes the archive here and the feature goes back to the session.
|
|
619
|
+
function reopenFeature({ tree, feature, check, ciRun, sha }) {
|
|
620
|
+
const archiveCommit = execFileSync('git', ['log', '-1', '--format=%H', '--fixed-strings', `--grep=chore(morph): archive ${feature}`], { cwd: tree, stdio: 'pipe' }).toString().trim();
|
|
621
|
+
execFileSync('git', ['revert', '--no-edit', archiveCommit], { cwd: tree, stdio: 'pipe' });
|
|
622
|
+
|
|
623
|
+
const path = join(tree, '.morph', 'features', feature, 'feature.json');
|
|
624
|
+
const fj = JSON.parse(readFileSync(path, 'utf8'));
|
|
625
|
+
const now = new Date().toISOString();
|
|
626
|
+
const reason = check.state === 'fail'
|
|
627
|
+
? `CI da PR vermelho: ${check.failed.map((c) => c.workflowName).join(', ')}`
|
|
628
|
+
: `CI da PR sem veredito: ${check.reason}`;
|
|
629
|
+
fj.approvalGates = { ...fj.approvalGates, review: { approved: false, timestamp: now, approvedBy: null, revokedBy: 'finish', revokedAt: now, reason } };
|
|
630
|
+
if (check.state === 'fail') {
|
|
631
|
+
const { pendingPr: _, ...stamp } = fj.verification;
|
|
632
|
+
fj.verification = { ...stamp, status: 'fail', nodes: { ...stamp.nodes, tests: 'fail' }, sha, ts: now, source: 'pr', ciRun };
|
|
633
|
+
const entry = fj.verifications?.feature;
|
|
634
|
+
if (entry) fj.verifications.feature = { ...entry, status: 'fail', failStreak: (entry.failStreak ?? 0) + 1, updatedAt: now };
|
|
635
|
+
} else {
|
|
636
|
+
delete fj.verification;
|
|
637
|
+
}
|
|
638
|
+
atomicWriteJson(path, fj);
|
|
639
|
+
recordGateEvent(tree, { feature, gate: 'review', step: 'unapprove', approvedBy: null });
|
|
640
|
+
commitFeatureState(tree, feature);
|
|
641
|
+
return reason;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The revert stays local (a push would pay another CI run), so the reopened PR still holds the archive:
|
|
645
|
+
// as a draft it cannot be merged by accident, on a plan without branch protection.
|
|
646
|
+
function setPrDraft(root, prUrl, draft) {
|
|
647
|
+
try {
|
|
648
|
+
const { bin, prefix } = resolveGh();
|
|
649
|
+
execFileSync(bin, [...prefix, 'pr', 'ready', prUrl, ...(draft ? ['--undo'] : [])], { cwd: root, stdio: 'pipe', timeout: 15000 });
|
|
650
|
+
} catch { /* gh refuses a PR already in that state */ }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp, pendingPr, featureTree }) {
|
|
604
654
|
try {
|
|
605
655
|
execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
|
|
606
656
|
} catch (err) {
|
|
@@ -608,13 +658,48 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
608
658
|
process.exit(1);
|
|
609
659
|
}
|
|
610
660
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
661
|
+
// A feature reopened by a red PR comes back here with that PR still open.
|
|
662
|
+
const openPr = openPrFor(expectedBranch, root);
|
|
663
|
+
let prUrl = typeof openPr === 'string' ? openPr : null;
|
|
664
|
+
if (prUrl) setPrDraft(root, prUrl, false);
|
|
665
|
+
if (!prUrl) {
|
|
666
|
+
try {
|
|
667
|
+
const { bin, prefix } = resolveGh();
|
|
668
|
+
prUrl = execFileSync(bin, [...prefix, 'pr', 'create', '--base', defaultBranch, '--head', expectedBranch, '--title', prTitle, '--body', prBody], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
669
|
+
} catch (err) {
|
|
670
|
+
emit({ finished: false, error: 'pr-create-failed', output: gitErr(err), hint: 'a branch foi enviada (push OK); abra o PR manualmente com `gh pr create`', feature, pushed: expectedBranch });
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let checks = null;
|
|
676
|
+
if (pendingPr) {
|
|
677
|
+
const sha = execFileSync('git', ['rev-parse', expectedBranch], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
678
|
+
const check = await waitForPrChecks(root, { branch: expectedBranch, sha, workflows: pendingPr.workflows });
|
|
679
|
+
if (check.state === 'fail' || check.state === 'no-verdict') {
|
|
680
|
+
let reason;
|
|
681
|
+
try {
|
|
682
|
+
reason = reopenFeature({ tree: featureTree, feature, check, ciRun: check.failed?.[0]?.detailsUrl ?? prUrl, sha });
|
|
683
|
+
} catch (err) {
|
|
684
|
+
emit({ finished: false, error: 'reopen-failed', output: gitErr(err), hint: `CI da PR sem verde (${check.state}) e o archive não foi desfeito: não mergeie ${prUrl}; rode \`git revert\` do commit de archive em ${featureTree}`, feature });
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
const red = check.state === 'fail';
|
|
688
|
+
setPrDraft(root, prUrl, true);
|
|
689
|
+
emit({
|
|
690
|
+
finished: false,
|
|
691
|
+
error: red ? 'pr-checks-fail' : 'pr-checks-no-verdict',
|
|
692
|
+
feature,
|
|
693
|
+
pr: { number: prNumberFromUrl(prUrl), url: prUrl },
|
|
694
|
+
reason,
|
|
695
|
+
...(red ? { failed: check.failed.map((c) => ({ workflow: c.workflowName, url: c.detailsUrl })) } : {}),
|
|
696
|
+
hint: red
|
|
697
|
+
? 'archive desfeito e Gate 3 reaberto: corrija (`gh run view <id> --log-failed`), commite e siga review.md §5; o finish reaproveita esta PR'
|
|
698
|
+
: `archive desfeito e Gate 3 reaberto: rode \`morph-spec verify ${feature} --json --detach\` (sem --ci, a completa local), \`gate-check ${feature} review\`, approve e o finish de novo; o finish reaproveita esta PR`,
|
|
699
|
+
});
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
checks = check.state === 'pass' ? { status: 'pass' } : { status: 'pending', reason: check.reason };
|
|
618
703
|
}
|
|
619
704
|
|
|
620
705
|
// The worktree flow keeps the branch (the open PR needs it); the common flow stays on morph/{f},
|
|
@@ -654,7 +739,10 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
654
739
|
pushed: expectedBranch,
|
|
655
740
|
base: defaultBranch,
|
|
656
741
|
pr: { number: prNumber, url: prUrl },
|
|
657
|
-
|
|
742
|
+
...(checks ? { checks } : {}),
|
|
743
|
+
nextStep: checks?.status === 'pending'
|
|
744
|
+
? `confira \`gh pr checks ${prNumber ?? expectedBranch}\` antes de mergear ${prLabel}: a suíte ainda não respondeu`
|
|
745
|
+
: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
|
|
658
746
|
...adrIndexNote(arch),
|
|
659
747
|
...(shedBackup ? { shedBackup } : {}),
|
|
660
748
|
...(e2e ? { e2e } : {}),
|
|
@@ -3,17 +3,18 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { buildRecap, changeSummary, recapScenes, renderRecapHtml } from '../../lib/recap/recap.js';
|
|
6
|
+
import { writeFeatureView } from '../../lib/view/feature-view.js';
|
|
7
|
+
import { FEATURE_VIEW_FILE } from '../../core/paths/output-schema.js';
|
|
6
8
|
import { narrateWithPiper, resolvePiper } from '../../lib/recap/piper.js';
|
|
7
9
|
|
|
8
|
-
export const RECAP_FILES = { html:
|
|
10
|
+
export const RECAP_FILES = { html: FEATURE_VIEW_FILE, data: 'recap.json' };
|
|
9
11
|
|
|
10
|
-
/** Writes `recap.
|
|
12
|
+
/** Writes `recap.json` and the page with the recap inside into `targetDir`, from the live feature folder. Never throws. */
|
|
11
13
|
export function writeRecap(featureDir, targetDir, feature, { cwd }) {
|
|
12
14
|
try {
|
|
13
15
|
const data = buildRecap(featureDir, feature, { changes: changeSummary(cwd) });
|
|
14
16
|
writeFileSync(join(targetDir, RECAP_FILES.data), JSON.stringify(data, null, 2));
|
|
15
|
-
|
|
16
|
-
return { ok: true, path: join(targetDir, RECAP_FILES.html) };
|
|
17
|
+
return writeFeatureView(targetDir, feature, { changes: data.changes, archived: true });
|
|
17
18
|
} catch (err) {
|
|
18
19
|
return { ok: false, reason: err.message };
|
|
19
20
|
}
|
|
@@ -56,7 +57,7 @@ export function recapCommand(feature, options = {}) {
|
|
|
56
57
|
note = piper.ok ? 'o piper falhou numa cena — ficou a voz do sistema' : piper.reason;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
if (narration === 'sistema')
|
|
60
|
+
if (narration === 'sistema') writeFeatureView(dir, feature, { changes: data.changes ?? null, archived: dir === archived });
|
|
60
61
|
|
|
61
62
|
if (options.json) {
|
|
62
63
|
console.log(JSON.stringify({ ok: true, path, narration, note }));
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
5
5
|
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
|
-
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
6
|
+
import { FEATURE_VIEW_FILE, getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
-
import { renderPlanHtml } from '../../lib/plan/plan-html.js';
|
|
9
8
|
import { buildChecks, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
10
9
|
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
10
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
@@ -39,9 +38,7 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
|
|
42
|
-
|
|
43
|
-
writeFileSync(viewPath, renderPlanHtml(featureName, plan, { planHash: hash }));
|
|
44
|
-
return { ...result, ok: true, exit: 0, written: true, viewPath };
|
|
41
|
+
return { ...result, ok: true, exit: 0, written: true, viewPath: join(cwd, '.morph', 'features', featureName, FEATURE_VIEW_FILE) };
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export function planCheckCommand(featureName, options = {}) {
|
|
@@ -82,7 +79,7 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
82
79
|
console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
|
|
83
80
|
} else {
|
|
84
81
|
console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
|
|
85
|
-
console.log(chalk.gray(`
|
|
82
|
+
console.log(chalk.gray(` página da feature: ${result.viewPath}`));
|
|
86
83
|
reportDotnetIgnores(gitignoreAdded);
|
|
87
84
|
}
|
|
88
85
|
process.exit(result.exit);
|