@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
@@ -39,7 +39,8 @@ infrastructure/github/
39
39
  - Push to container registry (GHCR or Docker Hub)
40
40
 
41
41
  ### Deploy Coolify (`deploy-coolify.yml.hbs`)
42
- - SSHes into the VPS and calls the local Coolify API (`localhost:8000/api/v1/deploy?uuid=...`) after Docker push
42
+ - SSHes into the VPS and calls the local Coolify API (`POST localhost:8000/api/v1/deploy?uuid=...`) after Docker push; the step fails unless the response carries a `deployment_uuid`
43
+ - The token is read on the VPS from `~/<token-file>` (secret `COOLIFY_TOKEN_FILE` holds the file name) and never leaves the SSH session
43
44
  - Runs health check after deployment
44
45
  - Supports staging + production environments
45
46
 
@@ -25,7 +25,8 @@ jobs:
25
25
  - name: Trigger Coolify Deploy (via SSH — the Coolify API is not publicly exposed)
26
26
  run: |
27
27
  ssh -i ~/.ssh/id_ed25519 ${{{{ secrets.COOLIFY_SSH_USER }}}}@${{{{ secrets.COOLIFY_SSH_HOST }}}} \
28
- 'T=$(cat ~/.coolify-token); curl -s -X GET -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=${{{{ secrets.COOLIFY_APPLICATION_UUID }}}}"'
28
+ 'T=$(cat ~/${{{{ secrets.COOLIFY_TOKEN_FILE }}}}); curl -s --fail-with-body -X POST -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=${{{{ secrets.COOLIFY_APPLICATION_UUID }}}}"' \
29
+ | grep -q deployment_uuid
29
30
 
30
31
  - name: Health Check
31
32
  run: |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.45.0",
3
+ "version": "8.47.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -12,12 +12,10 @@
12
12
  import chalk from 'chalk';
13
13
  import { loadTelemetry, aggregateTelemetry } from '../../lib/telemetry/telemetry-reader.js';
14
14
 
15
- // The knowledge-base count reflects EXPLICIT reads only. The core standards
16
- // load via @-refs in rules (Layer 0/1 auto-load) — those are injected by
17
- // Claude Code's rules system, not via the Read tool, so no hook ever sees them.
18
- // A low count here means "few on-demand reads", NOT "standards barely used".
15
+ // A rule injects its standards' digests through Claude Code's rules system,
16
+ // not the Read tool, so no hook sees them: a low count means "few full-text reads".
19
17
  const STANDARDS_CAVEAT =
20
- 'Só reads explícitos (via Read) são observáveis; standards auto-carregados por @-ref nas rules (Layer 0/1) não passam por tool call e ficam invisíveis aqui.';
18
+ 'Só reads explícitos (via Read) são observáveis; o resumo que uma rule injeta ao tocar arquivo casado por `paths:` não passa por tool call: não observável aqui.';
21
19
 
