@polymorphism-tech/morph-spec 9.2.0 → 9.3.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.
Files changed (53) hide show
  1. package/CLAUDE.md +14 -78
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +7 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -3
  8. package/framework/MORPH.md +5 -8
  9. package/framework/agents.json +9 -6
  10. package/framework/commands/morph-status.md +2 -2
  11. package/framework/docs/CLI.md +8 -2
  12. package/framework/evals/review-checklist.md +3 -0
  13. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  14. package/framework/hooks/claude-code/core/worktree-context.js +23 -0
  15. package/framework/hooks/claude-code/statusline.py +3 -2
  16. package/framework/hooks/shared/gate-event.js +18 -0
  17. package/framework/hooks/shared/next-step.js +107 -0
  18. package/framework/hooks/shared/slice-progress.js +18 -5
  19. package/framework/hooks/shared/state-writer.js +6 -1
  20. package/framework/mcp/registry.json +31 -1
  21. package/framework/schemas/feature.schema.json +1 -0
  22. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  23. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  24. package/framework/skills/core/morph-apply/SKILL.md +4 -2
  25. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  26. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  27. package/framework/skills/core/morph-media/SKILL.md +1 -1
  28. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  29. package/framework/skills/core/morph-review/SKILL.md +1 -1
  30. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  31. package/framework/skills/core/morph-uiux/SKILL.md +2 -1
  32. package/framework/standards/STANDARDS.meta.json +1 -1
  33. package/framework/standards/ai-agents/setup.md +2 -0
  34. package/framework/standards/integration/mcp/mcp-tools.md +3 -1
  35. package/package.json +1 -1
  36. package/src/commands/observability/cost.js +12 -0
  37. package/src/commands/project/status.js +6 -68
  38. package/src/commands/state/advance.js +1 -1
  39. package/src/commands/state/gate-check.js +2 -2
  40. package/src/commands/state/gate-decision.js +4 -1
  41. package/src/commands/state/next.js +24 -0
  42. package/src/commands/state/report-check.js +3 -1
  43. package/src/commands/validation/verify.js +6 -1
  44. package/src/core/state/state-manager.js +4 -0
  45. package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
  46. package/src/dashboard/scanner/features-scanner.js +5 -2
  47. package/src/lib/generators/handoff-generator.js +11 -25
  48. package/src/lib/next-step.js +10 -0
  49. package/src/lib/risk-detector.js +18 -7
  50. package/src/lib/stack/stack-scan.js +13 -1
  51. package/src/lib/telemetry/metrics.js +46 -0
  52. package/src/lib/validators/verify-runner.js +1 -1
  53. package/src/utils/agents-installer.js +23 -0
@@ -10,6 +10,7 @@ import {
10
10
  getPendingGates,
11
11
  } from '../../shared/state-reader.js';
12
12
  import { sliceProgress } from '../../shared/slice-progress.js';
13
+ import { collectNextSignals, decideNext, formatNext } from '../../shared/next-step.js';
13
14
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
14
15
  import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
15
16
  import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
@@ -133,11 +134,31 @@ function buildHandoffLine(dir, feature) {
133
134
  }
134
135
  }
135
136
 
