@polymorphism-tech/morph-spec 8.45.0 → 8.47.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 (146) hide show
  1. package/CLAUDE.md +6 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +2 -4
  4. package/bin/validate.js +4 -25
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +9 -9
  10. package/framework/commands/create-umbraco-lp.md +2 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +1 -1
  14. package/framework/commands/morph-infra.md +17 -203
  15. package/framework/commands/morph-media.md +3 -0
  16. package/framework/commands/morph-preflight.md +5 -28
  17. package/framework/commands/morph-proposal.md +2 -2
  18. package/framework/commands/morph-troubleshoot.md +2 -14
  19. package/framework/docs/CLI.md +8 -7
  20. package/framework/hooks/claude-code/core/state-sync.js +13 -1
  21. package/framework/hooks/shared/e2e-check.js +3 -4
  22. package/framework/hooks/shared/state-writer.js +1 -0
  23. package/framework/rules/ai-agents-standards.md +18 -16
  24. package/framework/rules/asaas-standards.md +3 -1
  25. package/framework/rules/csharp-standards.md +10 -8
  26. package/framework/rules/data-standards.md +5 -5
  27. package/framework/rules/dotnet-templates.md +1 -1
  28. package/framework/rules/frontend-standards.md +4 -2
  29. package/framework/rules/gsap-standards.md +10 -5
  30. package/framework/rules/hangfire-standards.md +3 -1
  31. package/framework/rules/infrastructure-standards.md +4 -2
  32. package/framework/rules/morph-workflow.md +2 -2
  33. package/framework/rules/neon-auth-standards.md +3 -1
  34. package/framework/rules/neon-standards.md +6 -4
  35. package/framework/rules/nextjs-standards.md +13 -11
  36. package/framework/rules/nextjs-templates.md +1 -1
  37. package/framework/rules/resend-standards.md +3 -1
  38. package/framework/schemas/config.schema.json +4 -0
  39. package/framework/schemas/feature.schema.json +4 -4
  40. package/framework/skills/README.md +1 -1
  41. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  42. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  43. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  44. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  45. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  46. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  47. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  48. package/framework/skills/core/morph-media/SKILL.md +3 -3
  49. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  50. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  51. package/framework/skills/core/morph-review/SKILL.md +30 -17
  52. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  53. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  54. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  55. package/framework/standards/STANDARDS.json +59 -56
  56. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  57. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  58. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  59. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  60. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  61. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  62. package/framework/standards/ai-agents/setup.md +1 -1
  63. package/framework/standards/ai-agents/structured-output.md +1 -1
  64. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  65. package/framework/standards/architecture/code-comments.md +2 -2
  66. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  67. package/framework/standards/backend/api/validation.md +135 -47
  68. package/framework/standards/backend/database/postgresql/database.md +11 -7
  69. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  70. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  71. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  72. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  73. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  74. package/framework/standards/backend/dotnet/testing.md +1 -1
  75. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  76. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  77. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  78. package/framework/standards/data/nosql/cache/redis.md +120 -44
  79. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  80. package/framework/standards/frontend/design-system/animations.md +1 -1
  81. package/framework/standards/frontend/design-system/naming.md +1 -1
  82. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  83. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  84. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  85. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  86. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  87. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  88. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  89. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  91. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  92. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  93. package/framework/templates/infrastructure/github/README.md +2 -1
  94. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  95. package/package.json +1 -1
  96. package/src/commands/observability/telemetry.js +3 -5
  97. package/src/commands/project/init.js +2 -1
  98. package/src/commands/project/update.js +16 -18
  99. package/src/commands/state/approve.js +17 -0
  100. package/src/commands/state/gate-check.js +4 -14
  101. package/src/commands/validation/verify.js +99 -57
  102. package/src/core/paths/output-schema.js +1 -1
  103. package/src/dashboard/app/App.jsx +0 -2
  104. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  105. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  106. package/src/dashboard/app/components/Markdown.jsx +1 -1
  107. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  108. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  109. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  110. package/src/dashboard/scanner/activity-scanner.js +1 -1
  111. package/src/dashboard/scanner/docs-scanner.js +5 -23
  112. package/src/dashboard/scanner/features-scanner.js +1 -1
  113. package/src/dashboard/scanner/index.js +0 -2
  114. package/src/dashboard/scanner/rules-scanner.js +15 -10
  115. package/src/dashboard/scanner/skills-scanner.js +2 -3
  116. package/src/lib/ci/ci-verify.js +118 -0
  117. package/src/lib/ci/github-ci.js +192 -0
  118. package/src/lib/ci/workflow-scaffold.js +122 -0
  119. package/src/lib/evaluator-report.js +13 -0
  120. package/src/lib/risk-detector.js +8 -0
  121. package/src/lib/stack/stack-profile.js +1 -1
  122. package/src/lib/telemetry/verify-event.js +3 -2
  123. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  124. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  125. package/src/lib/validators/validation-runner.js +8 -88
  126. package/src/lib/validators/verify-runner.js +22 -72
  127. package/src/scripts/setup-infra.js +33 -22
  128. package/src/utils/agents-installer.js +20 -8
  129. package/src/utils/file-copier.js +7 -5
  130. package/framework/commands/commit.md +0 -166
  131. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  132. package/framework/rubrics/backend.json +0 -41
  133. package/framework/rubrics/default.json +0 -36
  134. package/framework/rubrics/frontend.json +0 -36
  135. package/framework/templates/project/validate.js +0 -97
  136. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  137. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  138. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  139. package/src/lib/validators/architecture/index.js +0 -7
  140. package/src/lib/validators/css/css-validator.js +0 -340
  141. package/src/lib/validators/css/index.js +0 -7
  142. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  143. package/src/lib/validators/design-system/index.js +0 -7
  144. package/src/lib/validators/score-aggregator.js +0 -244
  145. package/src/lib/validators/ui/index.js +0 -7
  146. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -7,24 +7,14 @@
