@polymorphism-tech/morph-spec 8.24.2 → 8.24.3

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 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.24.2
6
+ **Version:** 8.24.3
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.24.2 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.24.3 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.24.2",
3
+ "version": "8.24.3",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v8.24.2 by Polymorphism Tech*
188
+ *morph-spec v8.24.3 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.24.2 by Polymorphism Tech*
252
+ *morph-spec v8.24.3 by Polymorphism Tech*
@@ -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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
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.24.2"
7
+ cliVersion: "8.24.3"
8
8
  ---
9
9
 
10
10
  # morph-uiux
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.24.2",
3
+ "version": "8.24.3",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -968,11 +968,23 @@ export function shedRootFeatureScope(root, feature) {
968
968
  * from blocking every other feature's finish). `ok:false` means the status call
969
969
  * itself failed; an empty `dirt` means clean enough to merge into.
970
970
  *
971
+ * `exemptOwnScope` additionally forgives everything under
972
+ * `.morph/features/{feature}/` — the EXACT pathspec `shedRootFeatureScope`
973
+ * backs up and discards. It exists so `finish` can ask "is the root clean
974
+ * enough to merge into?" BEFORE archiving anything, where the shed has not run
975
+ * yet and the feature's own scaffold is still on disk. Without it that early
976
+ * question answers "dirty" for a root that the shed would have cleaned, and a
977
+ * finish that works today would start being refused.
978
+ *
979
+ * Callers must keep the post-shed check: this one is an early refusal so the
980
+ * archive never happens, not the authoritative gate.
981
+ *
971
982
  * @param {string} root
972
983
  * @param {string} feature - the feature THIS finish acts on (its own scope is the shed's job)
984
+ * @param {{ exemptOwnScope?: boolean }} [opts]
973
985
  * @returns {{ ok: boolean, dirt: string[] }}
974
986
  */
975
- export function primaryRootUnexplainedDirt(root, feature) {
987
+ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
976
988
  let rootStatusRaw;
977
989
  try {
978
990
  // `--untracked-files=all`, not the default: the default collapses an
@@ -982,9 +994,11 @@ export function primaryRootUnexplainedDirt(root, feature) {
982
994
  } catch {
983
995
  return { ok: false, dirt: [] };
984
996
  }
997
+ const ownScope = `.morph/features/${feature}/`;
985
998
  const dirt = rootStatusRaw.split('\0').filter((record) => {
986
999
  if (!record || record.length < 4) return false;
987
1000
  const p = record.slice(3).replace(/\\/g, '/');
1001
+ if (opts.exemptOwnScope && p.startsWith(ownScope)) return false;
988
1002
  return !isLiveSiblingWorktreeRegistration(p, root, feature);
989
1003
  });
990
1004
  return { ok: true, dirt };
@@ -180,6 +180,14 @@ export async function finishCommand(feature, options = {}) {
180
180
  // merge). A feature with NO morph/{f} branch was born before this flow —
181
181
  // route it explicitly to archive-without-integrate, never a generic refusal.
182
182
  if (!branchExists(expectedBranch, root)) {
183
+ // No branch AND no feature on disk is not a legacy feature — it is a
184
+ // feature that is not here at all (wrong name, wrong tree, or already
185
+ // archived). Routing that to `archive legacy` would tell the user to
186
+ // archive something that does not exist.
187
+ if (!existsSync(join(root, '.morph', 'features', feature))) {
188
+ emit({ finished: false, error: 'feature-not-found', feature, ...featureElsewhereHint(root, root, feature) });
189
+ process.exit(1);
190
+ }
183
191
  emit({
184
192
  finished: false,
185
193
  error: 'no-feature-branch',
@@ -203,6 +211,22 @@ export async function finishCommand(feature, options = {}) {
203
211
  featureCwd = root;
204
212
  }
205
213
 
214
+ // Validation 0c: the feature must still BE there. Without this, a missing
215
+ // feature.json makes the optional chaining below fall through to
216
+ // `review-not-approved`, which collapses three different situations into one
217
+ // wrong diagnosis: gate genuinely unapproved, feature already archived, and
218
+ // feature living on a branch we are not on. The second one is what a user
219
+ // hits when retrying an interrupted finish, and being told "Gate 3 not
220
+ // approved" about an approved gate sends them looking in the wrong place.
221
+ //
222
+ // Keyed on the DIRECTORY, not on the parse: readFeatureJson also returns null
223
+ // for malformed JSON, and a legacy feature with a folder but no (or broken)
224
+ // feature.json is a real thing that `--force` is expected to push through.
225
+ if (!existsSync(join(featureCwd, '.morph', 'features', feature))) {
226
+ emit({ finished: false, error: 'feature-not-found', feature, ...featureElsewhereHint(featureCwd, root, feature) });
227
+ process.exit(1);
228
+ }
229
+
206
230
  // Validation 1: Gate 3 (review) approved on the feature's own feature.json.
207
231
  const fj = readFeatureJson(featureCwd, feature);
208
232
  if (!options.force && !(fj?.approvalGates?.review?.approved === true || fj?.approvals?.review?.approved === true)) {
@@ -239,6 +263,37 @@ export async function finishCommand(feature, options = {}) {
239
263
  process.exit(1);
240
264
  }
241
265
 
266
+ // ── MERGE precondition, BEFORE the archive ────────────────────────────────
267
+ // `--merge` lands its merge commit in the PRIMARY ROOT, so the root has to be
268
+ // clean. That check used to live inside integrateMerge — i.e. AFTER
269
+ // runArchive had already moved the feature to .morph/archive/, promoted its
270
+ // use cases and COMMITTED. A root dirtied by unrelated work therefore got a
271
+ // refusal on a half-finished feature, and the retry then failed for a
272
+ // different reason entirely (the feature was no longer in features/).
273
+ //
274
+ // `exemptOwnScope` because shedRootFeatureScope has not run yet: this
275
+ // feature's own root-side scaffold is still on disk and is not a reason to
276
+ // refuse — the shed backs it up and discards it moments later.
277
+ //
278
+ // `--pr` is exempt by design: integratePr pushes from the root but never
279
+ // checks out or merges there, so root dirt cannot corrupt it.
280
+ if (mode === 'merge' && isWorktreeFlow) {
281
+ const rootPre = primaryRootUnexplainedDirt(root, feature, { exemptOwnScope: true });
282
+ if (!rootPre.ok) {
283
+ emit({ finished: false, error: 'primary-root-status-failed', feature });
284
+ process.exit(1);
285
+ }
286
+ if (rootPre.dirt.length) {
287
+ emit({
288
+ finished: false,
289
+ error: 'primary-root-dirty',
290
+ hint: 'a raiz primária tem trabalho não commitado — o merge acontece nela; commite ou faça stash antes do finish',
291
+ feature,
292
+ });
293
+ process.exit(1);
294
+ }
295
+ }
296
+
242
297
  // ── PR preconditions run BEFORE the archive, so a refused --pr has NO side
243
298
  // effect (nothing archived, nothing committed). ─────────────────────────
244
299
  if (mode === 'pr') {
@@ -284,6 +339,11 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
284
339
  const { shedBackup } = shedRootFeatureScope(root, feature);
285
340
  const backupNote = shedBackup ? { shedBackup } : {};
286
341
 
342
+ // Final gate before the merge commit lands. The pre-archive check above
343
+ // already refused an unrelated-dirt root WITHOUT side effects, so reaching
344
+ // a refusal HERE means the shed itself dirtied the root — a bug signal, not
345
+ // user error. Kept because this is the last moment before we mutate the
346
+ // default branch, and it is nearly free.
287
347
  const rootClean = primaryRootUnexplainedDirt(root, feature);
288
348
  if (!rootClean.ok) {
289
349
  emit({ finished: false, error: 'primary-root-status-failed', feature, ...backupNote });
@@ -521,6 +581,52 @@ function gitErr(err) {
521
581
  return (((err.stdout && err.stdout.toString()) || '') + ((err.stderr && err.stderr.toString()) || '')).slice(0, 2000) || (err.message || '').slice(0, 500);
522
582
  }
523
583
 
584
+ /**
585
+ * Where a feature that is NOT in `features/` actually went, as extra fields on
586
+ * the `feature-not-found` payload.
587
+ *
588
+ * Exists because "not found" is the least useful thing we can say about a
589
+ * feature the user just worked on. The two real cases:
590
+ *
591
+ * - ARCHIVED: an earlier finish archived it and then failed to integrate.
592
+ * What is missing is the merge, not the whole finish — say so, because the
593
+ * user's instinct is to re-run `finish` and get refused again.
594
+ * - ON ITS BRANCH: the feature was created on `morph/{f}` and the command is
595
+ * running from the default branch, where `.morph/features/{f}` does not
596
+ * exist yet. Naming the branch turns a dead end into one checkout.
597
+ *
598
+ * Fail-open in every direction: a hint that cannot be computed is simply absent.
599
+ *
600
+ * @param {string} featureCwd - the tree we looked in
601
+ * @param {string} root - the primary root
602
+ * @param {string} feature
603
+ * @returns {{ archived?: string, hint?: string, onBranch?: string }}
604
+ */
605
+ function featureElsewhereHint(featureCwd, root, feature) {
606
+ try {
607
+ const archived = join(featureCwd, '.morph', 'archive', feature);
608
+ if (existsSync(archived)) {
609
+ return {
610
+ archived: archived.replace(/\\/g, '/'),
611
+ hint: `'${feature}' já foi arquivada por um finish anterior — o que faltou foi a integração. Verifique se o merge/PR já aconteceu antes de refazer o trabalho à mão.`,
612
+ };
613
+ }
614
+ } catch { /* fail-open */ }
615
+
616
+ try {
617
+ const branch = `morph/${feature}`;
618
+ const listed = execFileSync('git', ['branch', '--list', branch], { cwd: root, stdio: 'pipe' }).toString().trim();
619
+ if (listed) {
620
+ return {
621
+ onBranch: branch,
622
+ hint: `'${feature}' não está nesta árvore, mas a branch ${branch} existe — rode o finish de lá (\`git checkout ${branch}\`) ou do worktree da feature.`,
623
+ };
624
+ }
625
+ } catch { /* fail-open — sem git, sem hint */ }
626
+
627
+ return { hint: `'${feature}' não existe em .morph/features/ nesta árvore. Confira o nome com \`morph-spec list --all\`.` };
628
+ }
629
+
524
630
  function readFeatureJson(cwd, feature) {
525
631
  try {
526
632
  return JSON.parse(readFileSync(join(cwd, '.morph', 'features', feature, 'feature.json'), 'utf8'));