137
+ // No git here, so verify staleness is unknown: an escalation the tree already moved past, or the
138
+ // root skeleton of a feature living in a worktree, would be an order read from stale facts.
139
+ function buildNextLine(dir, feature) {
140
+ try {
141
+ const signals = collectNextSignals(feature, dir);
142
+ if (!signals) return null;
143
+ if (signals.worktreePath && !samePath(resolve(signals.worktreePath), resolve(dir))) {
144
+ return `▶ ${feature} vive no worktree ${signals.worktreePath}: abra a sessão lá (aqui é o esqueleto de nascimento).`;
145
+ }
146
+ const next = decideNext(signals);
147
+ if (next.action === 'escalate') return `▶ verify vermelho de novo: rode \`morph-spec next ${feature}\` antes de agir.`;
148
+ return `▶ ${formatNext(next)} (confirme com \`morph-spec next ${feature}\` depois de mudar a árvore)`;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
136
154
  function buildWorktreeContext(dir, feature) {
137
155
  const phase = getFeaturePhase(feature, dir);
138
156
  let text = `🌳 Worktree desta feature: ${feature} (branch morph/${feature}, fase ${phase}). ` +
139
157
  `Trabalhe apenas nesta feature nesta sessão; o estado vive no .morph/ deste worktree.`;
140
158
 
159
+ const nextLine = buildNextLine(dir, feature);
160
+ if (nextLine) text += `\n${nextLine}`;
161
+
141
162
  const envLine = buildEnvLine(dir);
142
163
  if (envLine) text += `\n${envLine}`;
143
164
 
@@ -173,6 +194,8 @@ function buildActiveFeatureSummary(dir) {
173
194
  if (pending.length) {
174
195
  text += ` · gates pendentes: ${pending.join(', ')}`;
175
196
  }
197
+ const nextLine = buildNextLine(dir, active.name);
198
+ if (nextLine) text += `\n${nextLine}`;
176
199
  return text;
177
200
  }
178
201
 
@@ -390,13 +390,14 @@ def derive_phase(feature_dir, gates):
390
390
 
391
391
 
392
392
  PARTIAL_MARKER = re.compile(r'^[\s#>*_]*PARCIAL\b')
393
+ BLOCKED_MARKER = re.compile(r'^[\s#>*_]*BLOQUEADO\b')
393
394
 
394
395
 
395
396
  def count_slices(feature_dir):
396
397
  """(done, total) of the slices in 2-plan/checks.json.
397
398
 
398
399
  Same rule as framework/hooks/shared/slice-progress.js: a slice is done when
399
- 3-implement/reports/{id}.md exists and its first line is not PARCIAL.
400
+ 3-implement/reports/{id}.md exists and its first line is neither PARCIAL nor BLOQUEADO.
400
401
  """
401
402
  try:
402
403
  slices = json.loads((feature_dir / '2-plan' / 'checks.json').read_text(encoding='utf-8')).get('slices')
@@ -412,7 +413,7 @@ def count_slices(feature_dir):
412
413
  except Exception:
413
414
  continue
414
415
  first = next((line for line in text.splitlines() if line.strip()), '')
415
- if not PARTIAL_MARKER.match(first):
416
+ if not PARTIAL_MARKER.match(first) and not BLOCKED_MARKER.match(first):
416
417
  done += 1
417
418
  return done, len(ids)
418
419
 
@@ -0,0 +1,18 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { appendTelemetryEvent } from './telemetry-logger.js';
4
+
5
+ export const GATE_EVENT = 'Gate';
6
+ export const GATE_STEPS = ['decision', 'approve', 'unapprove'];
7
+
8
+ /**
9
+ * The trail `cost --metrics` turns into "what did the human do with each pause":
10
+ * `gateDecisions` keeps only the latest decision, so the history lives here.
11
+ */
12
+ export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, approvedBy }) {
13
+ if (!existsSync(join(projectPath || process.cwd(), '.morph'))) return;
14
+ const event = { event: GATE_EVENT, feature, gate, step };
15
+ if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null });
16
+ else event.approvedBy = approvedBy ?? null;
17
+ appendTelemetryEvent(event, projectPath);
18
+ }
@@ -0,0 +1,107 @@
1
+ // The single rule of "what the session does next". `decideNext` is pure: collectors feed facts.
2
+ // Hooks and the CLI share it so the SessionStart pre-fetch and `morph-spec next` never disagree.
3
+
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { derivePhaseForFeature } from './state-reader.js';
7
+ import { sliceProgress, readySlices } from './slice-progress.js';
8
+
9
+ export const FIX_ATTEMPTS = 3;
10
+
11
+ const step = (action, command, reason, extra = {}) => ({ action, command, reason, ...extra });
12
+
13
+ /**
14
+ * @param {{ feature: string, phase: string, slices: object|null, gates: object,
15
+ * verification: { status?: string }|null, verificationStale?: boolean, failStreak?: number }} s
16
+ * `verificationStale` undefined = unknown (the hook has no git): read as fresh.
17
+ */
18
+ export function decideNext(s) {
19
+ const f = s.feature;
20
+ const approved = (g) => s.gates?.[g]?.approved === true;
21
+ // The hotfix pipeline (scout, human approval, ship) lives in its own skill, end to end.
22
+ const skill = (name) => (s.workType === 'hotfix' ? `/morph-hotfix ${f}` : `/${name} ${f}`);
23
+
24
+ switch (s.phase) {
25
+ case 'plan':
26
+ return s.slices
27
+ ? step('gate', `morph-spec gate-check ${f} plan`, 'plano extraído: o gate do plano decide a pausa')
28
+ : step('plan', `/morph-plan ${f}`, 'sem checks.json: escreva o plan.md e rode plan-check até zero erro');
29
+
30
+ case 'uiux':
31
+ return step('design', `morph-spec gate-check ${f} uiux`, 'complete 1-design/ (morph-uiux) e passe o gate de uiux');
32
+
33
+ case 'implement': {
34
+ if (!s.slices) return step('plan', skill('morph-plan'), 'sem checks.json: plan.md curto (uma fatia) e plan-check até zero erro');
35
+ if (s.slices.done < s.slices.total) {
36
+ const ready = readySlices(s.slices);
37
+ if (!ready.length) {
38
+ const waiting = s.slices.slices.filter((sl) => sl.status === 'bloqueado').map((sl) => sl.id);
39
+ if (waiting.length) {
40
+ return step('decide', 'AskUserQuestion (decisão da fatia)',
41
+ 'relatório BLOQUEADO: decida pelo plano/ADR ou pergunte, e retome o MESMO agente com a resposta — nunca re-despache',
42
+ { slices: waiting });
43
+ }
44
+ return step('blocked', `morph-spec report-check ${f}`, 'fatias pendentes sem dependência satisfeita');
45
+ }
46
+ const partial = ready.filter((sl) => sl.status === 'parcial').map((sl) => sl.id);
47
+ return step('dispatch', skill('morph-apply'),
48
+ partial.length ? `retome ${partial.join(', ')} (PARCIAL) e despache as prontas` : 'despache as fatias prontas',
49
+ { slices: ready.map((sl) => sl.id) });
50
+ }
51
+ const v = s.verification;
52
+ if (!v?.status || s.verificationStale === true) {
53
+ return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : 'todas as fatias relatadas, sem verify');
54
+ }
55
+ if (v.status === 'fail') {
56
+ const streak = s.failStreak ?? 0;
57
+ if (streak > 0 && streak % FIX_ATTEMPTS === 0) {
58
+ return step('escalate', 'AskUserQuestion (aceitar/continuar/parar)',
59
+ `${streak} verifies vermelhos seguidos, nunca uma tentativa a mais por conta própria. `
60
+ + `aceitar → ${skill('morph-review')} (o verify vermelho pausa o gate: o humano assina lá); `
61
+ + 'continuar → corrija e rode o verify de novo; parar → encerre aqui. Segue `escalate` até a árvore andar');
62
+ }
63
+ return step('fix', `morph-spec verify ${f}`,
64
+ `corrija pelo feedback do verify e rode de novo (${FIX_ATTEMPTS - (streak % FIX_ATTEMPTS)} tentativa(s) antes de perguntar)`);
65
+ }
66
+ return step('review', skill('morph-review'), 'verify verde: Gate 3');
67
+ }
68
+
69
+ case 'review':
70
+ return approved('review')
71
+ ? step('finish', `morph-spec finish ${f} --pr|--merge`, 'Gate 3 aprovado: arquiva e integra num passo')
72
+ : step('review', skill('morph-review'), 'relatórios do Gate 3 e gate-check review');
73
+
74
+ default:
75
+ return step('status', `morph-spec status ${f}`, `fase desconhecida: ${s.phase ?? '—'}`);
76
+ }
77
+ }
78
+
79
+ export function formatNext(n) {
80
+ const slices = n.slices?.length ? ` [${n.slices.join(', ')}]` : '';
81
+ return `próximo: ${n.action}${slices} → ${n.command} — ${n.reason}`;
82
+ }
83
+
84
+ /** Facts readable from `hooks/shared`; the CLI adds `verificationStale` (git) on top. Null without feature.json. */
85
+ export function collectNextSignals(featureName, cwd) {
86
+ const featureDir = join(cwd, '.morph', 'features', featureName);
87
+ let feature;
88
+ try {
89
+ const path = join(featureDir, 'feature.json');
90
+ if (!existsSync(path)) return null;
91
+ feature = JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
92
+ } catch {
93
+ return null;
94
+ }
95
+ let phase = 'plan';
96
+ try { phase = derivePhaseForFeature(featureName, cwd) || 'plan'; } catch {}
97
+ return {
98
+ feature: featureName,
99
+ workType: feature.workType ?? 'feature',
100
+ worktreePath: feature.worktree?.path ?? null,
101
+ phase,
102
+ slices: sliceProgress(featureDir),
103
+ gates: feature.approvals || feature.approvalGates || {},
104
+ verification: feature.verification ?? null,
105
+ failStreak: feature.verifications?.feature?.failStreak ?? 0,
106
+ };
107
+ }
@@ -4,7 +4,10 @@ import { join } from 'path';
4
4
  /** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
5
5
  export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
6
6
 
7
- export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
7
+ /** `BLOQUEADO`: the slice waits on a decision only the human can make; re-dispatching it repeats the question. */
8
+ export const BLOCKED_MARKER = /^[\s#>*_]*BLOQUEADO\b/;
9
+
10
+ export const SLICE_STATUSES = ['pendente', 'parcial', 'bloqueado', 'feito'];
8
11
 
9
12
  export function checksPath(featureDir) {
10
13
  return join(featureDir, '2-plan', 'checks.json');
@@ -17,10 +20,11 @@ export function sliceReportPath(featureDir, sliceId) {
17
20
  export function sliceStatusFromReport(text) {
18
21
  if (text == null) return 'pendente';
19
22
  const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
23
+ if (BLOCKED_MARKER.test(first)) return 'bloqueado';
20
24
  return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
21
25
  }
22
26
 
23
- /** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
27
+ /** @returns {{ total, done, partial, blocked, pending, slices } | null} null when the checks text is unreadable */
24
28
  export function sliceProgressFrom(checksText, readReport) {
25
29
  let slices;
26
30
  try {
@@ -39,7 +43,14 @@ export function sliceProgressFrom(checksText, readReport) {
39
43
  status: sliceStatusFromReport(readReport(s.id)),
40
44
  }));
41
45
  const count = (status) => out.filter((s) => s.status === status).length;
42
- return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
46
+ return {
47
+ total: out.length,
48
+ done: count('feito'),
49
+ partial: count('parcial'),
50
+ blocked: count('bloqueado'),
51
+ pending: count('pendente'),
52
+ slices: out,
53
+ };
43
54
  }
44
55
 
45
56
  function readOrNull(path) {
@@ -57,7 +68,7 @@ export function sliceProgress(featureDir) {
57
68
  export function readySlices(progress) {
58
69
  if (!progress) return [];
59
70
  const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
60
- return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
71
+ return progress.slices.filter((s) => s.status !== 'feito' && s.status !== 'bloqueado' && s.depends.every((d) => done.has(d)));
61
72
  }
62
73
 
63
74
  // Loose on purpose: a false "complete" costs a read, a false "incomplete"
@@ -94,6 +105,8 @@ export function inspectReport(path, { covers = [] } = {}) {
94
105
  const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
95
106
  const partial = PARTIAL_MARKER.test(firstLine);
96
107
  if (partial) missing.push('partial');
108
+ const blocked = BLOCKED_MARKER.test(firstLine);
109
+ if (blocked) missing.push('blocked');
97
110
 
98
- return { exists: true, complete: missing.length === 0, missing, partial };
111
+ return { exists: true, complete: missing.length === 0, missing, partial, blocked };
99
112
  }
@@ -4,6 +4,7 @@
4
4
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
5
5
  import { join, dirname } from 'path';
6
6
  import { clearStateCache } from './state-reader.js';
7
+ import { recordGateEvent } from './gate-event.js';
7
8
 
8
9
  const STATE_FILE = '.morph/state.json';
9
10
  const STATE_VERSION = '5.0.0';
@@ -81,10 +82,14 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
81
82
  ...(meta.selfAssessed ? { selfAssessed: true } : {}),
82
83
  };
83
84
  feature.updatedAt = now;
84
- return persistFeature(featureName, feature, projectPath);
85
+ if (!persistFeature(featureName, feature, projectPath)) return false;
85
86
  } catch {
86
87
  return false;
87
88
  }
89
+ try {
90
+ recordGateEvent(projectPath, { feature: featureName, gate, step: 'approve', approvedBy: meta.approvedBy || 'manual' });
91
+ } catch {}
92
+ return true;
88
93
  }
89
94
 
90
95
  // Stamps identity so a feature born implicitly by a hook is never an anonymous
@@ -277,9 +277,39 @@
277
277
  "warnings": ["OAuth against your Neon account on first use. The server can delete branches and projects: keep destructive calls behind a human."],
278
278
  "setupGuide": "Run: claude mcp add --transport http neon https://mcp.neon.tech/mcp"
279
279
  }