7
7
  * MORPH-SPEC 3.0 - Phase A: Active Validation Pipeline
8
8
  */
9
9
 
10
- import { existsSync, readFileSync } from 'fs';
11
- import { join } from 'path';
10
+ import { existsSync } from 'fs';
12
11
  import chalk from 'chalk';
13
12
  import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../core/paths/output-schema.js';
14
13
  import { getStackProfile } from '../stack/stack-profile.js';
15
- import { aggregateScores, loadRubric } from './score-aggregator.js';
16
14
 
17
- /**
18
- * Heavy validators skipped in 'light' mode (checkpoint-only).
19
- *
20
- * What light mode actually leaves behind is whatever the stack contributes that
21
- * is NOT in this set — in practice `contrast` and `nextjs-component`. There are
22
- * no `spec` or `content` validators; the switch in runSingleValidator has no
23
- * case for either, and the old comment here promised both.
24
- */
15
+ // Skipped in 'light' mode (checkpoint-only); light keeps `nextjs-component`.
25
16
  const HEAVY_VALIDATORS = new Set([
26
- 'architecture', 'packages', 'contract-compliance',
27
- 'css', 'design-system', 'reuse', 'code-budget'
17
+ 'packages', 'contract-compliance', 'reuse', 'code-budget'
28
18
  ]);
29
19
 
