@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -227,25 +227,18 @@ export function getFeature(featureName, projectPath) {
227
227
  * Derive phase from filesystem (phase deleted from state).
228
228
  *
229
229
  * Walks the phase map in descending order and returns the first phase whose
230
- * CANONICAL ARTIFACT exists. An empty folder is not enough — this avoids the
231
- * chicken-and-egg problem where `mkdir 2-plan/` (or any pre-created sibling)
232
- * advances the phase before plan content is written, then
233
- * `enforce-phase-writes` blocks writes back to `0-proposal/`.
230
+ * CANONICAL ARTIFACT exists. An empty folder is not enough — a pre-created
231
+ * sibling folder never advances the phase before its content is written.
234
232
  *
235
233
  * Canonical artifacts (one per phase) match MORPH.md §3:
236
- * - 4-review → review-report.md
237
- * - 3-implement → recap.md
238
- * - 2-plan → spec.md (the first plan-phase output produced)
239
- * - 1-design → design-system.md
240
- * - 0-proposal → proposal.md
234
+ * - 4-review → verifier.md
235
+ * - 3-implement → any slice report (`reports/*.md`)
236
+ * - 1-design → design-system.md
237
+ * - 2-plan → plan.md
241
238
  *
242
- * If no canonical file is found, returns `'proposal'` (the starting phase).
243
- *
244
- * @param {string} featureName
245
- * @param {string} [projectPath]
246
- * @returns {string} Phase name
239
+ * If no canonical file is found, returns `'plan'` (the starting phase).
247
240
  */
248
- export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
241
+ export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
249
242
 
250
243
  /**
251
244
  * The canonical artifact of each phase, descending. Exported so a caller that
@@ -253,32 +246,61 @@ export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review
253
246
  * tree, say) can answer the same question without re-encoding the map.
254
247
  */
255
248
  export const PHASE_ARTIFACTS = [
256
- ['4-review', 'review', 'review-report.md'],
257
- ['3-implement', 'implement', 'recap.md'],
258
- ['2-plan', 'plan', 'spec.md'],
249
+ ['4-review', 'review', 'verifier.md'],
250
+ ['3-implement', 'implement', 'reports/'],
259
251
  ['1-design', 'uiux', 'design-system.md'],
260
- ['0-proposal', 'proposal', 'proposal.md'],
252
+ ['2-plan', 'plan', 'plan.md'],
261
253
  ];
262
254
 
255
+ const DESIGN_DIR = '1-design';
256
+
263
257
  const gatesOf = (feature) => feature?.approvals || feature?.approvalGates;
264
258
 
265
259
  /**
266
260
  * Gate-implied phase from an ALREADY-LOADED feature object — no disk access.
267
- *
268
- * Split out of `gateImpliedPhase` so callers holding a feature.json that did
269
- * not come from the working tree (e.g. `git show morph/{f}:…`) can reuse the
270
- * rule instead of copying it. The path-based wrapper below is unchanged.
261
+ * An approved gate authorizes the NEXT phase:
262
+ * review approved → review (terminal)
263
+ * plan approved → implement, or uiux while the design branch was taken
264
+ * (`1-design/` exists) and its gate is still open
265
+ * otherwise → plan
271
266
  *
272
267
  * @param {Object|null} feature
268
+ * @param {boolean} [designStarted] - whether `1-design/` exists for the feature
273
269
  * @returns {string} phase name
274
270
  */
275
- export function gateImpliedPhaseFrom(feature) {
271
+ export function gateImpliedPhaseFrom(feature, designStarted = false) {
276
272
  const gates = gatesOf(feature);
277
273
  const ok = (g) => gates?.[g]?.approved === true;
278
274
  if (ok('review')) return 'review';
279
- if (ok('plan')) return 'implement';
280
- if (ok('uiux')) return 'plan';
281
- return 'proposal';
275
+ if (ok('plan')) return designStarted && !ok('uiux') ? 'uiux' : 'implement';
276
+ return 'plan';
277
+ }
278
+
279
+ /**
280
+ * Artifact predicate over a feature dir on disk. A path ending in `/` asks for
281
+ * any `.md` file directly under it: an empty folder is not an artifact.
282
+ */
283
+ export function diskArtifactProbe(featureDir) {
284
+ return (rel) => {
285
+ const abs = join(featureDir, ...rel.split('/').filter(Boolean));
286
+ if (!rel.endsWith('/')) return existsSync(abs);
287
+ try {
288
+ return readdirSync(abs, { withFileTypes: true }).some((e) => e.isFile() && e.name.endsWith('.md'));
289
+ } catch {
290
+ return false;
291
+ }
292
+ };
293
+ }
294
+
295
+ /** The same predicate over a set of feature-relative POSIX paths (a git tree). */
296
+ export function fileSetArtifactProbe(files) {
297
+ return (rel) => {
298
+ if (!rel.endsWith('/')) return files.has(rel);
299
+ for (const f of files) {
300
+ if (f.startsWith(rel) && f.endsWith('.md') && !f.slice(rel.length).includes('/')) return true;
301
+ }
302
+ return false;
303
+ };
282
304
  }
283
305
 
284
306
  /**
@@ -286,44 +308,33 @@ export function gateImpliedPhaseFrom(feature) {
286
308
  * access of its own, so the artifact universe can be a git tree.
287
309
  *
288
310
  * @param {Object|null} feature
289
- * @param {(relPath: string) => boolean} hasArtifact - receives POSIX-style
290
- * paths relative to the feature dir, e.g. `2-plan/spec.md`
311
+ * @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe}
312
+ * or {@link fileSetArtifactProbe}: POSIX paths relative to the feature dir
291
313
  * @returns {string} phase name
292
314
  */
293
315
  export function derivePhaseFrom(feature, hasArtifact) {
294
- let artifactPhase = 'proposal';
316
+ let artifactPhase = 'plan';
295
317
  for (const [folder, phase, canonical] of PHASE_ARTIFACTS) {
296
318
  if (hasArtifact(`${folder}/${canonical}`)) { artifactPhase = phase; break; }
297
319
  }
298
320
 
299
321
  // Gate-aware: an approved gate advances the phase even before the next
300
- // phase's first canonical artifact exists. This breaks the chicken-and-egg
301
- // where that first artifact could never be written because the phase never
302
- // advanced to permit it. Take whichever of (artifact, gate-implied) is later.
303
- const implied = gateImpliedPhaseFrom(feature);
322
+ // phase's first canonical artifact exists, or that artifact could never be
323
+ // written. Take whichever of (artifact, gate-implied) is later.
324
+ const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
325
+ const implied = gateImpliedPhaseFrom(feature, designStarted);
304
326
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
305
327
  }
306
328
 
307
- /**
308
- * The phase implied purely by approved gates, independent of which artifacts
309
- * exist on disk. An approved gate authorizes the NEXT phase, so:
310
- * review approved → review (terminal)
311
- * plan approved → implement
312
- * uiux approved → plan
313
- * otherwise → proposal (proposal-approval's fork between uiux/plan is
314
- * resolved by enforce-phase-writes, not here)
315
- * @returns {string} phase name
316
- */
329
+ /** The phase implied purely by approved gates — see {@link gateImpliedPhaseFrom}. */
317
330
  export function gateImpliedPhase(featureName, projectPath) {
318
- return gateImpliedPhaseFrom(getFeature(featureName, projectPath));
331
+ const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
332
+ return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
319
333
  }
320
334
 
321
335
  export function derivePhaseForFeature(featureName, projectPath) {
322
336
  const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
323
- return derivePhaseFrom(
324
- getFeature(featureName, projectPath),
325
- (rel) => existsSync(join(basePath, rel)),
326
- );
337
+ return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
327
338
  }
328
339
 
329
340
  /**
@@ -342,7 +353,7 @@ export function getFeaturePhase(featureName, projectPath) {
342
353
  /**
343
354
  * Get approval status for a gate.
344
355
  * @param {string} featureName
345
- * @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
356
+ * @param {string} gate - 'plan'|'uiux'|'review'
346
357
  * @param {string} [projectPath]
347
358
  * @returns {boolean}
348
359
  */
@@ -384,43 +395,6 @@ export function getOutputs(featureName, projectPath) {
384
395
  return feature?.outputs || null;
385
396
  }
386
397
 
387
- /**
388
- * Get uncompleted required outputs for current phase.
389
- * @param {string} featureName
390
- * @param {string} [projectPath]
391
- * @returns {Array<{type: string, path: string}>}
392
- */
393
- /**
394
- * Get task statistics directly from taskList (preferred over feature.tasks counters).
395
- * Excludes expanded tasks from counts, matching calculateProgress() behavior.
396
- *
397
- * @param {Object} feature - Feature state object
398
- * @returns {{ total: number, completed: number, inProgress: number, pending: number }}
399
- */
400
- export function getTaskStats(feature) {
401
- const list = feature?.taskList || [];
402
- if (list.length > 0) {
403
- const active = list.filter(t => t.status !== 'expanded');
404
- return {
405
- total: active.length,
406
- completed: active.filter(t => t.status === 'completed').length,
407
- inProgress: active.filter(t => t.status === 'in_progress').length,
408
- pending: active.filter(t => t.status === 'pending').length,
409
- };
410
- }
411
- // Fallback: read from legacy counter object
412
- const tasks = feature?.tasks;
413
- if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)) {
414
- return {
415
- total: tasks.total || 0,
416
- completed: tasks.completed || 0,
417
- inProgress: tasks.inProgress || 0,
418
- pending: tasks.pending || 0,
419
- };
420
- }
421
- return { total: 0, completed: 0, inProgress: 0, pending: 0 };
422
- }
423
-
424
398
  export function getMissingOutputs(featureName, projectPath) {
425
399
  const feature = getFeature(featureName, projectPath);
426
400
  if (!feature?.outputs) return [];
@@ -101,7 +101,7 @@ export function persistFeature(featureName, feature, projectPath) {
101
101
  * canonical shape — onto the feature and persists it. Returns true on success.
102
102
  *
103
103
  * @param {string} featureName
104
- * @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
104
+ * @param {string} gate - 'plan'|'uiux'|'review'
105
105
  * @param {{ approvedBy?: string, timestamp?: string }} [meta]
106
106
  * @param {string} [projectPath]
107
107
  */
@@ -142,9 +142,8 @@ export function createDefaultFeature(featureName) {
142
142
  createdAt: now,
143
143
  updatedAt: now,
144
144
  approvalGates: {
145
- proposal: { approved: false, timestamp: null, approvedBy: null },
146
- uiux: { approved: false, timestamp: null, approvedBy: null },
147
145
  plan: { approved: false, timestamp: null, approvedBy: null },
146
+ uiux: { approved: false, timestamp: null, approvedBy: null },
148
147
  review: { approved: false, timestamp: null, approvedBy: null },
149
148
  },
150
149
  outputs: {},
@@ -3,7 +3,7 @@
3
3
  "description": "Centralized MCP registry with phase relevance and install configuration for morph-spec workflows",
4
4
  "mcps": {
5
5
  "context7": {
6
- "phases": ["proposal", "plan", "implement"],
6
+ "phases": ["plan", "implement"],
7
7
  "tools": ["resolve-library-id", "query-docs"],
8
8
  "serverNames": ["context7", "plugin_context7_context7"],
9
9
  "detection": "enabledPlugins includes context7 or mcpServers includes context7",
@@ -216,7 +216,7 @@
216
216
  }
217
217
  },
218
218
  "graphify": {
219
- "phases": ["proposal", "plan", "implement", "review"],
219
+ "phases": ["plan", "implement", "review"],
220
220
  "tools": ["query_graph", "get_node", "get_neighbors", "get_community", "god_nodes", "graph_stats", "shortest_path", "list_prs", "get_pr_impact", "triage_prs"],
221
221
  "detection": "graphify-out/graph.json exists in the project root (written by morph-spec graph build, or by the graphify post-commit hook)",
222
222
  "fallback": "Comportamento atual: Grep/Glob/Read. O grafo é um subconjunto estrito do grep — no eval não encontrou um único arquivo que o grep perderia — então cair para grep não perde recall, só devolve mais ruído",
@@ -281,9 +281,8 @@
281
281
  },
282
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.",
283
283
  "phaseMatrix": {
284
- "proposal": ["context7", "graphify"],
285
- "uiux": ["playwright", "context7", "stitch", "morph-assets"],
286
284
  "plan": ["context7", "graphify"],
285
+ "uiux": ["playwright", "context7", "stitch", "morph-assets"],
287
286
  "implement": ["context7", "playwright", "vercel", "morph-assets", "graphify"],
288
287
  "review": ["playwright", "context7", "graphify"]
289
288
  }
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "version": "5.0.0",
3
3
  "phases": {
4
- "proposal": {
5
- "id": "proposal",
4
+ "plan": {
5
+ "id": "plan",
6
6
  "index": 0,
7
- "folder": "0-proposal",
8
- "displayName": "Proposal",
7
+ "folder": "2-plan",
8
+ "displayName": "Plan",
9
9
  "goals": [
10
- "Understand the business problem and user flows",
11
- "Detect existing stack (brownfield) or assume default stack (greenfield)",
12
- "Produce proposal.md and reach Gate 1"
10
+ "Understand the business problem and scan the stack (brownfield) or assume the default (greenfield)",
11
+ "Write plan.md — scenarios with proof, decisions, reuse and slices",
12
+ "Run plan-check to produce checks.json and reach the plan gate"
13
13
  ],
14
- "requiredOutputs": ["proposal"],
15
- "optionalOutputs": ["stackScan"],
14
+ "requiredOutputs": ["plan", "checks"],
15
+ "optionalOutputs": [],
16
16
  "pausePoints": [
17
- { "id": "proposal", "label": "Gate 1 — Approve proposal" }
17
+ { "id": "plan", "label": "Gate do plano — Approve plan.md + checks.json" }
18
18
  ],
19
- "skill": "morph-brainstorming"
19
+ "skill": "morph-plan"
20
20
  },
21
21
  "uiux": {
22
22
  "id": "uiux",
@@ -35,50 +35,34 @@
35
35
  ],
36
36
  "skill": "morph-uiux"
37
37
  },
38
- "plan": {
39
- "id": "plan",
40
- "index": 2,
41
- "folder": "2-plan",
42
- "displayName": "Plan",
43
- "goals": [
44
- "Write spec.md (technical spec)",
45
- "Write tasks.json (ordered task breakdown)"
46
- ],
47
- "requiredOutputs": ["spec", "tasks"],
48
- "optionalOutputs": ["decisions"],
49
- "pausePoints": [
50
- { "id": "plan", "label": "Gate 2 — Approve spec + tasks" }
51
- ],
52
- "skill": "morph-plan"
53
- },
54
38
  "implement": {
55
39
  "id": "implement",
56
- "index": 3,
40
+ "index": 2,
57
41
  "folder": "3-implement",
58
42
  "displayName": "Implementation",
59
43
  "goals": [
60
- "Execute tasks in order with auto-advance between tasks",
61
- "Advance a task only on verify green and its doneCriteria met",
62
- "Maintain recap.md and execution trace"
44
+ "Execute the slices of plan.md, in parallel waves when plan-check allows",
45
+ "Close a slice only on verify green and its scenarios proven",
46
+ "One report per slice (3-implement/reports/F{n}.md, with **Arquivos:** and **Standards:**) — the implementation record"
63
47
  ],
64
- "requiredOutputs": ["recap"],
48
+ "requiredOutputs": [],
65
49
  "optionalOutputs": ["evidencias"],
66
50
  "pausePoints": [],
67
- "skill": "morph-implement"
51
+ "skill": "morph-apply"
68
52
  },
69
53
  "review": {
70
54
  "id": "review",
71
- "index": 4,
55
+ "index": 3,
72
56
  "folder": "4-review",
73
57
  "displayName": "Review",
74
58
  "goals": [
75
- "Independent evaluator verdict (SHIP/FIX) with evidence-typed blockers",
76
- "Produce review-report.md and present Gate 3"
59
+ "Verifier proves every scenario; reviewer (and security, when required) judge the diff at a fixed SHA",
60
+ "verifier.md + reviewer.md (+ security.md) with SHIP/FIX and evidence-typed blockers, then Gate 3"
77
61
  ],
78
- "requiredOutputs": ["reviewReport"],
79
- "optionalOutputs": [],
62
+ "requiredOutputs": ["verifierReport", "reviewerReport"],
63
+ "optionalOutputs": ["securityReport"],
80
64
  "pausePoints": [
81
- { "id": "review", "label": "Gate 3 — Approve review-report" }
65
+ { "id": "review", "label": "Gate 3 — Approve the review reports" }
82
66
  ],
83
67
  "skill": "morph-review"
84
68
  }
@@ -9,6 +9,18 @@
9
9
  "type": "object",
10
10
  "additionalProperties": true,
11
11
  "properties": {
12
+ "stack": {
13
+ "type": "string",
14
+ "description": "Stack tags joined by `+` (e.g. `dotnet+nextjs`). Decides the build node's commands, which assets install, and that bin/ and obj/ go into .gitignore."
15
+ },
16
+ "frontendPath": {
17
+ "type": "string",
18
+ "description": "Folder of the frontend, relative to the project root: where the build node runs `tsc`. Absent: the root."
19
+ },
20
+ "backendPath": {
21
+ "type": "string",
22
+ "description": "Folder of the .NET solution, relative to the project root: where the build node runs `dotnet build`. Absent: the root, or the repository's single .sln/.slnx (else single .csproj) when the root has none."
23
+ },
12
24
  "ci": {
13
25
  "type": "boolean",
14
26
  "description": "false turns `verify {f} --ci` into the local verify (with the reason printed). Absent or true: the Gate 3 proof comes from the GitHub Actions workflow that runs `morph-spec verify` (created on first use), whenever origin is GitHub and gh is authenticated."
@@ -60,20 +72,13 @@
60
72
  "e2e": {
61
73
  "type": "boolean",
62
74
  "default": false,
63
- "description": "Requires the application stack to be up. Runs at feature scope only, never in the per-task loop."
75
+ "description": "Requires the application stack to be up."
64
76
  },
65
77
  "linuxImage": {
66
78
  "type": "string",
67
79
  "minLength": 1,
68
80
  "description": "Overrides the container image used by `verify --linux` for this node."
69
81
  },
70
- "groups": {
71
- "type": "array",
72
- "minItems": 1,
73
- "uniqueItems": true,
74
- "items": { "type": "string", "enum": ["backend", "frontend", "tests", "infra", "docs"] },
75
- "description": "Which task group(s) (tasks.json `group`) this node verifies. Absent means the runner's own default applies (RUNNER_DEFAULT_GROUPS in src/lib/tasks/test-nodes.js) — `none`/`evals` have no default and are unscoped unless declared here."
76
- },
77
82
  "evals": {
78
83
  "type": "object",
79
84
  "additionalProperties": false,
@@ -191,7 +196,7 @@
191
196
  "shutdownBuildServer": {
192
197
  "type": "boolean",
193
198
  "default": true,
194
- "description": "Run `dotnet build-server shutdown` at the end of a feature-scope `verify` and of `e2e down`, only when no slot is held and no dotnet build/test is alive. Never at the end of a task-scope verify: every task would pay a cold compiler."
199
+ "description": "Run `dotnet build-server shutdown` at the end of a `verify` and of `e2e down`, only when no slot is held and no dotnet build/test is alive."
195
200
  },
196
201
  "disableNodeReuse": {
197
202
  "type": "boolean",
@@ -38,17 +38,16 @@
38
38
  "type": "object",
39
39
  "description": "One entry per gate. Written ONLY by `morph-spec approve`/`advance` (approvedBy: \"auto\") or the state-sync hook on a human \"aprovado\" answer (approvedBy: \"manual\").",
40
40
  "properties": {
41
- "proposal": { "$ref": "#/definitions/gate" },
42
- "uiux": { "$ref": "#/definitions/gate" },
43
41
  "plan": { "$ref": "#/definitions/gate" },
42
+ "uiux": { "$ref": "#/definitions/gate" },
44
43
  "review": { "$ref": "#/definitions/gate" }
45
44
  },
46
45
  "additionalProperties": false
47
46
  },
48
47
  "gateDecisions": {
49
48
  "type": "object",
50
- "description": "Trust decision persisted BEFORE acting on a gate, written ONLY by `morph-spec gate-decision` (decidedAt is server-side). Read by the gate-guard hook to verify the observed action matched the recorded decision.",
51
- "propertyNames": { "enum": ["proposal", "uiux", "plan", "review"] },
49
+ "description": "Trust decision persisted BEFORE acting on a gate, written ONLY by `morph-spec gate-decision` and `morph-spec gate-check` (decidedAt is server-side). Read by the gate-guard hook to verify the observed action matched the recorded decision.",
50
+ "propertyNames": { "enum": ["plan", "uiux", "review"] },
52
51
  "additionalProperties": {
53
52
  "type": "object",
54
53
  "required": ["pause", "reason", "signals", "trust", "decidedAt"],
@@ -57,48 +56,26 @@
57
56
  "reason": { "type": "string", "description": "The signal that drove the decision (or 'nenhum sinal de risco')." },
58
57
  "signals": {
59
58
  "type": "object",
60
- "description": "Risk signals evaluated (free-form): taskCount, evaluatorVerdict, standardsViolations, designSystemDivergence..."
59
+ "description": "Risk signals evaluated (free-form): sliceCount, planChecks, reviewReports, standardsViolations, designSystemDivergence..."
61
60
  },
62
61
  "trust": { "type": "string", "enum": ["auto", "manual"] },
63
62
  "decidedAt": { "type": "string", "format": "date-time" },
64
63
  "mockupsHash": {
65
64
  "type": ["string", "null"],
66
65
  "description": "uiux only: hash of the 1-design/mockups.md the decision saw (null = no mockup). A pause:false unlocks the auto signature only while the mockup is still this one."
67
- }
68
- },
69
- "additionalProperties": false
70
- }
71
- },
72
- "taskVerifications": {
73
- "type": "object",
74
- "description": "Deterministic verify results per task, written ONLY by `morph-spec verify` (task scope). updatedAt is server-side; attempts auto-increment. Persisted only from the primary root or the feature's own registered worktree — see verify.js decidePersistence().",
75
- "propertyNames": { "pattern": "^T\\d+$" },
76
- "additionalProperties": {
77
- "type": "object",
78
- "required": ["status", "nodes", "attempts", "updatedAt"],
79
- "properties": {
80
- "status": { "type": "string", "enum": ["pass", "fail"] },
81
- "nodes": {
82
- "type": "object",
83
- "required": ["build", "tests", "validators"],
84
- "properties": {
85
- "build": { "type": "string", "enum": ["pass", "fail", "skip"] },
86
- "tests": { "type": "string", "enum": ["pass", "fail", "skip"] },
87
- "validators": { "type": "string", "enum": ["pass", "fail", "skip"] },
88
- "e2e": { "type": "string", "enum": ["pass", "fail", "skip"], "description": "Docker bring-up + Playwright specs against the running stack. Absent on entries written before the node existed — which is why it is NOT in `required`." },
89
- "evals": { "type": ["string", "null"], "enum": ["pass", "fail", "skip", null], "description": "Eval suites, pulled out of the tests rollup. Written by the SAME function that writes the singular stamp (nodeStatuses in verify.js): the two shapes must stay identical, or a stamp would omit what the history records. null means the project declares no eval node at all — distinct from skip, which means it declares one and this run did not execute it. Absent on entries written before the field existed, which is why it is NOT in required." }
90
- },
91
- "additionalProperties": false
92
66
  },
93
- "attempts": { "type": "integer", "minimum": 1 },
94
- "updatedAt": { "type": "string", "format": "date-time" }
67
+ "source": {
68
+ "type": "string",
69
+ "enum": ["manual", "gate-check"],
70
+ "description": "Which command decided: `gate-decision` writes manual, `gate-check` writes gate-check. A manual pause:true is sticky: `gate-check` keeps it paused (manual reason, fresh signals); only a new `gate-decision --pause false` clears it."
71
+ }
95
72
  },
96
73
  "additionalProperties": false
97
74
  }
98
75
  },
99
76
  "verifications": {
100
77
  "type": "object",
101
- "description": "Deterministic verify results at feature scope, written ONLY by `morph-spec verify` (feature scope, no task id).",
78
+ "description": "Deterministic verify results at feature scope, written ONLY by `morph-spec verify`.",
102
79
  "properties": {
103
80
  "feature": {
104
81
  "type": "object",
@@ -127,8 +104,8 @@
127
104
  },
128
105
  "verification": {
129
106
  "type": "object",
130
- "description": "The single latest deterministic verify stamp (any scope), written ONLY by `morph-spec verify`. Distinct from `verifications`/`taskVerifications` above (per-scope history with attempts/nodes): this is overwritten by every verify call regardless of scope, carrying just enough to correlate a pass/fail with the exact commit it covers. Primary consumer: `morph-spec gate-check`, which feeds `status` to shouldPauseGate as `verificationStatus` — a red verify pauses the gate (risk-detector.js) — and `nodes.e2e`/`e2eReason` as `e2eStatus`/`e2eReason`, the pair that decides whether an e2e SKIP is a recorded dispensation or an absence of proof (a skip never reddens `status`, so the rollup alone is blind to it). `scope` is the third pause input: a dispensation only counts from a feature-scoped stamp (see the field below). The rest (sha/ts, and the derived `verificationStale`) is audit context.",
131
- "required": ["status", "sha", "ts", "scope"],
107
+ "description": "The single latest deterministic verify stamp, written ONLY by `morph-spec verify`. Distinct from `verifications` above (history with attempts/nodes): this is overwritten by every persisted verify, carrying just enough to correlate a pass/fail with the exact commit it covers. Primary consumer: `morph-spec gate-check`, which feeds `status` to shouldPauseGate as `verificationStatus` — a red verify pauses the gate (risk-detector.js) — and `nodes.e2e`/`e2eReason` as `e2eStatus`/`e2eReason`, the pair that decides whether an e2e SKIP is a recorded dispensation or an absence of proof (a skip never reddens `status`, so the rollup alone is blind to it). The rest (sha/ts, and the derived `verificationStale`) is audit context.",
108
+ "required": ["status", "sha", "ts"],
132
109
  "properties": {
133
110
  "status": { "type": "string", "enum": ["pass", "fail"] },
134
111
  "nodes": {
@@ -142,31 +119,19 @@
142
119
  "evals": {
143
120
  "type": ["string", "null"],
144
121
  "enum": ["pass", "fail", "skip", null],
145
- "description": "Status of the `runner: \"evals\"` entries of nodes.tests.runs[], pulled out of the rollup. The rollup cannot answer what Gate 3 asks: a red eval and a red unit test both read as tests:fail, and an eval SKIPPED at task scope leaves the rollup green. null means this project declares NO eval node - distinct from 'skip', which means it declares one and this run did not execute it; risk-detector.js pauses on the two for different reasons because they call for different actions. Absent on stamps written before this field existed."
122
+ "description": "Status of the `runner: \"evals\"` entries of nodes.tests.runs[], pulled out of the rollup. The rollup cannot answer what Gate 3 asks: a red eval and a red unit test both read as tests:fail. null means this project declares NO eval node - distinct from 'skip', which means it declares one and this run did not execute it; risk-detector.js pauses on the two for different reasons because they call for different actions. Absent on stamps written before this field existed."
146
123
  }
147
124
  },
148
125
  "additionalProperties": false
149
126
  },
150
127
  "e2eReason": { "type": ["string", "null"], "description": "The LITERAL reason the e2e node skipped (e2e-check.js's own wording), or null when it did not skip. risk-detector.js's classifyE2eSkip() reads it: 'project does not containerize' / 'does not touch runtime' are legitimate dispensations and pass; 'docker is not available' / 'requested' (--skip-e2e) / anything unclassifiable — null included — are an absence of proof and pause Gate 3. A skip recorded WITHOUT a reason is refused on purpose: a nameless dispensation is a silent no-op." },
151
128
  "sha": { "type": ["string", "null"], "description": "HEAD commit SHA at verify time (git rev-parse HEAD); null when not in a git repo or no commits yet." },
152
- "ts": { "type": "string", "format": "date-time", "description": "Server-side timestamp, same instant as the corresponding verifications/taskVerifications entry's updatedAt." },
153
- "scope": { "type": "string", "description": "'feature', or 'task:<id>' for a task-scoped verify. READ BY THE GATE, not just audit: this stamp is singular and any verify overwrites it, so a 'task:<id>' value at ship time means the last verify was NOT the feature's. risk-detector.js therefore accepts an e2e dispensation ('project'/'scope' in e2eReason) only from scope === 'feature' — a `morph-spec verify {f} {task}` of a task whose group does not touch runtime stamps exactly the legitimate-sounding reason that would otherwise buy Gate 3 for the whole feature (the last task of a feature is typically group: docs). Absent/unknown falls on the safe side, like an unclassifiable reason." },
129
+ "ts": { "type": "string", "format": "date-time", "description": "Server-side timestamp, same instant as the corresponding verifications entry's updatedAt." },
154
130
  "source": { "type": "string", "enum": ["ci"], "description": "Present only when the stamp came from `verify --ci`: the verify --json the project CI uploaded for the pushed commit, judged by the same rules as a local run. Absent = local run." },
155
131
  "ciRun": { "type": "string", "description": "URL of the CI run whose artifact wrote this stamp (with source: ci). `sha` is that run's headSha, so verificationStale compares the code CI saw with HEAD." }
156
132
  },
157
133
  "additionalProperties": false
158
134
  },
159
- "tasks": {
160
- "type": "object",
161
- "description": "Aggregate task counters (informational — the source of truth is 2-plan/tasks.json).",
162
- "properties": {
163
- "total": { "type": "integer", "minimum": 0 },
164
- "completed": { "type": "integer", "minimum": 0 },
165
- "inProgress": { "type": "integer", "minimum": 0 },
166
- "pending": { "type": "integer", "minimum": 0 }
167
- },
168
- "additionalProperties": true
169
- },
170
135
  "outputs": {
171
136
  "type": "object",
172
137
  "description": "Outputs tracked by the state-sync hook: filename → { created, path }.",
@@ -234,7 +199,7 @@
234
199
  "approved": { "type": "boolean" },
235
200
  "timestamp": { "type": ["string", "null"], "description": "ISO timestamp of the approval/revocation, null while never evaluated." },
236
201
  "approvedBy": { "type": ["string", "null"], "description": "\"auto\" (approve/advance --mode auto), \"manual\" (human in the loop), \"policy:{workType}\" (gate stamped at birth by `morph-spec create` per the work type's skippedGates — see src/core/work-types.js; the gate-guard hook only special-cases \"auto\", so policy-stamped gates stay silent to it), a --approver name, or null while never evaluated." },
237
- "selfAssessed": { "type": "boolean", "description": "Gate `review` SOMENTE. `true` quando o Gate 3 foi aprovado sem `4-review/evaluator-report.md`, via `morph-spec approve <f> review --self-assessed`. O veredito deveria vir do avaliador independente; esta flag registra a degradação em vez de deixá-la silenciosa, e aparece no `morph-spec list`." }
202
+ "selfAssessed": { "type": "boolean", "description": "Gate `review` SOMENTE. `true` quando o Gate 3 foi aprovado sem os relatórios de review exigidos (`4-review/verifier.md`, `reviewer.md`, `security.md`), via `morph-spec approve <f> review --self-assessed` ou um \"aprovado\" humano sem eles. O veredito deveria vir dos agentes independentes; esta flag registra a degradação em vez de deixá-la silenciosa, e aparece no `morph-spec list`." }
238
203
  },
239
204
  "additionalProperties": true
240
205
  }
@@ -12,10 +12,9 @@ These are the only skills the harness needs. MORPH.md references them by path.
12
12
 
13
13
  | Skill | Purpose | User-invocable |
14
14
  |----------------------|----------------------------------------------------------------------|----------------|
15
- | `morph-brainstorming` | Business understanding + brownfield scan → proposal.md (Gate 1) | yes |
16
- | `morph-stack-scan` | Detect stack drift in existing projects, report in stack-scan.md | yes |
17
- | `morph-plan` | Produce use cases + spec.md + tasks.json (Gate 2) | yes |
18
- | `morph-implement` | Execute each task with standard + persona, auto-advance between tasks| no |
15
+ | `morph-plan` | Route, scan, interview, write plan.md, plan-check and the plan gate | yes |
16
+ | `morph-uiux` | Optional design branch after the plan gate (mockups + screen checklist) | yes |
17
+ | `morph-apply` | Run the plan slices (dispatch, report-check, verify) into the review | yes |
19
18
  | `morph-review` | Final review report with SHIP/FIX verdict (Gate 3) | yes |
20
19
  | `morph-standards` | Locate and load a specific standard on demand | no |
21
20
 
@@ -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: "8.47.0"
7
+ cliVersion: "9.0.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: "8.47.0"
7
+ cliVersion: "9.0.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---