280
+ },
281
+ "microsoft-learn": {
282
+ "phases": ["plan", "implement"],
283
+ "tools": ["microsoft_docs_search", "microsoft_docs_fetch", "microsoft_code_sample_search"],
284
+ "serverNames": ["microsoft-learn", "plugin_microsoft-docs_microsoft-learn"],
285
+ "detection": "mcpServers includes microsoft-learn or enabledPlugins includes microsoft-docs",
286
+ "fallback": "context7, then WebFetch on learn.microsoft.com",
287
+ "usage": "Official, current .NET 10 / ASP.NET Core / EF Core / Microsoft Agent Framework docs and code samples: search first, fetch the page when the excerpt is cut. Use it for any API the model may know from an older version",
288
+ "healthCheck": {
289
+ "toolPattern": "microsoft-learn",
290
+ "testCall": "Call microsoft_docs_search with query 'EF Core 10 what is new'",
291
+ "testParams": {
292
+ "query": "EF Core 10 what is new"
293
+ },
294
+ "successIndicator": "Returns Microsoft Learn excerpts with contentUrl",
295
+ "failureHint": "Microsoft Learn MCP not responding. Re-add it: claude mcp add --transport http microsoft-learn https://learn.microsoft.com/api/mcp"
296
+ },
297
+ "install": {
298
+ "autoInstall": false,
299
+ "transport": "http",
300
+ "config": {
301
+ "type": "http",
302
+ "url": "https://learn.microsoft.com/api/mcp"
303
+ },
304
+ "credentials": [],
305
+ "prerequisites": [],
306
+ "stackRelevance": ["dotnet"],
307
+ "warnings": [],
308
+ "setupGuide": "Run: claude mcp add --transport http microsoft-learn https://learn.microsoft.com/api/mcp"
309
+ }
280
310
  }