30
20
  /**
@@ -167,17 +157,12 @@ export async function runValidation(projectPath, featureName, options = {}) {
167
157
  result.issues.push(...issues);
168
158
 
169
159
  // A validator that skipped itself checked nothing: in `results` it would
170
- // be scored as a clean 10 and dilute the ones that ran.
160
+ // read as a validator that ran clean.
171
161
  if (!validatorResult._skipReason) result.results[validatorId] = { passed: errors === 0, issues };
172
162
  }
173
163
  } catch (err) {
174
- // A validator that throws is a FAILED validation, not a silent skip.
175
- // This used to record `passed: false` in `results` and stop there —
176
- // `result.errors` never moved, so `result.passed = errors === 0` stayed
177
- // true and the run reported "✅ All validations passed". The only trace
178
- // was a console line, and `verify` silences stdout. That is how a broken
179
- // import (`css`) and a path bug (`packages` under a different cwd) went
180
- // unnoticed: both threw, both read as clean.
164
+ // A validator that throws is a FAILED validation, not a silent skip:
165
+ // `verify` silences stdout, so a console line alone would read as clean.
181
166
  if (options.verbose) {
182
167
  console.log(chalk.yellow(` ⚠ Validator '${validatorId}' failed: ${err.message}`));
183
168
  }
@@ -208,17 +193,6 @@ export async function runValidation(projectPath, featureName, options = {}) {
208
193
  ? `✅ All validations passed (${result.warnings} warnings)`
209
194
  : `❌ ${result.errors} error(s), ${result.warnings} warning(s)`;
210
195
 
211
- // Score aggregation — layered on top of pass/fail
212
- try {
213
- const rubricId = readRubricConfig(projectPath);
214
- const rubric = loadRubric(projectPath, rubricId);
215
- const thresholdOverrides = readThresholdConfig(projectPath);
216
- result.scores = aggregateScores(result, rubric, thresholdOverrides);
217
- } catch {
218
- // Score aggregation is non-blocking — if it fails, pass/fail still works
219
- result.scores = null;
220
- }
221
-
222
196
  return result;
223
197
  }
224
198
 
@@ -243,7 +217,7 @@ function detectValidators(projectPath = '.') {
243
217
  * seven validators only two returned all three keys. `packages` — one of the
244
218
  * two defaults for the `dotnet` stack — returned
245
219
  * `{status, totalFiles, filesWithIssues, results}` and had every NuGet
246
- * incompatibility it found thrown away, while `verify` reported 10/10.
220
+ * incompatibility it found thrown away, while `verify` reported green.
247
221
  *
248
222
  * Reading the nested `results` shape here keeps that contract in ONE place
249
223
  * instead of asking every validator to remember it.
@@ -257,7 +231,7 @@ export function normalizeValidatorResult(validatorId, raw) {
257
231
 
258
232
  // Issues can be top-level, or nested one level under `results` — either as
259
233
  // an array of `{issues}` (packages: one entry per .csproj) or an object of
260
- // them (architecture: keyed by check name). The nested read is a FALLBACK,
234
+ // them keyed by check name. The nested read is a FALLBACK,
261
235
  // never additive: validators that expose both would otherwise report every
262
236
  // finding twice.
263
237
  const topLevel = Array.isArray(raw.issues) ? raw.issues : [];
@@ -299,38 +273,16 @@ export function normalizeValidatorResult(validatorId, raw) {
299
273
  */
