@polymorphism-tech/morph-spec 8.8.0 → 8.9.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 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.8.0
6
+ **Version:** 8.9.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.8.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.9.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
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.8.0 by Polymorphism Tech*
188
+ *morph-spec v8.9.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.8.0 by Polymorphism Tech*
252
+ *morph-spec v8.9.0 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.8.0"
7
+ cliVersion: "8.9.0"
8
8
  ---
9
9
 
10
10
  # create-umbraco-lp
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.8.0"
7
+ cliVersion: "8.9.0"
8
8
  ---
9
9
 
10
10
  # migrate-lp-to-umbraco
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.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.8.0"
7
+ cliVersion: "8.9.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
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.8.0"
7
+ cliVersion: "8.9.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -88,15 +88,109 @@ const REQUIRED_COMMAND_FILES = [
88
88
  'src/commands/context/context-refresh.js',
89
89
  ];
90
90
 
91
- // framework standards (stack-relevant only)
91
+ /**
92
+ * Validate a parsed `agents.json` against the v5 contract.
93
+ *
94
+ * Exported, and pure, so the tests can exercise the REAL rules. They used to
95
+ * re-implement this logic inline "because doctorCommand doesn't export
96
+ * individual check functions" — and the two promptly disagreed: the tests
97
+ * asserted a v4 document (version 4.x, `tier` and `active_phases` required)
98
+ * while this code treats exactly those fields as deprecated. Both stayed green
99
+ * because they never met. A canary that destroyed this whole check left the
100
+ * suite at 18/18.
101
+ *
102
+ * @param {Object} agentsData - parsed agents.json
103
+ * @returns {Array<{name: string, status: 'ok'|'warn'|'error', msg?: string}>}
104
+ */
105
+ export function validateAgentsJson(agentsData) {
106
+ const checks = [];
107
+ const agentEntries = Object.entries(agentsData.agents || {})
108
+ .filter(([k]) => !k.startsWith('_comment'));
109
+ const actualCount = agentEntries.length;
110
+ const declaredCount = agentsData.total_agents;
111
+
112
+ // 1: total_agents matches actual count
113
+ if (actualCount !== declaredCount) {
114
+ checks.push({
115
+ name: 'agents.json count',
116
+ status: 'warn',
117
+ msg: `total_agents declares ${declaredCount} but found ${actualCount} agents`
118
+ });
119
+ } else {
120
+ checks.push({ name: 'agents.json count', status: 'ok', msg: `${actualCount} agents` });
121
+ }
122
+
123
+ // 2: required v5 personas present
124
+ const missingPersonas = REQUIRED_AGENTS.filter(id => !(agentsData.agents || {})[id]);
125
+ if (missingPersonas.length > 0) {
126
+ checks.push({
127
+ name: 'agents.json required personas',
128
+ status: 'error',
129
+ msg: `Missing required personas: ${missingPersonas.join(', ')}`
130
+ });
131
+ } else {
132
+ checks.push({ name: 'agents.json required personas', status: 'ok' });
133
+ }
134
+
135
+ // 3: v5 schema (flat personas, no tiers)
136
+ const version = agentsData.version || 'unknown';
137
+ const isV5 = version.startsWith('5.');
138
+ const v4Fields = [];
139
+ const missingV5Fields = [];
140
+
141
+ for (const [id, data] of agentEntries) {
142
+ if (data.tier !== undefined) v4Fields.push(`${id}.tier`);
143
+ if (data.active_phases !== undefined) v4Fields.push(`${id}.active_phases`);
144
+ if (data.reports_to !== undefined) v4Fields.push(`${id}.reports_to`);
145
+ if (data.teammate !== undefined) v4Fields.push(`${id}.teammate`);
146
+ if (!data.title) missingV5Fields.push(`${id}.title`);
147
+ if (!Array.isArray(data.domains)) missingV5Fields.push(`${id}.domains`);
148
+ if (typeof data.persona !== 'string' || !data.persona) missingV5Fields.push(`${id}.persona`);
149
+ }
150
+
151
+ if (isV5) {
152
+ checks.push({ name: `agents.json schema (v${version})`, status: 'ok' });
153
+ } else {
154
+ checks.push({
155
+ name: `agents.json schema (v${version})`,
156
+ status: 'warn',
157
+ msg: 'expected v5.x — run agents migration'
158
+ });
159
+ }
160
+
161
+ if (v4Fields.length > 0) {
162
+ const sample = v4Fields.slice(0, 3).join(', ');
163
+ checks.push({
164
+ name: 'agents.json deprecated v4 fields',
165
+ status: 'warn',
166
+ msg: `${v4Fields.length} v4 field(s) found: ${sample}${v4Fields.length > 3 ? '...' : ''}`
167
+ });
168
+ }
169
+
170
+ if (missingV5Fields.length > 0 && isV5) {
171
+ const sample = missingV5Fields.slice(0, 3).join(', ');
172
+ checks.push({
173
+ name: 'agents.json v5 required fields',
174
+ status: 'warn',
175
+ msg: `${missingV5Fields.length} missing: ${sample}${missingV5Fields.length > 3 ? '...' : ''}`
176
+ });
177
+ }
178
+
179
+ return checks;
180
+ }
181
+
182
+ // framework standards (stack-relevant only).
183
+ // Keep every entry pointing at a file that exists: a check that can never pass
184
+ // carries the same information as one that can never fail — none. This list
185
+ // still named two standards from a `workflows/` directory that no longer
186
+ // exists, which pinned `doctor --full` at a permanent 3/6 and made a real
187
+ // regression indistinguishable from the usual red.
92
188
  const FRAMEWORK_STANDARDS = [
93
189
  'framework/standards/architecture/vertical-slice/vertical-slice.md',
94
190
  'framework/standards/data/nosql/cosmos-db.md',
95
191
  'framework/standards/data/nosql/cache/redis.md',
96
192
  'framework/standards/data/vector-search/rag-chunking.md',
97
- 'framework/standards/integration/mcp/mcp-tools.md',
98
- 'framework/standards/workflows/thread-management.md',
99
- 'framework/standards/workflows/parallel-execution.md'
193
+ 'framework/standards/integration/mcp/mcp-tools.md'
100
194
  ];