281
311
  },
282
- "_phaseMatrixNote": "Keys must be the v5 phases from framework/phases.json. The v4 keys 'design' and 'tasks' both collapsed into 'plan'; 'review' was absent entirely, so a project in review resolved to undefined. Stack-scoped MCPs (asaas, neon) stay out of this matrix on purpose — only universal ones are listed.",
312
+ "_phaseMatrixNote": "Keys must be the v5 phases from framework/phases.json. The v4 keys 'design' and 'tasks' both collapsed into 'plan'; 'review' was absent entirely, so a project in review resolved to undefined. Stack-scoped MCPs (asaas, neon, microsoft-learn) stay out of this matrix on purpose — only universal ones are listed.",
283
313
  "phaseMatrix": {
284
314
  "plan": ["context7", "graphify"],
285
315
  "uiux": ["playwright", "context7", "stitch", "morph-assets"],
@@ -95,6 +95,7 @@
95
95
  "additionalProperties": false
96
96
  },
97
97
  "attempts": { "type": "integer", "minimum": 1 },
98
+ "failStreak": { "type": "integer", "minimum": 0, "description": "Persisted red verifies in a row, reset to 0 by a green one. Read by `morph-spec next` (decideNext in framework/hooks/shared/next-step.js) to turn the fix loop into an escalation every FIX_ATTEMPTS reds. Absent on entries written before the field existed, which is why it is NOT in required." },
98
99
  "updatedAt": { "type": "string", "format": "date-time" }
