@polymorphism-tech/morph-spec 8.24.3 → 8.25.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/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/docs/CLI.md +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 +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/state/finish.js +191 -14
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.25.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.25.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
## Worktree / Fleet
|
|
23
23
|
|
|
24
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
|
|
24
|
+
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
|
|
25
25
|
- **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
|
|
26
26
|
- **`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.
|
|
27
27
|
- **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.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-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-implement
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.25.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
package/package.json
CHANGED
|
@@ -78,6 +78,14 @@ export function buildPrTitle(feature, fj) {
|
|
|
78
78
|
* `finished` is the integration (the actual value); `worktreeRemoved` is the
|
|
79
79
|
* best-effort teardown — two independent signals, as before.
|
|
80
80
|
*
|
|
81
|
+
* RESUMABLE. `merge-conflict`, `checkout-default-failed`, `push-failed` and
|
|
82
|
+
* `pr-create-failed` all fail AFTER the archive, and legitimately so: the
|
|
83
|
+
* archive commit is what travels with the branch. Re-running then found no
|
|
84
|
+
* `features/{f}` and refused from scratch, so every mid-way failure demanded a
|
|
85
|
+
* hand repair. Now a second run that finds the feature ARCHIVED on its own
|
|
86
|
+
* branch, not yet merged into the default, picks up at the integration and says
|
|
87
|
+
* `resumed: true` in its output — see Validation 0c.
|
|
88
|
+
*
|
|
81
89
|
* @param {string} feature
|
|
82
90
|
* @param {{ pr?: boolean, merge?: boolean, keep?: boolean, force?: boolean,
|
|
83
91
|
* discardIgnored?: boolean, cwd?: string }} [options]
|
|
@@ -98,7 +106,15 @@ export async function finishCommand(feature, options = {}) {
|
|
|
98
106
|
// containers and their ports when the feature closes. Best-effort by
|
|
99
107
|
// contract — a stack that refuses to come down must never fail a finish that
|
|
100
108
|
// already merged.
|
|
109
|
+
// Set by Validation 0c when this run picks up an already-archived feature.
|
|
110
|
+
// Stamped onto EVERY payload emitted from that point on, success or refusal:
|
|
111
|
+
// a resume merges a branch the user did not archive in this run, and that has
|
|
112
|
+
// to be visible. Someone who mistypes a name and lands on an archived feature
|
|
113
|
+
// would otherwise get a silent merge they never asked for.
|
|
114
|
+
let resumed = false;
|
|
115
|
+
|
|
101
116
|
const emit = (obj) => {
|
|
117
|
+
if (resumed) obj = { ...obj, resumed: true };
|
|
102
118
|
if (obj?.finished === true) {
|
|
103
119
|
try {
|
|
104
120
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
@@ -211,25 +227,128 @@ export async function finishCommand(feature, options = {}) {
|
|
|
211
227
|
featureCwd = root;
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
// Validation 0c: the feature must still
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
230
|
+
// Validation 0c: the feature must still be in `features/` — or, if it is not,
|
|
231
|
+
// this run has to decide which of three states it is in before doing anything:
|
|
232
|
+
// gone entirely, already ARCHIVED and awaiting integration (resume), or
|
|
233
|
+
// already integrated (nothing to do).
|
|
234
|
+
//
|
|
235
|
+
// Originally all three arrived as `review-not-approved`, because a missing
|
|
236
|
+
// feature.json parses to null and the optional chaining below fell straight
|
|
237
|
+
// through — being told "Gate 3 not approved" about an approved gate is what
|
|
238
|
+
// sent the user looking in the wrong place after an interrupted finish.
|
|
221
239
|
//
|
|
222
240
|
// Keyed on the DIRECTORY, not on the parse: readFeatureJson also returns null
|
|
223
241
|
// for malformed JSON, and a legacy feature with a folder but no (or broken)
|
|
224
242
|
// feature.json is a real thing that `--force` is expected to push through.
|
|
225
243
|
if (!existsSync(join(featureCwd, '.morph', 'features', feature))) {
|
|
226
|
-
|
|
227
|
-
|
|
244
|
+
const archivePath = join(featureCwd, '.morph', 'archive', feature);
|
|
245
|
+
if (!existsSync(archivePath)) {
|
|
246
|
+
emit({ finished: false, error: 'feature-not-found', feature, ...featureElsewhereHint(featureCwd, root, feature) });
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── RESUME ──────────────────────────────────────────────────────────────
|
|
251
|
+
// Archived HERE, on the branch this finish would merge — every path that
|
|
252
|
+
// reaches this line has already proved we are standing on `morph/{f}`
|
|
253
|
+
// (worktree flow: found.branch === expectedBranch; common flow: branchExists
|
|
254
|
+
// + currentBranch === expectedBranch). So the archive step is done and only
|
|
255
|
+
// the integration is missing. Which is exactly what a failed merge/push
|
|
256
|
+
// leaves behind, and what used to require repairing by hand.
|
|
257
|
+
//
|
|
258
|
+
// The one thing still to establish is whether the integration ALSO
|
|
259
|
+
// happened. A branch already contained in the default is a finish that
|
|
260
|
+
// COMPLETED (`--keep`, or a `--pr` whose PR was merged) — re-merging it is
|
|
261
|
+
// the hazard this check exists to stop, and it is why resume is keyed on
|
|
262
|
+
// the branch's relationship to the default rather than on the archive alone.
|
|
263
|
+
const target = detectDefaultBranch(root);
|
|
264
|
+
if (!target) {
|
|
265
|
+
emit({ finished: false, error: 'no-default-branch', hint: 'não foi possível resolver a branch default (origin/HEAD, main ou master)', feature });
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const merged = branchMergedInto(expectedBranch, target, root);
|
|
269
|
+
if (merged === null) {
|
|
270
|
+
// PROVE-or-DECLINE, same discipline as the worktree teardown: we could not
|
|
271
|
+
// establish the branch's state, so we do not act on a guess. Refusing is
|
|
272
|
+
// recoverable; a duplicate merge is not.
|
|
273
|
+
emit({
|
|
274
|
+
finished: false,
|
|
275
|
+
error: 'resume-undecidable',
|
|
276
|
+
feature,
|
|
277
|
+
branch: expectedBranch,
|
|
278
|
+
base: target,
|
|
279
|
+
archived: archivePath.replace(/\\/g, '/'),
|
|
280
|
+
hint: `'${feature}' está arquivada, mas não foi possível comparar ${expectedBranch} com ${target} (ref ausente?) — sem saber se o merge já aconteceu, retomar arriscaria mergear duas vezes. Verifique o estado do git antes de integrar à mão`,
|
|
281
|
+
});
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
if (merged) {
|
|
285
|
+
emit({
|
|
286
|
+
finished: false,
|
|
287
|
+
error: 'already-finished',
|
|
288
|
+
feature,
|
|
289
|
+
branch: expectedBranch,
|
|
290
|
+
base: target,
|
|
291
|
+
archived: archivePath.replace(/\\/g, '/'),
|
|
292
|
+
hint: `'${feature}' já foi arquivada E ${expectedBranch} já está contida em ${target} — o finish anterior concluiu. Nada a integrar.`,
|
|
293
|
+
});
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// "Already integrated" is MODE-SPECIFIC, and the ancestor test above only
|
|
298
|
+
// answers it for `--merge`. A `--pr` that completed leaves its branch
|
|
299
|
+
// deliberately UNMERGED (that is what the open PR is for), so the ancestor
|
|
300
|
+
// test says "not merged" and would wave the resume through — straight into
|
|
301
|
+
// a second `gh pr create` on a head that already has one. The common flow
|
|
302
|
+
// makes that trivially reachable: integratePr never leaves `morph/{f}`, so
|
|
303
|
+
// simply re-running the command lands here.
|
|
304
|
+
//
|
|
305
|
+
// `gh pr create` refuses a duplicate, so the damage was never two PRs — it
|
|
306
|
+
// was `pr-create-failed` standing in for "this already finished", which is
|
|
307
|
+
// the same collapsed diagnosis the `feature-not-found` split removed.
|
|
308
|
+
if (mode === 'pr') {
|
|
309
|
+
const openPr = openPrFor(expectedBranch, root);
|
|
310
|
+
if (openPr === null) {
|
|
311
|
+
emit({
|
|
312
|
+
finished: false,
|
|
313
|
+
error: 'resume-undecidable',
|
|
314
|
+
feature,
|
|
315
|
+
branch: expectedBranch,
|
|
316
|
+
archived: archivePath.replace(/\\/g, '/'),
|
|
317
|
+
hint: `'${feature}' está arquivada e ${expectedBranch} não foi mergeada, mas não foi possível consultar PRs abertos com \`gh\` — retomar às cegas abriria um PR duplicado. Verifique com \`gh pr list --head ${expectedBranch}\``,
|
|
318
|
+
});
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
if (openPr) {
|
|
322
|
+
emit({
|
|
323
|
+
finished: false,
|
|
324
|
+
error: 'already-finished',
|
|
325
|
+
feature,
|
|
326
|
+
branch: expectedBranch,
|
|
327
|
+
base: target,
|
|
328
|
+
archived: archivePath.replace(/\\/g, '/'),
|
|
329
|
+
...(typeof openPr === 'string' ? { pr: openPr } : {}),
|
|
330
|
+
hint: `'${feature}' já foi arquivada e ${expectedBranch} já tem PR aberto — o finish anterior concluiu. Falta apenas mergear o PR.`,
|
|
331
|
+
});
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
resumed = true;
|
|
228
337
|
}
|
|
229
338
|
|
|
230
|
-
// Validation 1: Gate 3 (review) approved on the feature's own feature.json
|
|
231
|
-
|
|
232
|
-
|
|
339
|
+
// Validation 1: Gate 3 (review) approved on the feature's own feature.json —
|
|
340
|
+
// which on a resume lives in `archive/{f}`, where runArchive moved it.
|
|
341
|
+
//
|
|
342
|
+
// NOT re-checked on a resume, deliberately. The archive IS the gate's receipt:
|
|
343
|
+
// runArchive refuses `review-not-approved` itself, so a feature that reached
|
|
344
|
+
// `archive/` either passed Gate 3 or was explicitly forced past it. Re-reading
|
|
345
|
+
// it here would only add a way to fail — an archive whose feature.json is
|
|
346
|
+
// absent or unparseable (legacy shapes, a forced close) parses to null and
|
|
347
|
+
// falls straight through to `review-not-approved`, which is the exact
|
|
348
|
+
// collapsed diagnosis the `feature-not-found` fix removed. Resume integrates
|
|
349
|
+
// what was already archived; it does not re-litigate why.
|
|
350
|
+
const fj = readFeatureJson(featureCwd, feature, resumed ? 'archive' : 'features');
|
|
351
|
+
if (!resumed && !options.force && !(fj?.approvalGates?.review?.approved === true || fj?.approvals?.review?.approved === true)) {
|
|
233
352
|
emit({ finished: false, error: 'review-not-approved', feature });
|
|
234
353
|
process.exit(1);
|
|
235
354
|
}
|
|
@@ -627,9 +746,67 @@ function featureElsewhereHint(featureCwd, root, feature) {
|
|
|
627
746
|
return { hint: `'${feature}' não existe em .morph/features/ nesta árvore. Confira o nome com \`morph-spec list --all\`.` };
|
|
628
747
|
}
|
|
629
748
|
|
|
630
|
-
|
|
749
|
+
/**
|
|
750
|
+
* @param {string} cwd
|
|
751
|
+
* @param {string} feature
|
|
752
|
+
* @param {'features'|'archive'} [bucket] - `archive` on a resume, where the
|
|
753
|
+
* feature.json has already been moved (and stamped `completed`).
|
|
754
|
+
*/
|
|
755
|
+
function readFeatureJson(cwd, feature, bucket = 'features') {
|
|
756
|
+
try {
|
|
757
|
+
return JSON.parse(readFileSync(join(cwd, '.morph', bucket, feature, 'feature.json'), 'utf8'));
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Is `branch` already contained in `target` — i.e. did an earlier finish get its
|
|
765
|
+
* merge in?
|
|
766
|
+
*
|
|
767
|
+
* Three-valued ON PURPOSE. `git merge-base --is-ancestor` answers by exit code,
|
|
768
|
+
* and the two failure modes mean opposite things: **1** is a real "no, not
|
|
769
|
+
* merged", while **128** is "one of these refs does not resolve" and proves
|
|
770
|
+
* nothing at all. Collapsing them to a boolean would read an unresolvable ref as
|
|
771
|
+
* "not merged" and let a resume re-merge a completed feature.
|
|
772
|
+
*
|
|
773
|
+
* @param {string} branch
|
|
774
|
+
* @param {string} target
|
|
775
|
+
* @param {string} cwd
|
|
776
|
+
* @returns {boolean|null} null when the comparison could not be made
|
|
777
|
+
*/
|
|
778
|
+
export function branchMergedInto(branch, target, cwd) {
|
|
779
|
+
try {
|
|
780
|
+
execFileSync('git', ['merge-base', '--is-ancestor', branch, target], { cwd, stdio: 'pipe' });
|
|
781
|
+
return true;
|
|
782
|
+
} catch (err) {
|
|
783
|
+
return err?.status === 1 ? false : null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Does `branch` already have an OPEN pull request? The `--pr` half of
|
|
789
|
+
* "already integrated" — a completed `--pr` leaves the branch unmerged on
|
|
790
|
+
* purpose, so `branchMergedInto` cannot see it.
|
|
791
|
+
*
|
|
792
|
+
* Three-valued for the same reason: an unanswerable query (no `gh`, no auth, no
|
|
793
|
+
* network) is not evidence of "no PR", and treating it as such would resume
|
|
794
|
+
* into a duplicate `gh pr create`.
|
|
795
|
+
*
|
|
796
|
+
* @param {string} branch
|
|
797
|
+
* @param {string} cwd
|
|
798
|
+
* @returns {string|boolean|null} the PR url, `true` (open PR, url unavailable),
|
|
799
|
+
* `false` (none open), or `null` when the question could not be asked
|
|
800
|
+
*/
|
|
801
|
+
function openPrFor(branch, cwd) {
|
|
802
|
+
const { bin, prefix } = resolveGh();
|
|
631
803
|
try {
|
|
632
|
-
|
|
804
|
+
const out = execFileSync(bin, [...prefix, 'pr', 'list', '--head', branch, '--state', 'open', '--json', 'url'], { cwd, stdio: 'pipe' })
|
|
805
|
+
.toString()
|
|
806
|
+
.trim();
|
|
807
|
+
const arr = JSON.parse(out || '[]');
|
|
808
|
+
if (!Array.isArray(arr) || !arr.length) return false;
|
|
809
|
+
return typeof arr[0]?.url === 'string' ? arr[0].url : true;
|
|
633
810
|
} catch {
|
|
634
811
|
return null;
|
|
635
812
|
}
|