101
195
 
102
196
  // required personas expected in agents.json (v5 — the universal, stack-agnostic
@@ -127,39 +221,61 @@ async function doctorFullCommand(frameworkRoot) {
127
221
  }
128
222
  };
129
223
 
130
- // ── 1. Core Libraries ───────────────────────────────────────────────────
131
- console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
132
- const missingLibs = [];
133
- for (const f of REQUIRED_LIB_FILES) {
134
- if (!(await pathExists(join(frameworkRoot, f)))) missingLibs.push(f.split('/').pop());
135
- }
136
- check(`Core Libraries (${REQUIRED_LIB_FILES.length - missingLibs.length}/${REQUIRED_LIB_FILES.length})`,
137
- missingLibs.length === 0, false,
138
- missingLibs.length > 0 ? `missing: ${missingLibs.join(', ')}` : '');
139
-
140
- // ── 2. Command Files ─────────────────────────────────────────────────────
141
- console.log(chalk.cyan(`\n src/commands/ — Command Files (${REQUIRED_COMMAND_FILES.length} files)`));
142
- const missingCmds = [];
143
- for (const f of REQUIRED_COMMAND_FILES) {
144
- if (!(await pathExists(join(frameworkRoot, f)))) missingCmds.push(f.split('/').pop());
224
+ // Two layouts reach this command and they disagree on where everything is.
225
+ //
226
+ // framework repo src/, framework/, no .morph/ at all
227
+ // installed project .morph/framework/, and the CLI in node_modules
228
+ //
229
+ // Half the checks assumed one layout and half the other, so `--full` could
230
+ // not pass anywhere: red in the repo (no .morph/) and red in a project (no
231
+ // src/). A check that can never pass reports exactly as much as one that can
232
+ // never fail. Each check now runs only where it means something.
233
+ const installedAgentsPath = join(frameworkRoot, '.morph/framework/agents.json');
234
+ const canonicalAgentsPath = join(frameworkRoot, 'framework/agents.json');
235
+ const inFrameworkRepo = !(await pathExists(installedAgentsPath))
236
+ && await pathExists(canonicalAgentsPath);
237
+ const agentsPath = inFrameworkRepo ? canonicalAgentsPath : installedAgentsPath;
238
+
239
+ console.log(chalk.gray(`\n Contexto: ${inFrameworkRepo ? 'repo do framework' : 'projeto instalado'}`));
240
+
241
+ // ── 1-2. Source tree (framework repo only) ──────────────────────────────
242
+ // An installed project has no src/ — the CLI lives in node_modules, and its
243
+ // integrity is npm's job, not the doctor's.
244
+ if (inFrameworkRepo) {
245
+ console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
246
+ const missingLibs = [];
247
+ for (const f of REQUIRED_LIB_FILES) {
248
+ if (!(await pathExists(join(frameworkRoot, f)))) missingLibs.push(f.split('/').pop());
249
+ }
250
+ check(`Core Libraries (${REQUIRED_LIB_FILES.length - missingLibs.length}/${REQUIRED_LIB_FILES.length})`,
251
+ missingLibs.length === 0, false,
252
+ missingLibs.length > 0 ? `missing: ${missingLibs.join(', ')}` : '');
253
+
254
+ console.log(chalk.cyan(`\n src/commands/ — Command Files (${REQUIRED_COMMAND_FILES.length} files)`));
255
+ const missingCmds = [];
256
+ for (const f of REQUIRED_COMMAND_FILES) {
257
+ if (!(await pathExists(join(frameworkRoot, f)))) missingCmds.push(f.split('/').pop());
258
+ }
259
+ check(`Command Files (${REQUIRED_COMMAND_FILES.length - missingCmds.length}/${REQUIRED_COMMAND_FILES.length})`,
260
+ missingCmds.length === 0, false,
261
+ missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
145
262
  }
146
- check(`Command Files (${REQUIRED_COMMAND_FILES.length - missingCmds.length}/${REQUIRED_COMMAND_FILES.length})`,
147
- missingCmds.length === 0, false,
148
- missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
149
263
 
150
264
  // ── 3. Framework Standards ───────────────────────────────────────────────
151
- console.log(chalk.cyan(`\n framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
265
+ // Same files, different root: the repo holds them at framework/standards/,
266
+ // an installed project at .morph/framework/standards/.
267
+ const stdsRoot = inFrameworkRepo ? frameworkRoot : join(frameworkRoot, '.morph');
268
+ console.log(chalk.cyan(`\n ${inFrameworkRepo ? '' : '.morph/'}framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
152
269
  const missingStds = [];
153
270
  for (const f of FRAMEWORK_STANDARDS) {
154
- if (!(await pathExists(join(frameworkRoot, f)))) missingStds.push(f.replace('framework/standards/', ''));
271
+ if (!(await pathExists(join(stdsRoot, f)))) missingStds.push(f.replace('framework/standards/', ''));
155
272
  }
156
273
  check(`Framework Standards (${FRAMEWORK_STANDARDS.length - missingStds.length}/${FRAMEWORK_STANDARDS.length})`,
157
274
  missingStds.length === 0, false,
158
275
  missingStds.length > 0 ? `missing: ${missingStds.join(', ')}` : '');
159
276
 
160
277
  // ── 5. Required Agents in agents.json ───────────────────────────────────
161
- console.log(chalk.cyan(`\n .morph/framework/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
162
- const agentsPath = join(frameworkRoot, '.morph/framework/agents.json');
278
+ console.log(chalk.cyan(`\n ${inFrameworkRepo ? 'framework' : '.morph/framework'}/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
163
279
  if (await pathExists(agentsPath)) {
164
280
  try {
165
281
  const agentsConfig = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
@@ -176,8 +292,10 @@ async function doctorFullCommand(frameworkRoot) {
176
292
  }
177
293
 
178
294
  // ── 6. agents.json version freshness ──────────────────────────────────
179
- const canonicalAgentsPath = join(frameworkRoot, 'framework/agents.json');
180
- if (await pathExists(agentsPath) && await pathExists(canonicalAgentsPath)) {
295
+ // Only meaningful for an installed project: it compares what was installed
296
+ // against the canonical source. In the framework repo the two are the same
297
+ // file, so the comparison is vacuous and is skipped.
298
+ if (!inFrameworkRepo && await pathExists(agentsPath) && await pathExists(canonicalAgentsPath)) {
181
299
  try {
182
300
  const installed = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
183
301
  const canonical = JSON.parse(await fs.readFile(canonicalAgentsPath, 'utf8'));
@@ -193,7 +311,9 @@ async function doctorFullCommand(frameworkRoot) {
193
311
  // agents.json is the single source of truth; the .claude/agents/morph-*.md
194
312
  // files are derived artifacts (ADR-001). Missing = broken dispatch (error);
195
313
  // stale/hand-edited or orphan = drift (warning).
196
- if (await pathExists(agentsPath)) {
314
+ // Skipped in the framework repo: the generated `.claude/agents/morph-*.md`
315
+ // are an artifact of installing into a project, not of the source tree.
316
+ if (!inFrameworkRepo && await pathExists(agentsPath)) {
197
317
  console.log(chalk.cyan('\n .claude/agents/ — Generated personas (drift vs agents.json)'));
198
318
  try {
199
319
  let projectTags = [];
@@ -661,82 +781,10 @@ export async function doctorCommand(options = {}) {
661
781
  if (await pathExists(agentsPath)) {
662
782
  try {
663
783
  const agentsData = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
664
- const agentEntries = Object.entries(agentsData.agents || {})
665
- .filter(([k]) => !k.startsWith('_comment'));
666
- const actualCount = agentEntries.length;
667
- const declaredCount = agentsData.total_agents;
668
-
669
- // Check 1: total_agents matches actual count
670
- if (actualCount !== declaredCount) {
671
- checks.push({
672
- name: 'agents.json count',
673
- status: 'warn',
674
- msg: `total_agents declares ${declaredCount} but found ${actualCount} agents`
675
- });
676
- hasWarnings = true;
677
- } else {
678
- checks.push({ name: 'agents.json count', status: 'ok', msg: `${actualCount} agents` });
679
- }
680
-
681
- // Check 2: required v5 personas present
682
- const requiredPersonas = REQUIRED_AGENTS;
683
- const missingPersonas = requiredPersonas.filter(id => !agentsData.agents[id]);
684
- if (missingPersonas.length > 0) {
685
- checks.push({
686
- name: 'agents.json required personas',
687
- status: 'error',
688
- msg: `Missing required personas: ${missingPersonas.join(', ')}`
689
- });
690
- hasErrors = true;
691
- } else {
692
- checks.push({ name: 'agents.json required personas', status: 'ok' });
693
- }
694
-
695
- // Check 3: v5 schema validation (flat personas, no tiers)
696
- const version = agentsData.version || 'unknown';
697
- const isV5 = version.startsWith('5.');
698
- const v4Fields = [];
699
- const missingV5Fields = [];
700
-
701
- for (const [id, data] of agentEntries) {
702
- if (data.tier !== undefined) v4Fields.push(`${id}.tier`);
703
- if (data.active_phases !== undefined) v4Fields.push(`${id}.active_phases`);
704
- if (data.reports_to !== undefined) v4Fields.push(`${id}.reports_to`);
705
- if (data.teammate !== undefined) v4Fields.push(`${id}.teammate`);
706
- if (!data.title) missingV5Fields.push(`${id}.title`);
707
- if (!Array.isArray(data.domains)) missingV5Fields.push(`${id}.domains`);
708
- if (typeof data.persona !== 'string' || !data.persona) missingV5Fields.push(`${id}.persona`);
709
- }
710
-
711
- if (isV5) {
712
- checks.push({ name: `agents.json schema (v${version})`, status: 'ok' });
713
- } else {
714
- checks.push({
715
- name: `agents.json schema (v${version})`,
716
- status: 'warn',
717
- msg: 'expected v5.x — run agents migration'
718
- });
719
- hasWarnings = true;
720
- }
721
-
722
- if (v4Fields.length > 0) {
723
- const sample = v4Fields.slice(0, 3).join(', ');
724
- checks.push({
725
- name: 'agents.json deprecated v4 fields',
726
- status: 'warn',
727
- msg: `${v4Fields.length} v4 field(s) found: ${sample}${v4Fields.length > 3 ? '...' : ''}`
728
- });
729
- hasWarnings = true;
730
- }
731
-
732
- if (missingV5Fields.length > 0 && isV5) {
733
- const sample = missingV5Fields.slice(0, 3).join(', ');
734
- checks.push({
735
- name: 'agents.json v5 required fields',
736
- status: 'warn',
737
- msg: `${missingV5Fields.length} missing: ${sample}${missingV5Fields.length > 3 ? '...' : ''}`
738
- });
739
- hasWarnings = true;
784
+ for (const c of validateAgentsJson(agentsData)) {
785
+ checks.push(c);
786
+ if (c.status === 'error') hasErrors = true;
787
+ else if (c.status === 'warn') hasWarnings = true;
740
788
  }
741
789
  } catch {
742
790
  checks.push({ name: 'agents.json', status: 'error', msg: 'invalid JSON' });
@@ -256,6 +256,31 @@ export async function approveCommand(featureName, gateName, options = {}) {
256
256
  }
257
257
  }
258
258
 
259
+ // Skipped-pause backstop: a gate whose recorded decision demanded a human
260
+ // pause (gateDecisions[gate].pause === true) cannot then be stamped by a
261
+ // non-human signature. The detection already existed — in gate-guard.js —
262
+ // but that hook is registered for `Write|Edit|AskUserQuestion`, and a Bash
263
+ // tool call fires no hook at all. Since MORPH.md requires gate approval to
264
+ // go through THIS command and forbids hand-editing the state, the only path
265
+ // the hook could ever see was the one the charter bans. The check belongs
266
+ // where the stamp is applied, not beside it.
267
+ //
268
+ // Gated on the RESOLVED `approvedBy`, matching the hotfix backstop above:
269
+ // an omitted --mode in a headless context also resolves to "auto" and must
270
+ // not slip through. `--force` does NOT bypass this either — it is the
271
+ // out-of-order recovery flag, a different concern. The way out is a human
272
+ // signature, or rewriting the decision that asked for the pause.
273
+ const decision = feature.gateDecisions?.[gateName];
274
+ if (decision?.pause === true && approvedBy === 'auto') {
275
+ console.error(chalk.red(`\n❌ O Gate "${gateName}" registrou pause:true — auto-aprovação não se aplica.`));
276
+ console.error(chalk.gray(` Sinal: ${decision.reason || 'risco detectado'}`));
277
+ console.error(chalk.gray(' A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num'));
278
+ console.error(chalk.gray(' terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).'));
279
+ console.error(chalk.gray(' Se o sinal de risco já não vale, regrave a decisão:'));
280
+ console.error(chalk.gray(` morph-spec gate-decision ${featureName} ${gateName} --pause false --reason "<motivo>"\n`));
281
+ process.exit(1);
282
+ }
283
+
259
284
  let selfAssessed = false;
260
285
  if (gateName === 'review') {
261
286
  const evaluatorReport = join(process.cwd(), '.morph', 'features', featureName, '4-review', 'evaluator-report.md');
@@ -120,24 +120,6 @@ export function getAbsoluteOutputPath(projectRoot, featureName, outputType) {
120
120
  return join(projectRoot, '.morph', 'features', featureName, entry.phaseDir, leafSegment(entry));
121
121
  }
122
122
 
123
- /**
124
- * Human-readable path of a schema entry, relative to the feature directory.
125
- *
126
- * SINGLE source for the `morph-outputs` tables: `scripts/generate-refs.js`
127
- * writes them and `test/framework/refs.test.js` verifies them, and if the two
128
- * rebuilt the string independently they would drift apart the first time the
129
- * schema grew a new shape. Multi-file types render the filename PATTERN, so a
130
- * reader sees it's a folder of N files.
131
- *
132
- * @param {Object} entry - an OUTPUT_SCHEMA value
133
- * @returns {string} e.g. "2-plan/spec.md" or "2-plan/usecases/UC-{nn}-{slug}.md"
134
- */
135
- export function getSchemaDisplayPath(entry) {
136
- return entry.multi
137
- ? `${entry.phaseDir}/${entry.dirname}/${entry.displayFilename}`
138
- : `${entry.phaseDir}/${entry.filename}`;
139
- }
140
-
141
123
  /**
142
124
  * Get the feature base directory (morph-relative, forward slashes).
143
125
  */