99
100
  },
100
101
  "additionalProperties": false
@@ -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: "9.2.0"
7
+ cliVersion: "9.3.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: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Executa as fatias do plano de uma feature morph-spec — dispatch p
4
4
  version: core
5
5
  when_to_use: Use com o gate do plano aprovado, em "use /morph-apply {feature}" ou "implementar feature"
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -108,6 +108,7 @@ relatório, não a mensagem** — a mensagem final pode não chegar ou vir resum
108
108
  | `ok` | siga |
109
109
  | `report-stale` | **não re-despache**: o agente escreveu depois de relatar; leia `observed` e atualize o relatório |
110
110
  | `report-incomplete` + `resume` | `PARCIAL` na 1ª linha = a fatia não está pronta — o `report-check` o acusa como `report-incomplete`. Retome o MESMO agente por `SendMessage`, colando `tree.dirty`, `tree.commits` e o que falta |
111
+ | `report-blocked` + `ask-human` | `BLOQUEADO` na 1ª linha = decisão que não é do agente; a pergunta e as opções estão no relatório. **Não re-despache.** Plano ou ADR já decide → decida e registre; senão `AskUserQuestion`. Retome o MESMO agente com a resposta |
111
112
  | `report-incomplete` + `complete-degraded` | complete a partir da mensagem, carimbe DEGRADADO e registre a falha no relatório |
