@polymorphism-tech/morph-spec 9.2.0 → 9.4.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 (79) 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 +14 -9
  10. package/framework/commands/morph-status.md +2 -2
  11. package/framework/docs/CLI.md +8 -2
  12. package/framework/evals/review-checklist.md +4 -1
  13. package/framework/hooks/README.md +1 -0
  14. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
  16. package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
  17. package/framework/hooks/claude-code/core/worktree-context.js +23 -0
  18. package/framework/hooks/claude-code/statusline.py +3 -2
  19. package/framework/hooks/shared/command-tokens.js +17 -0
  20. package/framework/hooks/shared/gate-event.js +18 -0
  21. package/framework/hooks/shared/next-step.js +107 -0
  22. package/framework/hooks/shared/slice-progress.js +18 -5
  23. package/framework/hooks/shared/state-writer.js +6 -1
  24. package/framework/mcp/registry.json +31 -1
  25. package/framework/rules/csharp-standards.md +5 -3
  26. package/framework/rules/dotnet-templates.md +2 -2
  27. package/framework/rules/nextjs-standards.md +1 -0
  28. package/framework/schemas/feature.schema.json +1 -0
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  30. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  31. package/framework/skills/core/morph-apply/SKILL.md +4 -2
  32. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  33. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  34. package/framework/skills/core/morph-media/SKILL.md +1 -1
  35. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  36. package/framework/skills/core/morph-review/SKILL.md +1 -1
  37. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  38. package/framework/skills/core/morph-uiux/SKILL.md +2 -1
  39. package/framework/standards/STANDARDS.json +55 -8
  40. package/framework/standards/STANDARDS.meta.json +29 -1
  41. package/framework/standards/ai-agents/setup.md +2 -0
  42. package/framework/standards/architecture/engineering-lens.md +100 -0
  43. package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
  44. package/framework/standards/backend/dotnet/analyzers.md +59 -0
  45. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
  46. package/framework/standards/integration/mcp/mcp-tools.md +3 -1
  47. package/framework/templates/README.md +0 -1
  48. package/framework/templates/REGISTRY.json +0 -207
  49. package/package.json +1 -1
  50. package/src/commands/observability/cost.js +12 -0
  51. package/src/commands/project/status.js +6 -68
  52. package/src/commands/project/worktree.js +1 -1
  53. package/src/commands/state/advance.js +1 -1
  54. package/src/commands/state/archive.js +1 -1
  55. package/src/commands/state/finish.js +1 -1
  56. package/src/commands/state/gate-check.js +2 -2
  57. package/src/commands/state/gate-decision.js +4 -1
  58. package/src/commands/state/next.js +24 -0
  59. package/src/commands/state/report-check.js +3 -1
  60. package/src/commands/validation/verify.js +6 -1
  61. package/src/core/state/state-manager.js +4 -0
  62. package/src/core/templates/template-registry.js +1 -43
  63. package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
  64. package/src/dashboard/scanner/features-scanner.js +5 -2
  65. package/src/lib/generators/handoff-generator.js +11 -25
  66. package/src/lib/next-step.js +10 -0
  67. package/src/lib/risk-detector.js +18 -7
  68. package/src/lib/stack/stack-scan.js +17 -7
  69. package/src/lib/telemetry/metrics.js +46 -0
  70. package/src/lib/validators/verify-runner.js +1 -1
  71. package/src/utils/agents-installer.js +24 -1
  72. package/src/utils/hooks-installer.js +1 -1
  73. package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
  74. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
  75. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
  76. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
  77. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
  78. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
@@ -34,7 +34,7 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
34
34
  | `teste` | verificador | teste vermelho; teste que não morde a regra que diz proteger | saída do verify, mutante sobrevivente, assert que passa pelo motivo errado |
35
35
  | `regressao` | revisor | comportamento que funcionava e quebrou | consumidor afetado + o caminho que quebra |
36
36
  | `escopo` | revisor | o diff faz o que nenhuma fatia pediu, ou deixa de fazer o que o contrato pede | arquivo/trecho fora das fatias, ou o item do contrato ausente |