22
20
  function section(title, rows, fmt) {
23
21
  if (!rows.length) return;
@@ -28,7 +28,7 @@ import {
28
28
  setupStitch,
29
29
  loadMcpRegistry
30
30
  } from '../../lib/installers/mcp-installer.js';
31
- import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
31
+ import { setupInfra, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
32
32
  import { installSkills } from '../../utils/skills-installer.js';
33
33
  import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
34
34
  import { getProjectTags } from '../../lib/stack-filter.js';
@@ -263,6 +263,7 @@ export async function initCommand(options) {
263
263
  const rulesSrc = join(fwSrcDir, 'rules');
264
264
  const rulesDest = join(targetPath, '.claude', 'rules');
265
265
  installRulesForStack(rulesSrc, rulesDest, stackTags);
266
+ installCommandsForStack(join(fwSrcDir, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
266
267
 
267
268
  // Skills
268
269
  await installSkills(targetPath, { projectTags: stackTags });
@@ -30,7 +30,7 @@ import { installSkills } from '../../utils/skills-installer.js';
30
30
  import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
31
31
  import { injectMorphImport } from '../../utils/claude-md-injector.js';
32
32
  import { getProjectTags } from '../../lib/stack-filter.js';
33
- import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
33
+ import { updateClaudeMdAgentsBlock, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
34
34
  import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
35
35
  import { createEffects } from '../../lib/installers/effects.js';
36
36
  import { applyEditorSettings } from '../../lib/worktree/vscode-settings.js';
@@ -84,6 +84,7 @@ export function frameworkDirsToClean(morphPath, targetPath) {
84
84
  join(morphPath, 'framework', 'standards'),
85
85
  join(morphPath, 'framework', 'hooks'),
86
86
  join(morphPath, 'framework', 'schemas'),
87
+ // Não é mais instalado: fica na lista para apagar o de instalações antigas.
87
88
  join(morphPath, 'framework', 'rubrics'),
88
89
  join(morphPath, 'framework', 'evals'),
89
90
  join(morphPath, 'config'),
@@ -594,10 +595,8 @@ export async function updateCommand(options = {}) {
594
595
  fx.copyFile(agentsSrc, agentsDest);
595
596
  }
596
597
 
597
- // Update the remaining framework payloads (parity with setup-infra 7b/7c/8b/8c/8d) —
598
- // MORPH.md, phases.json, schemas/, rubrics/, evals/. Without this, upgrades left
599
- // the installed charter/rubrics/eval assets stale forever.
600
- updateSpinner.text = 'Updating charter, schemas, rubrics and evals...';
598
+ // Paridade com setup-infra 7c/8b/8c/8d: sem isto o charter e os evals instalados ficam velhos.
599
+ updateSpinner.text = 'Updating charter, schemas and evals...';
601
600
  const canonicalFrameworkDir = join(__dirname, '..', '..', '..', 'framework');
602
601
  for (const file of ['MORPH.md', 'phases.json']) {
603
602
  const src = join(canonicalFrameworkDir, file);
@@ -605,7 +604,7 @@ export async function updateCommand(options = {}) {
605
604
  fx.copyFile(src, join(morphPath, 'framework', file));
606
605
  }
607
606
  }
608
- for (const dir of ['schemas', 'rubrics', 'evals']) {
607
+ for (const dir of ['schemas', 'evals']) {
609
608
  const src = join(canonicalFrameworkDir, dir);
610
609
  if (await pathExists(src)) {
611
610
  fx.copyDir(src, join(morphPath, 'framework', dir));
@@ -644,18 +643,6 @@ export async function updateCommand(options = {}) {
644
643
  const frameworkDir = join(__dirname, '..', '..', '..', 'framework');
645
644
  const claudeDest = join(targetPath, '.claude');
646
645
 
647
- let commandsCopied = false;
648
-
649
- // Copy commands directory (slash commands): framework/commands/ → .claude/commands/
650
- const commandsSrc = join(frameworkDir, 'commands');
651
- const commandsDest = join(claudeDest, 'commands');
652
- if (await pathExists(commandsSrc)) {
653
- fx.copyDir(commandsSrc, commandsDest);
654
- commandsCopied = true;
655
- } else {
656
- logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
657
- }
658
-
659
646
  // Compute project tags for stack-aware installation of skills, agents, and rules.
660
647
  // MUST come from the pre-clean backup: cleanFrameworkDirs() deleted .morph/config/
661
648
  // above and restoreUserConfig() only runs at the end — re-reading the file here
@@ -666,6 +653,17 @@ export async function updateCommand(options = {}) {
666
653
  if (configBackup) projectTags = getProjectTags(configBackup);
667
654
  } catch { /* ignore — malformed config */ }
668
655
 
656
+ let commandsCopied = false;
657
+ const commandsSrc = join(frameworkDir, 'commands');
658
+ if (await pathExists(commandsSrc)) {
659
+ const commandsDest = join(claudeDest, 'commands');
660
+ fx.ensureDir(commandsDest);
661
+ installCommandsForStack(commandsSrc, commandsDest, projectTags, fx);
662
+ commandsCopied = true;
663
+ } else {
664
+ logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
665
+ }
666
+
669
667
  // Sync morph skills to .claude/skills/ for native Claude Code discovery
670
668
  updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
671
669
  const skillsResult = await installSkills(targetPath, { projectTags, fx });
@@ -6,6 +6,9 @@ import { join } from 'path';
6
6
  import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
7
7
  import { unmetPrereqs } from '../../core/state/gate-order.js';
8
8
  import { evaluatorReportState, VERDICT_TOKEN } from '../../lib/evaluator-report.js';
9
+ import { shouldPauseGate } from '../../lib/risk-detector.js';
10
+ import { collectSignals } from './gate-check.js';
11
+ import { readTrustFromConfig } from './gate-decision.js';
9
12
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
13
  import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
11
14
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
@@ -351,6 +354,20 @@ export async function approveCommand(featureName, gateName, options = {}) {
351
354
  selfAssessed = true;
352
355
  }
353
356
 
357
+ // Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
358
+ // campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
359
+ // a decisão pela MESMA régua do gate-check. --self-assessed já dispensou o veredito.
360
+ if (approvedBy === 'auto') {
361
+ const signals = collectSignals(featureName, feature, process.cwd(), 'review');
362
+ if (selfAssessed) signals.evaluatorVerdict = { status: 'ok', verdict: 'SHIP', blockers: 0, untyped: 0 };
363
+ const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
364
+ if (pause) {
365
+ console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
366
+ console.error(chalk.gray(' Corrija e rode de novo, ou leve a decisão ao humano (--approver <nome>).\n'));
367
+ process.exit(1);
368
+ }
369
+ }
370
+
354
371
  if (report.status === 'ok' && report.superseded.length > 0) {
355
372
  console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
356
373
  console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { getFeature } from '../../core/state/state-manager.js';
4
4
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
5
5
  import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
6
- import { evaluatorReportState, evaluatorMediaSection } from '../../lib/evaluator-report.js';
6
+ import { evaluatorReportState, evaluatorMediaSection, verdictSignal } from '../../lib/evaluator-report.js';
7
7
  import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
8
8
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
9
9
  import { minimatch } from 'minimatch';
@@ -151,7 +151,7 @@ function collectPromptModel(cwd, defaultBranch) {
151
151
  * @param {string} gateName
152
152
  * @returns {object}
153
153
  */
154
- function collectSignals(featureName, feature, cwd, gateName) {
154
+ export function collectSignals(featureName, feature, cwd, gateName) {
155
155
  const tasks = readTasks(featureName);
156
156
  const defaultBranch = detectDefaultBranch(cwd);
157
157
  const sha = getHeadSha(cwd);
@@ -179,17 +179,7 @@ function collectSignals(featureName, feature, cwd, gateName) {
179
179
  }
180
180
 
181
181
  if (gateName === 'review') {
182
- const report = evaluatorReportState(cwd, featureName);
183
- signals.evaluatorVerdict = report.status === 'ok'
184
- ? {
185
- status: 'ok',
186
- verdict: report.verdict,
187
- blockers: report.blockers.length,
188
- blockerIds: report.blockers.map((b) => b.id),
189
- untyped: report.untyped.length,
190
- superseded: report.superseded,
191
- }
192
- : { status: report.status };
182
+ signals.evaluatorVerdict = verdictSignal(evaluatorReportState(cwd, featureName));
193
183
  signals.mediaClaims = {
194
184
  section: evaluatorMediaSection(cwd, featureName),
195
185
  featureGeneratesMedia: featureGeneratesMedia(tasks),
@@ -269,7 +259,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
269
259
  process.exit(1);
270
260
  }
271
261
 
272
- // Owner-tree guard: gate-check reads workType/tasks/scores/git off cwd
262
+ // Owner-tree guard: gate-check reads workType/tasks/verdict/git off cwd
273
263
  // AND writes the resulting decision through persistGateDecision — both
274
264
  // halves are wrong from a tree that isn't this feature's own, so refuse
275
265
  // rather than compute a decision from stale reads and then fail to save it.
@@ -4,7 +4,7 @@ import { getFeature, updateFeature } from '../../core/state/state-manager.js';
4
4
  import { runVerify, readTask, VerifyUsageError } from '../../lib/validators/verify-runner.js';
5
5
  import { resolvePrimaryRoot } from '../project/worktree.js';
6
6
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
7
- import { canonicalPath } from '../../lib/tasks/test-runner.js';
7
+ import { canonicalPath, readMorphConfig } from '../../lib/tasks/test-runner.js';
8
8
  import { getHeadSha } from '../../lib/git/default-branch.js';
9
9
  import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
10
10
  import { buildVerifyEvent, recordVerifyEvent, findRepeatedVerify } from '../../lib/telemetry/verify-event.js';
@@ -12,6 +12,7 @@ import { treeFingerprint } from '../../lib/git/tree-fingerprint.js';
12
12
  import { HeavyBusyError } from '../../lib/heavy/gate.js';
13
13
  import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
14
14
  import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
15
+ import { runCiVerify, CiUsageError } from '../../lib/ci/ci-verify.js';
15
16
 
16
17
  /** ✓ pass · ✗ fail · – skip */
17
18
  function statusIcon(status) {
@@ -42,18 +43,13 @@ function selectionLines(runs = []) {
42
43
  });
43
44
  }
44
45
 
45
- /**
46
- * Human-readable rendering of a runVerify() result — mirrors the
47
- * ✓/✗-per-node + feedback-list style of formatScoreBreakdown() in
48
- * score-aggregator.js, adapted to verify's build/tests/validators nodes.
49
- */
50
46
  function formatVerifyResult(result) {
51
47
  const lines = [];
52
48
  const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
53
49
 
54
50
  lines.push('');
55
51
  lines.push(chalk.bold(`Verify — ${scopeLabel}`));
56
- lines.push(chalk.gray(`rubric: ${result.rubric}${result.stack ? ` | stack: ${result.stack}` : ''}`));
52
+ if (result.stack) lines.push(chalk.gray(`stack: ${result.stack}`));
57
53
  lines.push('');
58
54
  lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
59
55
  lines.push('');
@@ -68,8 +64,6 @@ function formatVerifyResult(result) {
68
64
  let detail;
69
65
  if (node.status === 'skip') {
70
66
  detail = node.reason || '';
71
- } else if (name === 'validators' && node.score != null) {
72
- detail = `score ${node.score}/10`;
73
67
  } else if (node.reason) {
74
68
  // A green e2e still carries meaning worth showing: smoke-only vs specs,
75
69
  // warm reuse vs rebuild. Without it, "pass" hides how much was proven.
@@ -190,6 +184,53 @@ function evalsStatusOf(result) {
190
184
  */
191
185
  const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
192
186
 
187
+ const CI_EXCLUSIVE_FLAGS = [
188
+ ['filter', '--filter'], ['skipTests', '--skip-tests'], ['skipBuild', '--skip-build'], ['skipE2e', '--skip-e2e'],
189
+ ['freshE2e', '--fresh-e2e'], ['linux', '--linux'], ['recordBaseline', '--record-baseline'], ['refreshCache', '--refresh-cache'],
190
+ ['noPersist', '--no-persist'],
191
+ ];
192
+
193
+ async function runLocalVerify(featureName, taskId, options) {
194
+ try {
195
+ return await runVerify(process.cwd(), featureName, taskId || null, {
196
+ timeout: options.timeout,
197
+ skipTests: options.skipTests,
198
+ skipBuild: options.skipBuild,
199
+ skipE2e: options.skipE2e,
200
+ freshE2e: options.freshE2e,
201
+ filter: options.filter,
202
+ recordBaseline: options.recordBaseline,
203
+ refreshCache: options.refreshCache,
204
+ linux: options.linux,
205
+ linuxImage: options.linuxImage,
206
+ // Task scope: switch the recorte off (#87). At feature scope nothing is
207
+ // ever narrowed, so the flag is a documented no-op there, not an error.
208
+ full: options.full,
209
+ });
210
+ } catch (err) {
211
+ // A wrong invocation is exit 2, like an unknown feature or task — never a
212
+ // verdict. `--filter` that selects nothing must not print green (or red):
213
+ // there is no verification to report, only a command to fix.
214
+ if (err instanceof VerifyUsageError) {
215
+ console.error(chalk.red(`\n❌ ${err.message}\n`));
216
+ process.exit(2);
217
+ }
218
+ // Ocupado não é veredito: nada foi verificado, então não há stamp nem
219
+ // evento, e o exit 3 nunca é "vermelho".
220
+ if (err instanceof HeavyBusyError) {
221
+ if (options.json) {
222
+ const holders = err.holders.map(({ pid, label, feature, node, since }) => ({ pid, label, feature, node, since }));
223
+ console.log(JSON.stringify({
224
+ status: 'busy', persisted: false, busy: { reason: err.reason, holders, freeGb: err.freeGb, waitedMs: err.waitedMs },
225
+ }, null, 2));
226
+ }
227
+ console.error(chalk.yellow(`\n⏳ verify não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\` mostra quem segura os slots).\n`));
228
+ process.exit(3);
229
+ }
230
+ throw err;
231
+ }
232
+ }
233
+
193
234
  /**
194
235
  * `morph-spec verify <feature> [task]` — the deterministic code-before-judge
195
236
  * gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
@@ -202,8 +243,8 @@ const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'],
202
243
  * Exit 2 is "the command cannot mean what you typed", and there are four
203
244
  * paths to it — never a verdict, because a verdict about nothing is worse
204
245
  * than a refusal:
205
- * - unknown feature, or unknown task id (the task's `group` decides the
206
- * rubric, so it is a hard usage error here);
246
+ * - unknown feature, or unknown task id (the task's `group` and `outputs`
247
+ * decide the scope, so it is a hard usage error here);
207
248
  * - `--filter` that matches no declared node (a filtered-to-empty run is
208
249
  * green and proves nothing);
209
250
  * - `--record-baseline` with nowhere to write (no `config.json`), or with a
@@ -223,6 +264,14 @@ export async function verifyCommand(featureName, taskId, options = {}) {
223
264
  process.exit(2);
224
265
  }
225
266
 
267
+ if (options.ci) {
268
+ const conflict = taskId ? `a task ${taskId} (o loop por task é local)` : CI_EXCLUSIVE_FLAGS.find(([flag]) => options[flag])?.[1];
269
+ if (conflict) {
270
+ console.error(chalk.red(`\n❌ --ci roda a suíte inteira da feature no CI; não combina com ${conflict}\n`));
271
+ process.exit(2);
272
+ }
273
+ }
274
+
226
275
  // The runner's own reader (one reader of tasks.json for verify, #87): null
227
276
  // means tasks.json or the task is missing, and both are this usage error.
228
277
  if (taskId) {
@@ -232,51 +281,36 @@ export async function verifyCommand(featureName, taskId, options = {}) {
232
281
  }
233
282
  }
234
283
 
235
- // Taken before the run: it is the tree the run is about to read.
236
- const fingerprint = treeFingerprint(process.cwd());
237
-
238
- let result;
239
- try {
240
- result = await runVerify(process.cwd(), featureName, taskId || null, {
241
- rubric: options.rubric,
242
- timeout: options.timeout,
243
- skipTests: options.skipTests,
244
- skipBuild: options.skipBuild,
245
- skipE2e: options.skipE2e,
246
- freshE2e: options.freshE2e,
247
- filter: options.filter,
248
- recordBaseline: options.recordBaseline,
249
- refreshCache: options.refreshCache,
250
- linux: options.linux,
251
- linuxImage: options.linuxImage,
252
- // Task scope: switch the recorte off (#87). At feature scope nothing is
253
- // ever narrowed, so the flag is a documented no-op there, not an error.
254
- full: options.full,
255
- });
256
- } catch (err) {
257
- // A wrong invocation is exit 2, like an unknown feature or task — never a
258
- // verdict. `--filter` that selects nothing must not print green (or red):
259
- // there is no verification to report, only a command to fix.
260
- if (err instanceof VerifyUsageError) {
261
- console.error(chalk.red(`\n❌ ${err.message}\n`));
262
- process.exit(2);
263
- }
264
- // Ocupado não é veredito: nada foi verificado, então não há stamp nem
265
- // evento, e o exit 3 nunca é "vermelho".
266
- if (err instanceof HeavyBusyError) {
267
- if (options.json) {
268
- const holders = err.holders.map(({ pid, label, feature, node, since }) => ({ pid, label, feature, node, since }));
269
- console.log(JSON.stringify({
270
- status: 'busy', persisted: false, busy: { reason: err.reason, holders, freeGb: err.freeGb, waitedMs: err.waitedMs },
271
- }, null, 2));
284
+ let ci = null;
285
+ if (options.ci) {
286
+ try {
287
+ const outcome = await runCiVerify(process.cwd(), featureName, readMorphConfig(process.cwd()), {
288
+ log: (line) => console.error(chalk.gray(` ${line}`)),
289
+ });
290
+ if (outcome.mode === 'ci') {
291
+ ci = outcome;
292
+ } else if (outcome.mode === 'pending') {
293
+ if (options.json) console.log(JSON.stringify({ status: 'pending', persisted: false, reason: outcome.reason }, null, 2));
294
+ console.error(chalk.yellow(`\n⏳ ${outcome.reason}. Não é vermelho e não há stamp: não repita em laço — siga e deixe o gate-check decidir.\n`));
295
+ process.exit(3);
296
+ } else {
297
+ console.error(chalk.yellow(`\n↪ CI indisponível (${outcome.reason}) — verify local.\n`));
272
298
  }
273
- console.error(chalk.yellow(`\n⏳ verify não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\` mostra quem segura os slots).\n`));
274
- process.exit(3);
299
+ } catch (err) {
300
+ if (err instanceof CiUsageError) {
301
+ console.error(chalk.red(`\n❌ ${err.message}\n`));
302
+ process.exit(2);
303
+ }
304
+ throw err;
275
305
  }
276
- throw err;
277
306
  }
278
307
 
279
- const cleanup = afterHeavyRun({
308
+ // Taken before the run: it is the tree the run is about to read.
309
+ const fingerprint = treeFingerprint(process.cwd());
310
+
311
+ const result = ci ? ci.result : await runLocalVerify(featureName, taskId, options);
312
+
313
+ const cleanup = ci ? { status: 'skipped', reason: 'ci' } : afterHeavyRun({
280
314
  projectPath: process.cwd(),
281
315
  config: readHeavyConfig(process.cwd()),
282
316
  scope: taskId ? 'task' : 'feature',
@@ -303,7 +337,6 @@ export async function verifyCommand(featureName, taskId, options = {}) {
303
337
  compact = {
304
338
  status: result.status,
305
339
  nodes: nodeStatuses(result),
306
- score: result.nodes.validators.score ?? null,
307
340
  attempts: (prevEntry?.attempts ?? 0) + 1,
308
341
  updatedAt: now,
309
342
  };
@@ -329,9 +362,10 @@ export async function verifyCommand(featureName, taskId, options = {}) {
329
362
  status: result.status,
330
363
  nodes: nodeStatuses(result),
331
364
  e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
332
- sha: getHeadSha(process.cwd()),
365
+ sha: ci ? ci.run.headSha : getHeadSha(process.cwd()),
333
366
  ts: now,
334
367
  scope: taskId ? `task:${taskId}` : 'feature',
368
+ ...(ci ? { source: 'ci', ciRun: ci.run.url } : {}),
335
369
  };
336
370
  await updateFeature(featureName, 'verification', stamp);
337
371
  }
@@ -341,9 +375,9 @@ export async function verifyCommand(featureName, taskId, options = {}) {
341
375
  let repeatOf = null;
342
376
  try {
343
377
  const cwd = process.cwd();
344
- if (!taskId && persist) repeatOf = findRepeatedVerify(cwd, { feature: featureName, task: null, scope: result.scope, fingerprint });
378
+ if (!taskId && persist && !ci) repeatOf = findRepeatedVerify(cwd, { feature: featureName, task: null, scope: result.scope, fingerprint });
345
379
  const tree = resolve(cwd).replace(/\\/g, '/') === resolvePrimaryRoot(cwd) ? 'root' : 'worktree';
346
- recordVerifyEvent(cwd, buildVerifyEvent(result, { sha: getHeadSha(cwd), tree, persisted: persist, reason, fingerprint }));
380
+ recordVerifyEvent(cwd, buildVerifyEvent(result, { sha: ci ? ci.run.headSha : getHeadSha(cwd), tree, persisted: persist, reason, fingerprint, source: ci ? 'ci' : 'local' }));
347
381
  } catch { /* telemetry never changes the verdict */ }
348
382
 
349
383
  // The refusal always goes to stderr, so it survives --json (whose stdout has
@@ -355,10 +389,18 @@ export async function verifyCommand(featureName, taskId, options = {}) {
355
389
  }
356
390
 
357
391
  if (options.json) {
358
- const output = { ...result, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup };
392
+ const output = {
393
+ ...result, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup,
394
+ ...(ci ? { source: 'ci', ciRun: { id: ci.run.databaseId, url: ci.run.url, sha: ci.run.headSha }, workflowCreated: ci.created } : {}),
395
+ };
359
396
  console.log(JSON.stringify(output, null, 2));
360
397
  } else {
361
398
  console.log(formatVerifyResult(result));
399
+ if (ci) {
400
+ console.log(chalk.cyan(`fonte: CI ${ci.run.url}`));
401
+ if (result.status === 'fail') console.log(chalk.gray(` log das falhas: gh run view ${ci.run.databaseId} --log-failed`));
402
+ console.log('');
403
+ }
362
404
  if (cleanup.status !== 'skipped') {
363
405
  const mark = cleanup.status === 'ran' ? chalk.green('✓') : chalk.yellow('⚠');
364
406
  console.log(chalk.gray(`${mark} dotnet build-server shutdown: ${cleanup.status}${cleanup.reason ? ` (${cleanup.reason})` : ''}\n`));
@@ -74,7 +74,7 @@ export const OUTPUT_SCHEMA = {
74
74
  // (morph-plan §5). Runtime evidence: input, model, output, verdict — pointing
75
75
  // at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
76
76
  // construction: never in phases.json requiredOutputs. Readers: morph-review
77
- // dispatch prompt, proof rubric eval-de-modelo, test-coverage Q7.
77
+ // dispatch prompt.
78
78
  evidencias: { dirname: 'evidencias', filenamePattern: '^T\\d+\\.md$', displayFilename: '{taskId}.md',
79
79
  phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
80
80
 
@@ -14,7 +14,6 @@ import SkillsLibrary from './pages/SkillsLibrary.jsx';
14
14
  import RulesMap from './pages/RulesMap.jsx';
15
15
  import Standards from './pages/Standards.jsx';
16
16
  import Guide from './pages/Guide.jsx';
17
- import Rubrics from './pages/Rubrics.jsx';
18
17
  import { useNow } from './hooks/usePref.js';
19
18
  import { useProjects, useCurrentProject, projectHref } from './lib/project.js';
20
19
 
@@ -77,7 +76,6 @@ export default function App() {
77
76
  <Route path="/p/:project/activity" element={<Activity />} />
78
77
  <Route path="/guide" element={<Guide />} />
79
78
  <Route path="/guide/:doc" element={<Guide />} />
80
- <Route path="/rubrics" element={<Rubrics />} />
81
79
  <Route path="/agents" element={<AgentMap />} />
82
80
  <Route path="/phases" element={<PhasePipeline />} />
83
81
  <Route path="/hooks" element={<HookInspector />} />
@@ -9,12 +9,12 @@ import { apiBase, projectHref, featureHref } from '../lib/project.js';
9
9
 
10
10
  const KIND_LABEL = {
11
11
  page: 'Página', project: 'Projeto', feature: 'Feature', task: 'Task', skill: 'Skill', persona: 'Persona',
12
- standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia', rubric: 'Rubrica',
12
+ standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia',
13
13
  };
14
14
 
15
15
  const PAGES = [
16
16
  ['Portfólio', '/'], ['Guia', '/guide'], ['Fases', '/phases'], ['Skills', '/skills'],
17
- ['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'], ['Rubricas', '/rubrics'],
17
+ ['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'],
18
18
  ];
19
19
 
20
20
  /** Everything the framework knows that has a page — built once. */
@@ -35,7 +35,6 @@ function knowledgeIndex() {
35
35
  out.push({ kind: 'guide', title: h.text, hint: d.title, to: `/guide/${d.id}#${h.slug}`, hay: `${h.text} ${d.title}` });
36
36
  }
37
37
  }
38
- for (const r of morphData.docs?.rubrics || []) out.push({ kind: 'rubric', title: r.title, to: `/rubrics?sel=${encodeURIComponent(`dim:${r.id}`)}`, hay: r.title });
39
38
  return out;
40
39
  }
41
40
 
@@ -16,7 +16,6 @@ export function entityHref(kind, id) {
16
16
  case 'hook': return q('hooks');
17
17
  case 'phase': return q('phases');
18
18
  case 'guide': return `/guide/${encodeURIComponent(id)}`;
19
- case 'rubric': return q('rubrics');
20
19
  default: return '#';
21
20
  }
22
21
  }
@@ -7,7 +7,7 @@ const REMARK = [remarkGfm];
7
7
  const REHYPE = [rehypeSlug];
8
8
 
9
9
  /** Frontmatter keys worth showing, in the order a reader wants them. */
10
- const META_ORDER = ['name', 'description', 'when-to-invoke', 'user-invokable', 'stacks', 'version', 'cliVersion'];
10
+ const META_ORDER = ['name', 'description', 'when_to_use', 'user-invocable', 'stacks', 'version', 'cliVersion'];
11
11
 
12
12
  function MetaBlock({ meta }) {
13
13
  const keys = Object.keys(meta).sort((a, b) => {
@@ -1,6 +1,6 @@
1
1
  import { NavLink, useLocation, useNavigate } from 'react-router-dom';
2
2
  import {
3
- MagnifyingGlass, SquaresFour, Kanban, Pulse, BookOpen, FlowArrow, PuzzlePiece, UsersThree, Books, Ruler, Plug, Scales,
3
+ MagnifyingGlass, SquaresFour, Kanban, Pulse, BookOpen, FlowArrow, PuzzlePiece, UsersThree, Books, Ruler, Plug,
4
4
  } from '@phosphor-icons/react';
5
5
  import { projectHref } from '../lib/project.js';
6
6
 
@@ -12,7 +12,6 @@ const KNOWLEDGE = [
12
12
  { to: '/standards', label: 'Standards', icon: Books },
13
13
  { to: '/rules', label: 'Rules', icon: Ruler },
14
14
  { to: '/hooks', label: 'Hooks', icon: Plug },
15
- { to: '/rubrics', label: 'Rubricas', icon: Scales },
16
15
  ];
17
16
 
18
17
  function Badge({ pulse }) {
@@ -15,7 +15,7 @@ const NODE_W = 260;
15
15
  // ─── Standard resolution ──────────────────────────────────────────────────────
16
16
 
17
17
  /**
18
- * A rule's @-import → the standard it names in the registry. `id` is what the
18
+ * A rule's route → the standard it names in the registry. `id` is what the
19
19
  * Standards page selects by; with no registry match the path itself is passed,
20
20
  * so the link still says which file the rule meant.
21
21
  */
@@ -196,7 +196,7 @@ function RuleDetail({ rule, byPath }) {
196
196
  )}
197
197
  </Section>
198
198
 
199
- <Section title="Standards importados">
199
+ <Section title="Standards roteados (resumo)">
200
200
  {rule.standardRefs.length > 0 ? (
201
201
  <div className="flex flex-wrap gap-1.5">
202
202
  {rule.standardRefs.map((ref) => {
@@ -209,7 +209,7 @@ function RuleDetail({ rule, byPath }) {
209
209
  })}
210
210
  </div>
211
211
  ) : (
212
- <p className="text-xs">Esta regra não importa nenhum standard.</p>
212
+ <p className="text-xs">Esta regra não roteia nenhum standard.</p>
213
213
  )}
214
214
  </Section>
215
215
 
@@ -315,10 +315,10 @@ export function SkillDetail({ skill, phases = morphData.phases?.list || [] }) {
315
315
  {skill.description && <p className="leading-relaxed text-[var(--color-fg)]">{skill.description}</p>}
316
316
 
317
317
  <dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-2 rounded-xl bg-white/[0.03] px-4 py-3 shadow-[inset_0_0_0_1px_var(--color-hair)]">
318
- {skill.whenToInvoke && (
318
+ {skill.whenToUse && (
319
319
  <>
320
320
  <dt className="text-xs leading-6 text-[var(--color-faint)]">Quando usar</dt>
321
- <dd className="leading-6">{skill.whenToInvoke}</dd>
321
+ <dd className="leading-6">{skill.whenToUse}</dd>
322
322
  </>
323
323
  )}
324
324
  <dt className="text-xs leading-6 text-[var(--color-faint)]">Fase</dt>
@@ -389,7 +389,7 @@ export default function SkillsLibrary() {
389
389
  const isMatch = useCallback((s) => {
390
390
  const q = search.trim().toLowerCase();
391
391
  if (q) {
392
- const hay = [s.id, s.name, s.description, s.whenToInvoke].filter(Boolean).join(' ').toLowerCase();
392
+ const hay = [s.id, s.name, s.description, s.whenToUse].filter(Boolean).join(' ').toLowerCase();
393
393
  if (!hay.includes(q)) return false;
394
394
  }
395
395
  if (phaseFilter) {
@@ -18,7 +18,7 @@ import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logg
18
18
  export const DEFAULT_KINDS = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'task', 'verify', 'gate', 'failure'];
19
19
  export const ALL_KINDS = [...DEFAULT_KINDS, 'write', 'read', 'command', 'tool'];
20
20
 
21
- const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|score|finish)\b/;
21
+ const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|finish)\b/;
22
22
  const MORPH_PERSONA = /^morph-(.+)$/;
23
23
  const SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
24
24
  const SILENT_EVENTS = new Set(['ToolBatch', 'SessionUsage', 'AgentUsage']);