112
113
  | `work-landed-report-missing` | não re-despache; grave você o relatório, DEGRADADO |
113
114
  | `no-work-no-report` / `report-missing-evidence-unknown` | re-despache uma vez; segunda ausência sem trabalho = escale |
@@ -131,7 +132,8 @@ verify da feature. `--filter` é do loop do próprio sub-agente; o Gate 3 e o CI
131
132
  inteira. Nó de teste sem `expectedCount`, ou fatia que apagou teste de propósito (limpeza, `morph-spec tests-audit`): acrescente `--record-baseline` a ESTA rodada — ela mede e
132
133
  prova de uma vez; nunca um segundo verify só para gravar a baseline. Vermelho: corrija com `verify.feedback` (direto no código, ou re-despachando a fatia dona)
133
134
  e rode de novo; **3 tentativas** sem verde → `AskUserQuestion` ("Verify de {feature} vermelho após 3
134
- tentativas — aceitar com ressalva, continuar tentando, ou parar?").
135
+ tentativas — aceitar com ressalva, continuar tentando, ou parar?"). Quem conta é o `feature.json`:
136
+ `morph-spec next {feature}` devolve `fix` enquanto há tentativa e `escalate` na 3ª vermelha seguida.
135
137
 
136
138
  **Cenário que julga saída de LLM** (`prova: eval`): a evidência é rodar, não ler código. Com nó
137
139
  `runner: "evals"`: `morph-spec verify {feature} --filter {id-do-nó-evals} --skip-e2e --no-persist`
@@ -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_use: Use quando chegar "/morph-hotfix {feature}", quando a morph-plan rotear o pedido como workType hotfix, ou quando o usuário disser "produção caiu / hotfix / urgente" a respeito de um incidente
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha
4
4
  version: core
5
5
  when_to_use: Use quando a máquina estiver lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
6
6
  user-invocable: false
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-machine
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
4
4
  version: core
5
5
  when_to_use: Use quando a tarefa for PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisar decidir modelo, prompt e aprovação
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs
@@ -4,7 +4,7 @@ description: Leva uma feature do pedido ao plano aprovado — roteia o tipo de t
4
4
  version: core
5
5
  when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -4,7 +4,7 @@ description: Conduz o Gate 3 — despacha verificador ∥ revisor (∥ seguranç
4
4
  version: core
5
5
  when_to_use: Use quando a fase review começar, com todas as fatias com relatório final e o verify da feature verde
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when_to_use: Use quando precisar consultar um standard específico durante a implementação
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX de uma feature com plano aprovado
4
4
  version: core
5
5
  when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design, depois do gate do plano) e antes de /morph-apply
6
6
  user-invocable: true
7
- cliVersion: "9.2.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -22,6 +22,7 @@ Entra depois do gate do plano, quando a `morph-plan` §6 decidiu que a feature c
22
22
 
23
23
  - WCAG AA e todos os estados de componente interativo (default/hover/focus/disabled/loading/error), não só o caminho feliz.
24
24
  - Reusa token e componente existentes antes de criar; token novo precisa de justificativa. Sistemas, não telas isoladas.
25
+ - Tarefa principal da tela em até 3 interações, uma ação primária por tela, voltar/cancelar sempre à vista. Erro diz o que fazer a seguir; conteúdo gerado por IA aparece marcado como tal.
25
26
  - **Toda tela sai com o checklist de tela preenchido.** O `advance --design` já escreveu `1-design/mockups.md` a partir do template do pacote, a fonte única dos dez itens (tabela `### Checklist de tela` dentro de cada `## Tela:`). "Não se aplica" vale se escrito; `…`, vazio ou "a definir" reprovam. Design sem tela nova ou alterada (só componente ou estilo): apague o `1-design/mockups.md` semeado. É o checklist que faz a lista de 2 000 linhas paginar: o verificador do Gate 3 cobra cada item decidido em `## Checklist de tela`. Tela de dados segue os standards de `admin-app` (lista/tabela, formulário, feedback, estrutura de página); login, onboarding, primeira tela, estado vazio de destaque e sucesso são `brand-moment`.
26
27
  - **Conformidade não basta: a régua estética vale junto.** Direção escrita antes de componente (`1-design/direcao.md`: conceito, metáfora, o que torna a tela memorável, o que ela não pode parecer; 2 ou 3 direções para tela nova ou momento de marca), celular desenhado e não derivado, movimento com versão estática digna. A régua é o standard `frontend-design-system-art-direction`.
27
28
  - **Toda tela nova ou alterada tem protótipo renderizado e conferido por print.** `1-design/prototipos/{tela}.html` autossuficiente, com os tokens reais do projeto (lidos do CSS de tokens), desktop e celular lado a lado, controles animado/estático/pausar; prints `{tela}-1280.png` e `{tela}-390.png` ao lado, saídos do laço de autocrítica. A `## Tela:` aponta o protótipo na linha `**Protótipo:**`.
@@ -1123,7 +1123,7 @@
1123
1123
  "MCP server",
1124
1124
  "MCP client"
1125
1125
  ],