37
- | `arquitetura` | revisor | padrão ou abstração nova contra o stack e os standards | o trecho e o standard que ele contraria |
37
+ | `arquitetura` | revisor | padrão ou abstração nova contra o stack e os standards; item da lista fechada de `architecture-engineering-lens#review-blockers` que o DIFF introduziu (o que já existia é nota) | o trecho e o standard que ele contraria |
38
38
  | `performance` | revisor | custo que o volume real torna visível | a consulta/lista e o volume que a derruba |
39
39
  | `seguranca` | segurança | authz, segredo, injeção, PII, custo sem teto, migration destrutiva | o caminho de exploração |
40
40
 
@@ -129,6 +129,9 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
129
129
  5. **Performance só com evidência**: lista sem paginação no servidor sobre volume real, N+1 visível.
130
130
  6. **Chamada a LLM**: dá para reconstruir o que foi ao provedor e o que ele respondeu neste turno?
131
131
  Não → nota.
132
+ 7. **Tela no diff**: cor ou espaçamento literal fora do CSS de tokens, mais de uma ação primária,
133
+ erro sem próxima ação, conteúdo gerado por IA sem marca, foco invisível → nota. Bloqueia só pelo
134
+ contrato de tela do verificador.
132
135
 
133
136
  ## Segurança (`4-review/security.md`)
134
137
 
@@ -25,6 +25,7 @@ typechecks or runs tests: that is `morph-spec verify`.
25
25
  | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to `plan.md` (and the uiux artifacts) after their gate is approved; an append-only `## Errata` block at the end of `plan.md` passes |
26
26
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
27
27
  | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
28
+ | **PreToolUse** (Bash) | conventional-commit.js | Block | Blocks a `git commit -m` whose subject is not Conventional Commits (`<type>(<scope>): <subject>`); a `$(…)`/heredoc message, `-F` and git-generated `Merge`/`Revert` pass |
28
29
  | **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
29
30
  | **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
30
31
  | **PreToolUse** (Bash) | warn-bash-source-write.js | Warn | Heredoc/redirect/`tee` into project source (`isProjectSource`): context naming the file, the command still runs — Write/Edit is what phase, protected-file and comment guards see |
@@ -3,23 +3,10 @@ import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
3
  import { stateExists } from '../../shared/state-reader.js';
4
4
  // From shared/, never from the sibling hook: importing a hook module fires its
5
5
  // standalone tail (stdin read + exit) inside THIS process.
6
- import { tokenize } from '../../shared/command-tokens.js';
7
-
8
- const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
6
+ import { gitInvocations } from '../../shared/command-tokens.js';
9
7
 
10
8
  export function isRawWorktreeAdd(command) {
11
- const tokens = tokenize(command);
12
- const lower = tokens.map((t) => t.toLowerCase());
13
- for (let i = 0; i < lower.length; i++) {
14
- if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
15
- let j = i + 1;
16
- while (j < lower.length && lower[j].startsWith('-')) {
17
- if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2;
18
- else j += 1;
19
- }
20
- if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
21
- }
22
- return false;
9
+ return gitInvocations(command).some(({ sub, args }) => sub === 'worktree' && args[0]?.toLowerCase() === 'add');
23
10
  }
24
11
 