300
274
  async function runSingleValidator(validatorId, projectPath, featureName, options) {
301
275
  switch (validatorId) {
302
- case 'architecture': {
303
- const { validateArchitecture } = await import('./architecture/architecture-validator.js');
304
- return await validateArchitecture(projectPath, options);
305
- }
306
-
307
276
  case 'packages': {
308
277
  const { validatePackages } = await import('./packages/package-validator.js');
309
278
  return await validatePackages(projectPath, options);
310
279
  }
311
280
 
312
- case 'contrast': {
313
- const { validateContrast } = await import('./ui/ui-contrast-validator.js');
314
- return await validateContrast(projectPath, options);
315
- }
316
-
317
- case 'css': {
318
- // The module exports `validateCssClasses`; this asked for `validateCss`,
319
- // which is undefined — the call threw on every invocation.
320
- const { validateCssClasses } = await import('./css/css-validator.js');
321
- return await validateCssClasses(projectPath, options);
322
- }
323
-
324
281
  case 'contract-compliance': {
325
282
  const { validateContracts } = await import('./contracts/contract-compliance-validator.js');
326
283
  return await validateContracts(projectPath, featureName, options);
327
284
  }
328
285
 
329
- case 'design-system': {
330
- const { validateDesignSystem } = await import('./design-system/design-system-validator.js');
331
- return await validateDesignSystem(projectPath, featureName, options);
332
- }
333
-
334
286
  case 'reuse': {
335
287
  const { validateReuse } = await import('./reuse/reuse-validator.js');
336
288
  return await validateReuse(projectPath, featureName, options);
@@ -414,35 +366,3 @@ export function formatValidationResults(result) {
414
366
 
415
367
  console.log('');
416
368
  }
417
-
418
- /**
419
- * Read rubric ID from project config.
420
- * @param {string} projectPath
421
- * @returns {string}
422
- */
423
- function readRubricConfig(projectPath) {
424
- try {
425
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
426
- if (!existsSync(configPath)) return 'default';
427
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
428
- return config.validation?.rubric ?? 'default';
429
- } catch {
430
- return 'default';
431
- }
432
- }
433
-
434
- /**
435
- * Read threshold overrides from project config.
436
- * @param {string} projectPath
437
- * @returns {Object} { approve?: number, retry?: number }
438
- */
439
- function readThresholdConfig(projectPath) {
440
- try {
441
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
442
- if (!existsSync(configPath)) return {};
443
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
444
- return config.validation?.thresholds ?? {};
445
- } catch {
446
- return {};
447
- }
448
- }
@@ -18,7 +18,6 @@
18
18
  * - applyTaskScope() from src/lib/tasks/test-scope.js (the task-scoped cut, #87)
19
19
  * - runValidation() from ./validation-runner.js
20
20
  * - runE2eCheck() from framework/hooks/shared/e2e-check.js
21
- * - loadRubric() + aggregateScores() from ./score-aggregator.js
22
21
  *
23
22
  * @module verify-runner
24
23
  */
@@ -44,7 +43,6 @@ import {
44
43
  evalEmptyRunProblem, evalColdCacheWarning,
45
44
  } from '../tasks/eval-node.js';
46
45
  import { runValidation } from './validation-runner.js';
47
- import { loadRubric, aggregateScores } from './score-aggregator.js';
48
46
  import { checkOutputsTraceability } from './traceability-check.js';
49
47
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
50
48
  import { createHeavyGate } from '../heavy/gate.js';
@@ -52,6 +50,9 @@ import { createHeavyGate } from '../heavy/gate.js';
52
50
  /** Default build timeout — dotnet cold build is slow; 30s (the Stop-hook default) is too short. */
53
51
  const DEFAULT_BUILD_TIMEOUT = 120_000;
54
52
 
53
+ // Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
54
+ const VALIDATOR_FEEDBACK_LIMIT = 10;
55
+
55
56
  /** Number of trailing output lines kept for feedback/reporting. */
56
57
  const OUTPUT_TAIL_LINES = 20;
57
58
 
@@ -87,7 +88,6 @@ export class VerifyUsageError extends Error {
87
88
  * @param {string} featureName - Feature name
88
89
  * @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
89
90
  * @param {Object} [options]
90
- * @param {string} [options.rubric] - Force a rubric id (highest precedence)
91
91
  * @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
92
92
  * (default: 120000 for build; for tests, see resolveTestTimeout())
93
93
  * @param {boolean} [options.skipTests] - Skip the tests node
@@ -121,7 +121,6 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
121
121
 
122
122
  async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
123
123
  const {
124
- rubric: rubricOverride,
125
124
  timeout = DEFAULT_BUILD_TIMEOUT,
126
125
  skipTests = false,
127
126
  skipBuild = false,
@@ -157,9 +156,9 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
157
156
  );
158
157
  }
159
158
 
160
- // The task is read ONCE: its group picks the rubric and the e2e
161
- // applicability and, with its outputs, the test scope (#87). One reader of
162
- // tasks.json for verify, so the three can never disagree about the task.
159
+ // The task is read ONCE: its group picks the e2e applicability and, with its
160
+ // outputs, the test scope. One reader of tasks.json for verify, so the two
161
+ // can never disagree about the task.
163
162
  const task = taskId ? readTask(projectPath, featureName, taskId) : null;
164
163
 
165
164
  // `--record-baseline` is judged in the same breath, and for the same reason:
@@ -253,8 +252,7 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
253
252
  appendTestsFeedback(feedback, nodes.tests);
254
253
 
255
254
  // 3. validators — ALWAYS runs, even on a red build (free static feedback).
256
- const rubricId = resolveRubricId({ task, rubricOverride, projectPath });
257
- nodes.validators = await runValidatorsNode({ projectPath, featureName, rubricId, _runValidation });
255
+ nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
258
256
  appendValidatorsFeedback(feedback, nodes.validators);
259
257
 
260
258
  // 4. e2e — the only node that runs the application. Like tests, it is not
@@ -298,7 +296,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
298
296
  task: taskId || null,
299
297
  scope: taskId ? 'task' : 'feature',
300
298
  stack,
301
- rubric: rubricId,
302
299
  status,
303
300
  nodes,
304
301
  feedback,
@@ -972,7 +969,7 @@ function recordBaselineFrom(projectPath, testsNode) {
972
969
  // Validators node
973
970
  // ============================================================================
974
971
 
975
- async function runValidatorsNode({ projectPath, featureName, rubricId, _runValidation }) {
972
+ async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
976
973
  const start = Date.now();
977
974
 
978
975
  // runValidation() prints chalk-formatted progress to stdout. verify emits
@@ -997,46 +994,40 @@ async function runValidatorsNode({ projectPath, featureName, rubricId, _runValid
997
994
  if (validationResult && validationResult._skipReason) {
998
995
  return {
999
996
  status: 'skip',
1000
- score: null,
1001
- action: null,
1002
997
  ran: [],
1003
998
  skipped,
1004
999
  reason: validationResult._skipReason,
1005
- priorityFixes: [],
1000
+ issues: [],
1006
1001
  durationMs,
1007
1002
  };
1008
1003
  }
1009
1004
 
1010
- const rubric = loadRubric(projectPath, rubricId);
1011
- const scoreResult = aggregateScores(validationResult, rubric);
1012
-
1013
1005
  return {
1014
1006
  status: validationResult.passed === false ? 'fail' : 'pass',
1015
- score: scoreResult.score,
1016
- action: scoreResult.action,
1017
1007
  ran: Object.keys(validationResult.results || {}),
1018
1008
  skipped,
1019
1009
  reason: null,
1020
- priorityFixes: scoreResult.priorityFixes || [],
1010
+ issues: validationResult.issues || [],
1021
1011
  durationMs,
1022
1012
  };
1023
1013
  }
1024
1014
 
1025
1015
  function appendValidatorsFeedback(feedback, validatorsNode) {
1026
1016
  // A validator that could not check (reuse without a graph) is invisible in a
1027
- // green score; when the whole node skipped, its own line already says why.
1017
+ // green node; when the whole node skipped, its own line already says why.
1028
1018
  if (validatorsNode.status !== 'skip') {
1029
1019
  for (const s of validatorsNode.skipped || []) {
1030
1020
  if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
1031
1021
  }
1032
1022
  }
1033
- for (const fix of validatorsNode.priorityFixes || []) {
1034
- const topIssue = fix.issues && fix.issues[0];
1035
- if (!topIssue) continue;
1036
- const location = topIssue.file
1037
- ? ` (${topIssue.file}${topIssue.line ? ':' + topIssue.line : ''})`
1038
- : '';
1039
- feedback.push(`[validators/${fix.category} ${fix.score}/10] ${topIssue.message}${location}`);
1023
+ const issues = validatorsNode.issues || [];
1024
+ const ordered = [
1025
+ ...issues.filter((i) => i.level === 'error'),
1026
+ ...issues.filter((i) => i.level !== 'error'),
1027
+ ].slice(0, VALIDATOR_FEEDBACK_LIMIT);
1028
+ for (const issue of ordered) {
1029
+ const location = issue.file ? ` (${issue.file}${issue.line ? ':' + issue.line : ''})` : '';
1030
+ feedback.push(`[validators/${issue.validator ?? 'unknown'}] ${issue.message}${location}`);
1040
1031
  }
1041
1032
  }
1042
1033
 
@@ -1105,42 +1096,12 @@ function appendE2eFeedback(feedback, e2eNode) {
1105
1096
  }
1106
1097
  }
1107
1098
 
1108
- // ============================================================================
1109
- // Rubric selection — flag > task group > config > default
1110
- // ============================================================================
1111
-
1112
- function resolveRubricId({ projectPath, task, rubricOverride }) {
1113
- if (rubricOverride) return rubricOverride;
1114
-
1115
- const mapped = mapGroupToRubric(task?.group);
1116
- if (mapped) return mapped;
1117
-
1118
- const configRubric = readConfigRubric(projectPath);
1119
- if (configRubric) return configRubric;
1120
-
1121
- return 'default';
1122
- }
1123
-
1124
- /**
1125
- * Map a task's `group` field to a rubric id.
1126
- * backend|infra → 'backend', frontend → 'frontend', tests|docs|absent → null
1127
- * (falls through to config/default).
1128
- */
1129
- function mapGroupToRubric(group) {
1130
- if (!group) return null;
1131
- const g = String(group).toLowerCase();
1132
- if (g === 'backend' || g === 'infra') return 'backend';
1133
- if (g === 'frontend') return 'frontend';
1134
- return null;
1135
- }
1136
-
1137
1099
  /**
1138
1100
  * The task as verify needs it, or null when tasks.json or the task is missing.
1139
1101
  *
1140
- * The ONE reader of tasks.json for verify (#87): the group picks the rubric and
1141
- * the e2e applicability, group + outputs pick the test scope, and the command
1142
- * uses the null to refuse an unknown task id. Two readers is how a rubric and
1143
- * a scope start disagreeing about the same task.
1102
+ * The ONE reader of tasks.json for verify: the group picks the e2e
1103
+ * applicability, group + outputs pick the test scope, and the command uses the
1104
+ * null to refuse an unknown task id.
1144
1105
  *
1145
1106
  * @param {string} projectPath
1146
1107
  * @param {string} featureName
@@ -1163,17 +1124,6 @@ export function readTask(projectPath, featureName, taskId) {
1163
1124
  }
1164
1125
  }
1165
1126
 
1166
- function readConfigRubric(projectPath) {
1167
- try {
1168
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
1169
- if (!existsSync(configPath)) return null;
1170
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
1171
- return config?.validation?.rubric ?? null;
1172
- } catch {
1173
- return null;
1174
- }
1175
- }
1176
-
1177
1127
  // ============================================================================
1178
1128
  // Shared helpers
1179
1129
  // ============================================================================
@@ -13,7 +13,7 @@
13
13
  import { join, dirname, resolve } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { execSync } from 'child_process';
16
- import { readFileSync, readdirSync } from 'fs';
16
+ import { readFileSync, readdirSync, existsSync } from 'fs';
17
17
  import {
18
18
  copyDirectory,
19
19
  copyFile,
@@ -89,7 +89,27 @@ export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = real
89
89
  }
90
90
 
91
91
  /**
92
- * Re-install only stack-aware assets (rules, skills, agents) using current config.json.
92
+ * Slash commands filtered by `stacks:` frontmatter. Clean-slate like the
93
+ * skills: removes only a framework command that the stack filtered out, never
94
+ * a command the user wrote into `.claude/commands/`.
95
+ */
96
+ export function installCommandsForStack(commandsSrc, commandsDest, projectTags, fx = realEffects()) {
97
+ const sourceFiles = readdirSync(commandsSrc).filter(f => f.endsWith('.md'));
98
+ let installed = 0;
99
+ for (const file of sourceFiles) {
100
+ const dest = join(commandsDest, file);
101
+ if (shouldInstall(parseStacks(readFileSync(join(commandsSrc, file), 'utf-8')), projectTags)) {
102
+ fx.copyFile(join(commandsSrc, file), dest);
103
+ installed++;
104
+ } else if (!fx.dryRun && existsSync(dest)) {
105
+ fx.remove(dest);
106
+ }
107
+ }
108
+ return { installed };
109
+ }
110
+
111
+ /**
112
+ * Re-install only stack-aware assets (commands, rules, skills, agents) using current config.json.
93
113
  * Skips infrastructure steps (dirs, framework copy, hooks).
94
114
  *
95
115
  * @param {string} targetPath - Absolute path to the target project directory
@@ -109,8 +129,16 @@ async function installStackAwareAssets(targetPath, log) {
109
129
  } catch { /* ignore malformed config */ }
110
130
  }
111
131
 
112
- log('Installing stack-filtered rules...');
113
132
  const claudeDest = join(targetPath, '.claude');
133
+ log('Installing stack-filtered commands...');
134
+ const commandsSrc = join(FRAMEWORK_DIR, 'commands');
135
+ const commandsDest = join(claudeDest, 'commands');
136
+ if (await pathExists(commandsSrc)) {
137
+ await ensureDir(commandsDest);
138
+ installCommandsForStack(commandsSrc, commandsDest, projectTags);
139
+ }
140
+
141
+ log('Installing stack-filtered rules...');
114
142
  const rulesSrc = join(FRAMEWORK_DIR, 'rules');
115
143
  const rulesDest = join(claudeDest, 'rules');
116
144
  if (await pathExists(rulesSrc)) {
@@ -336,18 +364,8 @@ export async function setupInfra(targetPath, {
336
364
  await copyDirectory(hooksSrc, hooksDest);
337
365
  }
338
366
 
339
- // --- 7b. Copy framework/rubrics → .morph/framework/rubrics ---
340
- log('Step 7b: Copying rubrics...');
341
- const rubricsSrc = join(FRAMEWORK_DIR, 'rubrics');
342
- const rubricsDest = join(frameworkDestDir, 'rubrics');
343
- if (await pathExists(rubricsSrc)) {
344
- await copyDirectory(rubricsSrc, rubricsDest);
345
- }
346
-
347
367
  // --- 7c. Copy framework/evals → .morph/framework/evals ---
348
- // Rubricas .md (bandas 0-10) + trace-schema.md — referenciadas por
349
- // morph-review/agents.json via .morph/framework/evals/.
350
- log('Step 7c: Copying evals (rubrics .md + trace schema)...');
368
+ log('Step 7c: Copying evals (review checklist + trace schema)...');
351
369
  const evalsSrc = join(FRAMEWORK_DIR, 'evals');
352
370
  const evalsDest = join(frameworkDestDir, 'evals');
353
371
  if (await pathExists(evalsSrc)) {
@@ -396,16 +414,9 @@ export async function setupInfra(targetPath, {
396
414
  await copyDirectory(schemasSrc, schemasDest);
397
415
  }
398
416
 
399
- // --- 9. Copy framework/commands → .claude/commands ---
400
- log('Step 9: Setting up Claude Code integration...');
401
417
  const claudeDest = join(targetPath, '.claude');
402
- const commandsSrc = join(FRAMEWORK_DIR, 'commands');
403
- const commandsDest = join(claudeDest, 'commands');
404
- if (await pathExists(commandsSrc)) {
405
- await copyDirectory(commandsSrc, commandsDest);
406
- }
407
418
 
408
- // --- 10-13. Install stack-aware assets (rules, skills, agents) ---
419
+ // --- 9-13. Install stack-aware assets (commands, rules, skills, agents) ---
409
420
  log('Steps 10-13: Installing stack-aware assets...');
410
421
  const { agentCounts, domainCounts, projectTags } = await installStackAwareAssets(targetPath, log);
411
422
 
@@ -431,10 +431,8 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
431
431
  // Canonical 1:1 naming: frontmatter name === filename === subagent_type.
432
432
  `name: morph-${agent.id}`,
433
433
  `description: ${description}`,
434
- // model is dimensioned per stage/criticality in agents.json (v5.2.0):
435
- // evaluator (quality multiplier) → opus, domain implementers → sonnet,
436
- // scout → sonnet (measured: same cost as haiku, fewer rejected maps). Falls back to `inherit` only for
437
- // personas that omit the field.
434
+ // The evaluator is sonnet here; the dispatch routes it to opus when the risk
435
+ // detector flags the diff. A persona without the field inherits the session model.
438
436
  `model: ${agent.model ?? 'inherit'}`,
439
437
  `tools: ${buildToolsLine(agent, knownMcpIds)}`,
440
438
  // Per persona in agents.json (ADR-004); absent → DEFAULT_MAX_TURNS. The
@@ -487,11 +485,25 @@ function buildBody(agent, maxTurns) {
487
485
  }
488
486
  }
489
487
 
488
+ if (isImplementer(agent)) {
489
+ lines.push(
490
+ '## How you operate',
491
+ '',
492
+ 'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
493
+ '',
494
+ );
495
+ } else {
496
+ // A read-only persona has no task to do nor doneCriteria to report.
497
+ lines.push(
498
+ '## Como você opera',
499
+ '',
500
+ 'Você é read-only: nunca escreva, edite nem reverta código, testes, `tasks.json` ou `feature.json` (nada de `git checkout --`). ' +
501
+ 'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
502
+ '',
503
+ );
504
+ }
505
+
490
506
  lines.push(
491
- '## How you operate',
492
- '',
493
- 'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
494
- '',
495
507
  // `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps an
496
508
  // evaluator partial from carrying a Verdict the gate would read as final.
497
509
  '## Turn budget',
@@ -199,10 +199,12 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
199
199
  content = await readFile(gitignorePath);
200
200
  hasMorphSection = content.includes('# MORPH-SPEC');
201
201
  }
202
+ const original = content;
203
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
202
204
 
203
205
  // If MORPH section already exists, check if rules are up to date
204
206
  if (hasMorphSection) {
205
- const lines = content.split('\n');
207
+ const lines = content.split(/\r?\n/);
206
208
  const morphStartIndex = lines.findIndex(line => line.trim() === '# MORPH-SPEC');
207
209
 
208
210
  // Find the end of MORPH section (next empty line or section header)
@@ -225,17 +227,17 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
225
227
  if (missingRules.length > 0) {
226
228
  // Update the section with all rules
227
229
  lines.splice(morphStartIndex, morphEndIndex - morphStartIndex, ...morphRules.slice(1, -1));
228
- content = lines.join('\n');
230
+ content = lines.join(eol);
229
231
  }
230
232
  } else {
231
233
  // Add MORPH section
232
234
  if (content && !content.endsWith('\n')) {
233
- content += '\n';
235
+ content += eol;
234
236
  }
235
- content += morphRules.join('\n');
237
+ content += morphRules.join(eol);
236
238
  }
237
239
 
238
- fx.writeFile(gitignorePath, content);
240
+ if (content !== original) fx.writeFile(gitignorePath, content);
239
241
  }
240
242
 
241
243
  /**