1126
- "verifiedAgainst": "Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`.",
1126
+ "verifiedAgainst": "Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`; Microsoft Learn MCP (`https://learn.microsoft.com/api/mcp`, tools listed by `tools/list` on 2026-10-17).",
1127
1127
  "provedBy": []
1128
1128
  },
1129
1129
  "frontend-admin-app-data-table": {
@@ -331,6 +331,8 @@ AIAgent instrumented = agent.AsBuilder()
331
331
 
332
332
  ## 7. What NOT to do (anti-patterns)
333
333
 
334
+ `morph-spec stack-scan` cobra três linhas desta tabela no código: Semantic Kernel, hosted agent service e versão flutuante.
335
+
334
336
  | Wrong | Right |
335
337
  |-------|-------|
336
338
  | `using Microsoft.SemanticKernel` | `using Microsoft.Agents.AI` |
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** mcp, model-context-protocol, mcp-tools, context7, playwright, mcp-client
6
6
  > **Load When:** an MCP tool is used in any morph-spec phase
7
7
 
8
- **Verified against:** Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`. Last-verified: 2026-09-24.
8
+ **Verified against:** Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`; Microsoft Learn MCP (`https://learn.microsoft.com/api/mcp`, tools listed by `tools/list` on 2026-10-17). Last-verified: 2026-10-17.
9
9
 
10
10
  ---
11
11
 
@@ -138,6 +138,7 @@ Fallback: manual image creation, stock photos, or placeholder divs.
138
138
  |------|----------|---------|
139
139
  | **Neon CLI (`neonctl`) / psql** | Database schema, tables, relationships, RLS | `psql <DB_URL> -c "SELECT ..."` |
140
140
  | **Context7** | Library docs for architecture decisions | `mcp__context7__query-docs()` |
141
+ | **Microsoft Learn** (stack .NET) | Official docs for a .NET/EF Core/MAF decision (limits, what is new, migration) | `mcp__microsoft-learn__microsoft_docs_search({ query })` |
141
142
  | **GitHub CLI (`gh`)** | Check existing code patterns, PRs | `gh search code` (Bash) |
142
143
 
143
144
  ```bash
@@ -213,6 +214,7 @@ gh issue create --title "F1: Create Entity Lead" \
213
214
  | **Neon CLI (`neonctl`) / psql** | Run migrations, create RLS policies | `dotnet ef database update` |
214
215
  | **GitHub CLI (`gh`)** | Create PR, push branches | `gh pr create` (Bash) |
215
216
  | **Context7** | Look up API usage during coding | `mcp__context7__query-docs()` |
217
+ | **Microsoft Learn** (stack .NET) | Current .NET 10 / EF Core / ASP.NET Core / MAF API: search, then fetch the page when the excerpt is cut | `mcp__microsoft-learn__microsoft_docs_search({ query })` → `microsoft_docs_fetch({ url })`; `microsoft_code_sample_search` for snippets |
216
218
  | **Playwright** | Smoke test deployed features, verify UI | `mcp__playwright__browser_navigate()` |
217
219
  | **Vercel** | Deploy, check logs, manage env vars | `mcp__vercel__list_projects()` |
218
220
  | **Azure CLI** | Resource management, deployments | `az` commands |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -145,6 +145,18 @@ function printMetricsBody(m) {
145
145
  ? trees.map(([tree, s]) => `${tree} rodou ${s.ran}, pulou ${s.skipped}`).join(' · ')
146
146
  : 'nenhum evento Verify na janela'}`);
147
147
  for (const [reason, n] of Object.entries(m.reuse.skipReasons)) console.log(chalk.gray(` pulou ${n}×: ${reason}`));
148
+ const gates = Object.entries(m.gates);
149
+ if (!gates.length) {
150
+ console.log(' gates: nenhum evento Gate na janela');
151
+ return;
152
+ }
153
+ for (const [gate, g] of gates) {
154
+ console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas → ${g.approvedAfterPause} aprovadas como estavam, `
155
+ + `${g.reworkedAfterPause} retrabalhadas${g.unapproved ? `, ${g.unapproved} desaprovações` : ''}`);
156
+ for (const [signal, s] of Object.entries(g.bySignal)) {
157
+ console.log(chalk.gray(` ${signal}: ${s.paused} pausas, ${s.approvedAfterPause} aprovadas como estavam, ${s.reworkedAfterPause} retrabalhadas`));
158
+ }
159
+ }
148
160
  }
149
161
 
150
162
  function printMetrics(feature, options, cwd) {