25
12
  export async function run(payload) {
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
+ import { stateExists } from '../../shared/state-reader.js';
4
+ import { gitInvocations } from '../../shared/command-tokens.js';
5
+
6
+ const CONVENTIONAL = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|style|revert)(\([^)]+\))?!?: \S/;
7
+ const GIT_GENERATED = /^(Merge|Revert) /;
8
+ const MESSAGE_FLAG = /^-[a-zA-Z]*m$/;
9
+
10
+ function subjectOf(args) {
11
+ for (let i = 0; i < args.length; i++) {
12
+ const arg = args[i];
13
+ if (arg === '--message' || MESSAGE_FLAG.test(arg)) return args[i + 1] ?? null;
14
+ if (arg.startsWith('--message=')) return arg.slice('--message='.length);
15
+ if (/^-m./.test(arg)) return arg.slice(2);
16
+ }
17
+ return null;
18
+ }
19
+
20
+ // `$(…)` and heredocs are only known after the shell expands them.
21
+ function isJudgeable(subject) {
22
+ return subject !== null && !subject.includes('$(') && !subject.includes('<<') && !subject.includes('`');
23
+ }
24
+
25
+ export function nonConventionalSubject(command) {
26
+ for (const { sub, args } of gitInvocations(command)) {
27
+ if (sub !== 'commit') continue;
28
+ const subject = subjectOf(args);
29
+ if (!isJudgeable(subject)) continue;
30
+ const firstLine = subject.split('\n')[0].trim();
31
+ if (!CONVENTIONAL.test(firstLine) && !GIT_GENERATED.test(firstLine)) return firstLine;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export async function run(payload) {
37
+ try {
38
+ if (!stateExists()) return null;
39
+
40
+ const command = payload?.tool_input?.command || '';
41
+ if (!command.includes('commit')) return null;
42
+ const subject = nonConventionalSubject(command);
43
+ if (subject === null) return null;
44
+
45
+ return blockResult(
46
+ `Bloqueado: o assunto do commit não segue Conventional Commits: "${subject}".\n\n` +
47
+ 'Formato: `<tipo>(<escopo>): <assunto>` — tipo em feat, fix, refactor, test, chore ' +
48
+ '(também docs, perf, ci, build, style, revert); `!` antes de `:` marca breaking change.\n' +
49
+ 'Exemplos: `feat(orders): aceita cupom no checkout`, `fix: trata pedido sem itens`, ' +
50
+ '`chore(F2): wip handler de pagamento`.'
51
+ );
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ await runStandalone(run);
@@ -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
 
@@ -108,6 +108,23 @@ function simpleCommands(head) {
108
108
  return commands.filter((c) => c.length > 0);
109
109
  }
110
110
 
111
+ const GIT_EXECUTABLE = /(^|[\\/])git(\.exe)?$/i;
112
+ const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
113
+
114
+ /** Each `git <sub> …` the command runs, global options skipped, one `{ sub, args }` per simple command. */
115
+ export function gitInvocations(command) {
116
+ const invocations = [];
117
+ for (const tokens of simpleCommands(command)) {
118
+ const texts = tokens.map((t) => t.text);
119
+ const at = texts.findIndex((t) => GIT_EXECUTABLE.test(t));
120
+ if (at < 0) continue;
121
+ let j = at + 1;
122
+ while (j < texts.length && texts[j].startsWith('-')) j += GIT_GLOBAL_WITH_VALUE.has(texts[j]) ? 2 : 1;
123
+ if (j < texts.length) invocations.push({ sub: texts[j].toLowerCase(), args: texts.slice(j + 1) });
124
+ }
125
+ return invocations;
126
+ }
127
+
111
128
  function positionals(args, takesValue) {
112
129
  const out = [];
113
130
  const values = [];
@@ -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"],
@@ -8,13 +8,15 @@ stacks:
8
8
 
9
9
  # C# and .NET VSA Standards
10
10
 
11
- - **architecture-vertical-slice-vertical-slice** (`architecture/vertical-slice/vertical-slice.md`) — Uma slice por operação em `Features/{Entity}Feature/{Verb}{Entity}/`: Request/Response `sealed record`, Handler, Validator e Endpoint `IApiEndpoint` `internal sealed`. Decorators Scrutor: Logging → Validation → Handler. PROIBIDO: MediatR/`IRequest`, AggregateRoot, DomainEvent, CQRS formal, camada Application Service, DTO compartilhado entre features, `IRepository<T>`/`IUnitOfWork`.
12
- - **backend-dotnet-vsa-handler-patterns** (`backend/dotnet/vsa-handler-patterns.md`) — Handler `sealed class` com primary constructor implementando `IHandler<TRequest, TResponse>` (não MediatR): injeta `ApplicationDbContext` direto, usa `Guid.CreateVersion7()` (nunca `Guid.NewGuid()`), passa `CancellationToken` em tudo e devolve `Result<T>`. Erros em `{Feature}Errors.cs`. SQL cru ou store dedicado só com justificativa em `plan.md` (`## Decisões`).
11
+ - **architecture-vertical-slice-vertical-slice** (`architecture/vertical-slice/vertical-slice.md`) — Uma slice por operação em `Features/{Entity}Feature/{Verb}{Entity}/`: Request/Response `record`, Handler, Validator e Endpoint `IApiEndpoint`. Decorators Scrutor: Logging → Validation → Handler. PROIBIDO: MediatR/`IRequest`, AggregateRoot, DomainEvent, CQRS formal, camada Application Service, DTO compartilhado entre features, `IRepository<T>`/`IUnitOfWork`.
12
+ - **backend-dotnet-vsa-handler-patterns** (`backend/dotnet/vsa-handler-patterns.md`) — Handler implementando `IHandler<TRequest, TResponse>` (não MediatR): injeta `ApplicationDbContext` direto e devolve `Result<T>`. Erros em `{Feature}Errors.cs`. SQL cru ou store dedicado só com justificativa em `plan.md` (`## Decisões`).
13
13
  - **backend-dotnet-result-pattern** (`backend/dotnet/result-pattern.md`) — Falha esperada é `Result`/`Result<T>`, nunca exceção. Endpoint: `result.Match(value => Results.Ok(value), error => error.ToHttpResult())`; nunca leia `.Value` fora do `Match`. Status pelo `ErrorType` (Validation 400, NotFound 404, Conflict 409, Unauthorized 401, Forbidden 403, Unexpected 500), nunca BadRequest para tudo. PROIBIDO: try/catch no handler, retornar null, erro string sem código.
14
- - **backend-dotnet-fluent-validation-vsa** (`backend/dotnet/fluent-validation-vsa.md`) — Um `sealed AbstractValidator<TRequest>` (FluentValidation 12) por request, na pasta da slice; o `ValidationDecorator` roda antes do handler e devolve `Result.Failure(ValidationError)`. `AddValidatorsFromAssembly()` ANTES de `AddHandlersFromAssembly()`. Sempre `.WithMessage()`; tempo via `TimeProvider` lido DENTRO da regra. Nunca lance `ValidationException`.
14
+ - **backend-dotnet-fluent-validation-vsa** (`backend/dotnet/fluent-validation-vsa.md`) — Um `AbstractValidator<TRequest>` (FluentValidation 12) por request, na pasta da slice; o `ValidationDecorator` roda antes do handler e devolve `Result.Failure(ValidationError)`. `AddValidatorsFromAssembly()` ANTES de `AddHandlersFromAssembly()`. Sempre `.WithMessage()`; tempo via `TimeProvider` lido DENTRO da regra. Nunca lance `ValidationException`.
15
15
  - **backend-dotnet-scrutor** (`backend/dotnet/scrutor.md`) — `AddHandlersFromAssembly` registra cada `IHandler<,>` como Scoped (filtro `!t.ContainsGenericParameters`) e depois `Decorate` Validation e então Logging (o último registrado é o mais externo). Endpoints `IApiEndpoint` via `TryAddEnumerable` Transient + `MapApiEndpoints`. Nunca registre handler um a um.
16
16
  - **backend-dotnet-scalar-ui** (`backend/dotnet/scalar-ui.md`) — Scalar no lugar do Swagger: `Microsoft.AspNetCore.OpenApi` 10.0.0 + `Scalar.AspNetCore` 2.12.40, nunca Swashbuckle. `MapOpenApi()` só em `IsDevelopment()` e antes de `MapScalarApiReference`. Todo endpoint com `.WithTags(ApiTags.X)` (constantes), `.Produces<T>` e `.ProducesProblem(status)` por falha possível.
17
+ - **backend-dotnet-analyzers** (`backend/dotnet/analyzers.md`) — O build cobra, a prosa não repete: `Guid.NewGuid()` é erro RS0030 (use `Guid.CreateVersion7()`) e token recebido e não repassado é erro CA2016, por `Directory.Build.props` + `BannedSymbols.txt` + `morph.globalconfig` na raiz. `sealed`, primary constructor e `var` são gosto: siga o arquivo vizinho.
17
18
  - **backend-api-validation** (`backend/api/validation.md`) — O validator checa só FORMA (sem DbContext); existência, unicidade e estado ficam no handler com `{Entity}Errors` tipados. Checagem de estado valida o INVÁLIDO (enum ordenado, estados de erro em 100+). PROIBIDO: lançar exceção, validar na lambda do endpoint, misturar DataAnnotations, `if (status != Expected)`, switch exaustivo sobre estados válidos.
18
19
  - **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
20
+ - **architecture-engineering-lens** (`architecture/engineering-lens.md`) — Escreva para quem herda amanhã: convenção do projeto, o mais simples, causa raiz. PROIBIDO no que você adiciona: `catch` que engole, flag booleana que bifurca o método (são dois métodos), efeito colateral escondido, símbolo/flag/config sem consumidor, abstração ou camada sem segundo uso, literal repetido sem constante. Nomes de domínio buscáveis, funções pequenas, ≤ 3 parâmetros, condicional positiva. Commit em Conventional Commits (hook `conventional-commit`).
19
21
 
20
22
  > Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
@@ -40,8 +40,8 @@ For each new feature slice, follow the patterns in these templates:
40
40
  |----------|------|---------|
41
41
  | Endpoint | `.morph/framework/templates/code/dotnet/feature/endpoint.cs.hbs` | IApiEndpoint with MapEndpoint() |
42
42
  | Handler | `.morph/framework/templates/code/dotnet/feature/handler.cs.hbs` | IHandler<TRequest, TResponse> |
43
- | Request | `.morph/framework/templates/code/dotnet/feature/request.cs.hbs` | Sealed record DTO |
44
- | Response | `.morph/framework/templates/code/dotnet/feature/response.cs.hbs` | Sealed record DTO |
43
+ | Request | `.morph/framework/templates/code/dotnet/feature/request.cs.hbs` | record DTO |
44
+ | Response | `.morph/framework/templates/code/dotnet/feature/response.cs.hbs` | record DTO |
45
45
  | Validator | `.morph/framework/templates/code/dotnet/feature/validator.cs.hbs` | FluentValidation AbstractValidator |
46
46
  | Entity | `.morph/framework/templates/code/dotnet/feature/entity.cs.hbs` | Domain entity |
47
47
  | Errors | `.morph/framework/templates/code/dotnet/feature/errors.cs.hbs` | Error factory |
@@ -19,5 +19,6 @@ stacks:
19
19
  - **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET; banco NUNCA em route handler. Zod no client e no server (`safeParse`, 400). `strict: true` + `noUncheckedIndexedAccess`; sem `any` em resposta de API. shadcn via `npx shadcn@latest add`.
20
20
  - **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react` (nunca `framer-motion` junto). Variants em `motion/variants.ts` com durations/easings de `@/lib/design-tokens`. `useReducedMotion()` obrigatório; o `layout.tsx` raiz segue Server Component. 3–5 momentos de motion por página; cena guiada por scroll é do GSAP, nunca no mesmo elemento.
21
21
  - **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
22
+ - **architecture-engineering-lens** (`architecture/engineering-lens.md`) — Escreva para quem herda amanhã: convenção do projeto, o mais simples, causa raiz. PROIBIDO no que você adiciona: `catch` que engole, flag booleana que bifurca o método (são dois métodos), efeito colateral escondido, símbolo/flag/config sem consumidor, abstração ou camada sem segundo uso, literal repetido sem constante. Nomes de domínio buscáveis, funções pequenas, ≤ 3 parâmetros, condicional positiva. Commit em Conventional Commits (hook `conventional-commit`).
22
23
 
23
24
  > Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
@@ -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.4.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.4.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.4.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.4.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.4.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.4.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs