@polymorphism-tech/morph-spec 8.43.0 → 8.45.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 (203) hide show
  1. package/CLAUDE.md +16 -13
  2. package/README.md +10 -10
  3. package/bin/morph-spec.js +10 -13
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -4
  7. package/docs/QUICKSTART.md +6 -6
  8. package/framework/CLAUDE.md +6 -8
  9. package/framework/MORPH.md +39 -45
  10. package/framework/agents.json +8 -9
  11. package/framework/commands/morph-apply.md +1 -3
  12. package/framework/commands/morph-hotfix.md +1 -1
  13. package/framework/commands/morph-proposal.md +2 -2
  14. package/framework/docs/CLI.md +22 -17
  15. package/framework/evals/review-checklist.md +90 -0
  16. package/framework/evals/trace-schema.md +2 -12
  17. package/framework/hooks/README.md +2 -3
  18. package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
  20. package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
  21. package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
  22. package/framework/hooks/claude-code/core/validate-completion.js +3 -56
  23. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/build-check.js +1 -1
  31. package/framework/hooks/shared/comment-scanner.js +1 -1
  32. package/framework/hooks/shared/phase-utils.js +0 -5
  33. package/framework/hooks/shared/stdin-reader.js +26 -26
  34. package/framework/phases.json +4 -6
  35. package/framework/rubrics/backend.json +1 -1
  36. package/framework/rubrics/default.json +1 -1
  37. package/framework/rubrics/frontend.json +1 -1
  38. package/framework/rules/ai-agents-standards.md +1 -0
  39. package/framework/rules/morph-workflow.md +6 -6
  40. package/framework/schemas/feature.schema.json +2 -28
  41. package/framework/schemas/tasks.schema.json +6 -2
  42. package/framework/skills/README.md +1 -2
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +145 -246
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
  48. package/framework/skills/core/morph-implement/SKILL.md +101 -201
  49. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  50. package/framework/skills/core/morph-media/SKILL.md +1 -1
  51. package/framework/skills/core/morph-plan/SKILL.md +171 -287
  52. package/framework/skills/core/morph-proposal/SKILL.md +4 -5
  53. package/framework/skills/core/morph-review/SKILL.md +109 -430
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +2 -2
  56. package/framework/skills/core/morph-uiux/SKILL.md +2 -3
  57. package/framework/standards/STANDARDS.json +54 -10
  58. package/framework/standards/ai-agents/agent-archetypes.md +1 -1
  59. package/framework/standards/ai-agents/agent-session.md +1 -1
  60. package/framework/standards/ai-agents/agent-spec.md +1 -1
  61. package/framework/standards/ai-agents/batching.md +1 -1
  62. package/framework/standards/ai-agents/context-providers.md +1 -1
  63. package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
  64. package/framework/standards/ai-agents/cost-and-budget.md +1 -1
  65. package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
  66. package/framework/standards/ai-agents/guardrails.md +1 -1
  67. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  68. package/framework/standards/ai-agents/mcp-server.md +1 -1
  69. package/framework/standards/ai-agents/mcp-tools.md +1 -1
  70. package/framework/standards/ai-agents/media-pipeline.md +1 -1
  71. package/framework/standards/ai-agents/media-video.md +1 -1
  72. package/framework/standards/ai-agents/middleware-patterns.md +1 -1
  73. package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
  74. package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
  75. package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
  76. package/framework/standards/ai-agents/modalities-vision.md +1 -1
  77. package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
  78. package/framework/standards/ai-agents/observability-patterns.md +3 -3
  79. package/framework/standards/ai-agents/production.md +2 -2
  80. package/framework/standards/ai-agents/prompt-sources.md +1 -1
  81. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  82. package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
  83. package/framework/standards/ai-agents/service-tier-flex.md +1 -1
  84. package/framework/standards/ai-agents/setup.md +1 -1
  85. package/framework/standards/ai-agents/structured-output.md +1 -1
  86. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  87. package/framework/standards/ai-agents/testing-ai.md +1 -1
  88. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  89. package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
  90. package/framework/standards/ai-agents/workflows.md +1 -1
  91. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  92. package/framework/templates/CLAUDE.md.core.template +1 -1
  93. package/framework/templates/IDE-SNIPPETS.md +262 -262
  94. package/framework/templates/README.md +1 -1
  95. package/framework/templates/REGISTRY.json +0 -81
  96. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  97. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  98. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  99. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  100. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  101. package/framework/templates/docs/spec.md +19 -205
  102. package/framework/templates/docs/ui-components.md +52 -52
  103. package/framework/templates/docs/ui-design-system.md +76 -76
  104. package/framework/templates/docs/ui-flows.md +167 -167
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  117. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  118. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  119. package/framework/templates/state.template.json +17 -17
  120. package/framework/templates/ui/design-system.css +226 -226
  121. package/package.json +2 -2
  122. package/src/commands/e2e/e2e.js +1 -1
  123. package/src/commands/project/heavy.js +1 -1
  124. package/src/commands/project/status.js +3 -3
  125. package/src/commands/project/worktree.js +34 -16
  126. package/src/commands/state/approve.js +9 -50
  127. package/src/commands/state/finish.js +4 -0
  128. package/src/commands/state/gate-check.js +18 -107
  129. package/src/commands/state/gate-decision.js +1 -1
  130. package/src/commands/state/report-check.js +42 -17
  131. package/src/commands/validation/mutate.js +1 -1
  132. package/src/commands/validation/scout-check.js +28 -0
  133. package/src/commands/validation/validate-feature.js +5 -32
  134. package/src/commands/validation/verify.js +16 -25
  135. package/src/core/paths/output-schema.js +0 -2
  136. package/src/core/state/state-manager.js +1 -2
  137. package/src/core/templates/template-renderer.js +477 -477
  138. package/src/dashboard/app/lib/format.js +5 -0
  139. package/src/dashboard/app/main.jsx +13 -13
  140. package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
  141. package/src/dashboard/app/pages/Features.jsx +7 -7
  142. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  143. package/src/dashboard/app/pages/Rubrics.jsx +7 -11
  144. package/src/dashboard/scanner/docs-scanner.js +1 -2
  145. package/src/dashboard/scanner/features-scanner.js +3 -4
  146. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/agents/output-collector.js +0 -1
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/e2e/compose-env.js +5 -2
  151. package/src/lib/evaluator-report.js +52 -334
  152. package/src/lib/generators/design-system-generator.js +508 -508
  153. package/src/lib/generators/handoff-generator.js +4 -5
  154. package/src/lib/git/evidence-staleness.js +39 -0
  155. package/src/lib/installers/memory-installer.js +2 -2
  156. package/src/lib/mutation/manifest.js +8 -2
  157. package/src/lib/mutation/report.js +5 -3
  158. package/src/lib/mutation/runner.js +9 -7
  159. package/src/lib/risk-detector.js +63 -125
  160. package/src/lib/scout/scout-check.js +293 -0
  161. package/src/lib/tasks/eval-node.js +4 -34
  162. package/src/lib/tasks/task-classifier.js +30 -30
  163. package/src/lib/tasks/task-parser.js +209 -209
  164. package/src/lib/tasks/tasks-json-parser.js +7 -5
  165. package/src/lib/tasks/test-nodes.js +1 -1
  166. package/src/lib/tasks/test-runner.js +1 -1
  167. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
  168. package/src/lib/validators/css/css-validator.js +340 -340
  169. package/src/lib/validators/reuse/reuse-validator.js +6 -8
  170. package/src/lib/validators/verify-runner.js +1 -1
  171. package/src/lib/worktree/env-allocator.js +35 -5
  172. package/src/scripts/setup-infra.js +1 -1
  173. package/src/utils/agent-id-mapper.js +11 -11
  174. package/src/utils/agents-installer.js +5 -9
  175. package/src/utils/claude-settings-manager.js +300 -300
  176. package/src/utils/hooks-installer.js +3 -10
  177. package/src/utils/skills-installer.js +1 -1
  178. package/framework/evals/proof-rubrics/README.md +0 -76
  179. package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
  180. package/framework/evals/proof-rubrics/fiacao.md +0 -54
  181. package/framework/evals/proof-rubrics/guard-cego.md +0 -52
  182. package/framework/evals/proof-rubrics/media-claims.md +0 -109
  183. package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
  184. package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
  185. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
  186. package/framework/evals/rubrics/README.md +0 -45
  187. package/framework/evals/rubrics/architecture.md +0 -103
  188. package/framework/evals/rubrics/code-quality.md +0 -98
  189. package/framework/evals/rubrics/condensed.md +0 -57
  190. package/framework/evals/rubrics/contracts.md +0 -67
  191. package/framework/evals/rubrics/test-coverage.md +0 -99
  192. package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
  193. package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
  194. package/framework/evals/suites/standard.yaml +0 -5
  195. package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
  196. package/framework/skills/core/morph-eval/SKILL.md +0 -64
  197. package/framework/templates/.idea/morph-templates.xml +0 -92
  198. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  199. package/framework/templates/docs/clarifications.md +0 -253
  200. package/framework/templates/docs/onboarding.md +0 -122
  201. package/framework/templates/docs/schema-analysis.md +0 -119
  202. package/framework/templates/docs/user-stories.md +0 -34
  203. package/src/commands/state/score.js +0 -147
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.43.0",
3
+ "version": "8.45.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -44,7 +44,7 @@
44
44
  "generate:check": "node scripts/generate-refs.js --check && node scripts/generate-ai-pin-props.js --check && node scripts/api-probe/probe.mjs --check",
45
45
  "generate-standards": "node scripts/generate-standards-registry.js",
46
46
  "generate-standards:check": "node scripts/generate-standards-registry.js --check",
47
- "validate": "node scripts/validate-framework.js && node scripts/check-rubric-parity.js",
47
+ "validate": "node scripts/validate-framework.js",
48
48
  "freshness": "node scripts/check-freshness.js",
49
49
  "verify": "npm run generate:check && npm run generate-standards:check && npm run validate && npm run freshness && npm test",
50
50
  "test": "node scripts/run-tests.mjs",
@@ -178,7 +178,7 @@ export async function e2eUpCommand(featureName, options = {}) {
178
178
  if (options.json) {
179
179
  console.log(JSON.stringify({ status: 'busy', feature: featureName, busy: { reason: err.reason, waitedMs: err.waitedMs } }, null, 2));
180
180
  }
181
- console.error(chalk.yellow(`\n⏳ e2e up não rodou: ${err.message}. Tente de novo quando liberar (\`morph-spec heavy status\`).\n`));
181
+ console.error(chalk.yellow(`\n⏳ e2e up 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\`).\n`));
182
182
  process.exit(EXIT_BUSY);
183
183
  } finally {
184
184
  gate.release();
@@ -45,7 +45,7 @@ export function heavyRunCommand(command, options = {}) {
45
45
  gate.enter(file);
46
46
  } catch (err) {
47
47
  if (!(err instanceof HeavyBusyError)) throw err;
48
- console.error(chalk.yellow(`\n⏳ ${file} não rodou: ${err.message}. Tente de novo quando liberar (\`morph-spec heavy status\`).\n`));
48
+ console.error(chalk.yellow(`\n⏳ ${file} 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`));
49
49
  process.exit(EXIT_BUSY);
50
50
  }
51
51
 
@@ -64,7 +64,7 @@ function checkPhaseOutputs(phaseKey, feature) {
64
64
  const phaseOutputMap = {
65
65
  proposal: ['proposal', 'stackScan'],
66
66
  uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
67
- plan: ['spec', 'mandate', 'tasks', 'decisions'],
67
+ plan: ['spec', 'tasks', 'decisions'],
68
68
  implement: ['recap'],
69
69
  review: ['reviewReport']
70
70
  };
@@ -102,7 +102,7 @@ function formatPhase(phaseKey, currentPhase, feature, options = {}) {
102
102
  const phaseOutputMap = {
103
103
  proposal: ['proposal', 'stackScan'],
104
104
  uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
105
- plan: ['spec', 'mandate', 'tasks', 'decisions'],
105
+ plan: ['spec', 'tasks', 'decisions'],
106
106
  implement: ['recap'],
107
107
  review: ['reviewReport']
108
108
  };
@@ -160,7 +160,7 @@ function getNextSteps(feature) {
160
160
  // Check for missing outputs in current phase
161
161
  const phaseOutputMap = {
162
162
  proposal: ['proposal'],
163
- plan: ['spec', 'mandate', 'tasks'],
163
+ plan: ['spec', 'tasks'],
164
164
  implement: ['recap'],
165
165
  review: ['reviewReport']
166
166
  };
@@ -882,19 +882,21 @@ export async function allocateWorktreeEnv(worktreePath, root, envKey, { morphFea
882
882
  } catch { morphFeature = null; }
883
883
  }
884
884
  try {
885
- const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
885
+ const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent, resolvePortAliases } =
886
886
  await import('../../lib/worktree/env-allocator.js');
887
887
  const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
888
888
  if (!common) return null;
889
889
  const gitCommonDir = resolve(root, common);
890
890
  const block = allocateEnvBlock(envKey, { gitCommonDir, ...resolveFleetConfig(root) });
891
891
  let stack = '';
892
+ let aliases = {};
892
893
  try {
893
894
  const cfg = JSON.parse(readFileSync(join(root, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
894
895
  stack = cfg?.project?.stack || '';
896
+ aliases = resolvePortAliases(cfg).aliases;
895
897
  } catch { /* fail-open — stackless env still carries PORT */ }
896
898
  mkdirSync(join(worktreePath, '.morph'), { recursive: true });
897
- writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature }), 'utf8');
899
+ writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature, aliases }), 'utf8');
898
900
  excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
899
901
  return block;
900
902
  } catch {
@@ -1238,10 +1240,17 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
1238
1240
  }
1239
1241
 
1240
1242
  /**
1241
- * Clear the root-side `.worktree` registration for a finished feature and commit
1242
- * that cleanup scoped to its own pathspec. Runs whenever the worktree is
1243
- * REMOVED — including `--pr`, where no merge landed — so a stale registration
1244
- * never trips the anti-dupe guard or the next finish's root-clean.
1243
+ * Clear the root-side `.worktree` registration for a finished feature. Runs
1244
+ * whenever the worktree is REMOVED — including `--pr`, where no merge landed —
1245
+ * so a stale registration never trips the anti-dupe guard or the next finish's
1246
+ * root-clean.
1247
+ *
1248
+ * A registration that was committed on the default branch is cleared by a commit
1249
+ * scoped to the feature's own pathspec. An untracked scaffold (what
1250
+ * `create --worktree` leaves at the root) is never committed: the branch owns
1251
+ * the feature, and committing the skeleton would put it on the default next to
1252
+ * the archived copy. When the feature is already archived the skeleton is shed,
1253
+ * backup first, by the same primitive `finish` uses.
1245
1254
  *
1246
1255
  * Written DIRECTLY (never saveState, which re-persists every sibling's
1247
1256
  * feature.json and can dirty the root outside this command's pathspec). The thin
@@ -1250,9 +1259,12 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
1250
1259
  *
1251
1260
  * @param {string} root
1252
1261
  * @param {string} feature
1262
+ * @param {{ stamp?: string }} [opts] - the caller's backup stamp, for the shed
1263
+ * @returns {Promise<{ shedBackup: string|null }>}
1253
1264
  */
1254
- export async function clearRootWorktreeRegistration(root, feature) {
1265
+ export async function clearRootWorktreeRegistration(root, feature, { stamp } = {}) {
1255
1266
  const featPathspec = `.morph/features/${feature}`;
1267
+ const gitOpts = { cwd: root, stdio: 'pipe' };
1256
1268
  try {
1257
1269
  const rootFjPath = join(root, '.morph', 'features', feature, 'feature.json');
1258
1270
  if (existsSync(rootFjPath)) {
@@ -1265,16 +1277,19 @@ export async function clearRootWorktreeRegistration(root, feature) {
1265
1277
  }
1266
1278
  } catch { /* fail-open */ }
1267
1279
 
1268
- // Clearing that field can itself dirty the root (a committed registration
1269
- // survives the shed's checkout and the merge). Committing is in contract —
1270
- // finish already writes commits — and scoped to this feature's path only.
1271
1280
  try {
1272
- const dirtyAfter = execFileSync('git', ['status', '--porcelain', '--', featPathspec], { cwd: root, stdio: 'pipe' }).toString();
1273
- if (dirtyAfter.trim() !== '') {
1274
- execFileSync('git', ['add', '--', featPathspec], { cwd: root, stdio: 'pipe' });
1275
- execFileSync('git', ['commit', '-m', `morph-spec finish: clear ${feature} worktree registration`, '--', featPathspec], { cwd: root, stdio: 'pipe' });
1281
+ const tracked = execFileSync('git', ['ls-files', '--', featPathspec], gitOpts).toString().trim() !== '';
1282
+ if (!tracked) {
1283
+ if (!existsSync(join(root, '.morph', 'archive', feature))) return { shedBackup: null };
1284
+ return shedRootFeatureScope(root, feature, { stamp });
1285
+ }
1286
+ let unchanged = true;
1287
+ try { execFileSync('git', ['diff', '--quiet', 'HEAD', '--', featPathspec], gitOpts); } catch { unchanged = false; }
1288
+ if (!unchanged) {
1289
+ execFileSync('git', ['commit', '-m', `morph-spec finish: clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
1276
1290
  }
1277
1291
  } catch { /* fail-open — a dirty root is a nuisance, not a reason to fail */ }
1292
+ return { shedBackup: null };
1278
1293
  }
1279
1294
 
1280
1295
  /**
@@ -1423,7 +1438,7 @@ export function pickRemovalFailure(stderrs) {
1423
1438
  * @returns {Promise<{ worktreeRemoved: boolean, ignoredBackup: string|null, discarded: string[],
1424
1439
  * blockers: string[], ignoredBlocking: string[], shieldable: string[], declineReason: string|null,
1425
1440
  * relinked: boolean, refused: boolean, infraLeftUnlinked: string[],
1426
- * removalFailure: {kind: string, detail: string}|null }>}
1441
+ * removalFailure: {kind: string, detail: string}|null, rootShedBackup: string|null }>}
1427
1442
  */
1428
1443
  export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, relinkOnFailure = false, stamp }) {
1429
1444
  // Drop the infra junctions FIRST, without following them — this is what makes
@@ -1453,6 +1468,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
1453
1468
 
1454
1469
  let worktreeRemoved = false;
1455
1470
  let removalFailure = null;
1471
+ let rootShedBackup = null;
1456
1472
  if (canRemove) {
1457
1473
  const attempt = await attemptWorktreeRemoval(worktreePath, root);
1458
1474
  worktreeRemoved = attempt.removed;
@@ -1473,7 +1489,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
1473
1489
  const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
1474
1490
  if (common) releaseEnvBlock(feature, { gitCommonDir: resolve(root, common) });
1475
1491
  } catch { /* fail-open */ }
1476
- await clearRootWorktreeRegistration(root, feature);
1492
+ ({ shedBackup: rootShedBackup } = await clearRootWorktreeRegistration(root, feature, { stamp }));
1477
1493
  }
1478
1494
 
1479
1495
  // Rollback (A4) — ONLY on a REFUSAL, never on a FAILURE. The two look alike
@@ -1568,6 +1584,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
1568
1584
  refused,
1569
1585
  infraLeftUnlinked,
1570
1586
  removalFailure,
1587
+ rootShedBackup,
1571
1588
  };
1572
1589
  }
1573
1590
 
@@ -1789,6 +1806,7 @@ export async function runWorktreeRemove(featureArg, options = {}) {
1789
1806
  if (teardown.replicable.length) out.replicatedInRoot = teardown.replicable;
1790
1807
  if (teardown.ignoredBackup) out.ignoredBackup = teardown.ignoredBackup;
1791
1808
  if (teardown.shieldable.length) out.backedUp = teardown.shieldable;
1809
+ if (teardown.rootShedBackup) out.shedBackup = teardown.rootShedBackup;
1792
1810
  } else {
1793
1811
  out.refused = teardown.refused;
1794
1812
  if (teardown.blockers.length) out.reparsePoints = teardown.blockers;
@@ -5,9 +5,10 @@ import { existsSync, readFileSync } from 'fs';
5
5
  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
- import { evaluatorReportState, COMPOSITE_SCORE_TOKEN } from '../../lib/evaluator-report.js';
8
+ import { evaluatorReportState, VERDICT_TOKEN } from '../../lib/evaluator-report.js';
9
9
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
10
  import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
11
+ import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
11
12
 
12
13
  /**
13
14
  * The literal token `review-report.md` carries to declare which tree it
@@ -57,7 +58,7 @@ function reviewStaleness(featureName) {
57
58
  // of staleness — say "unstamped" rather than block on a guess.
58
59
  return { status: 'unstamped', head };
59
60
  }
60
- if (reviewed === head) return { status: 'ok', reviewed, head };
61
+ if (!treeMovedSince(cwd, reviewed, head)) return { status: 'ok', reviewed, head };
61
62
 
62
63
  let ahead = 0;
63
64
  try {
@@ -246,18 +247,6 @@ export async function approveCommand(featureName, gateName, options = {}) {
246
247
  process.exit(1);
247
248
  }
248
249
 
249
- // Gate 3 evaluator backstop: the composite score of a feature comes from
250
- // the INDEPENDENT evaluator (persona `evaluator`, fresh context) dispatched
251
- // by morph-review — never from self-assessment by whoever implemented it.
252
- // Nothing enforced that. When the dispatch was impossible (observed: a
253
- // session instruction forbade sub-agents), the flow still produced a
254
- // review-report.md carrying a composite score and a verdict, with a caveat
255
- // in prose nobody is obliged to read. A gate that yields a grade without
256
- // its evaluator is not a gate.
257
- //
258
- // The escape hatch is EXPLICIT and RECORDED (`--self-assessed`), never
259
- // silent — the silence is the defect. `--force` deliberately does NOT
260
- // bypass this: it is the out-of-order recovery flag, a different concern.
261
250
  // Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
262
251
  // moves. Observed: review-report.md written at b30ac6e, then T3 landed and
263
252
  // inverted the default of a business rule; the report stayed in the repo
@@ -345,25 +334,16 @@ export async function approveCommand(featureName, gateName, options = {}) {
345
334
  process.exit(1);
346
335
  }
347
336
  } else {
348
- // `existsSync` puro não bastava: um evaluator-report SEM o token
349
- // `**Composite-Score:**` passava aqui E era descartado pelo gate-check
350
- // (que então caía no proxy de taskScores). O resultado era um Gate 3
351
- // carimbado `approvedBy: "auto"` sobre a média das auto-avaliações de
352
- // quem implementou — precisamente o que MORPH.md §5 proíbe. Arquivo
353
- // presente sem veredito legível é relatório OCO, não relatório.
354
337
  const report = evaluatorReportState(process.cwd(), featureName);
355
338
  if (report.status !== 'ok') {
356
339
  if (!options.selfAssessed) {
357
340
  if (report.status === 'missing') {
358
341
  console.error(chalk.red('\n❌ Gate 3 exige o avaliador independente — `4-review/evaluator-report.md` não existe.'));
359
342
  } else {
360
- console.error(chalk.red('\n❌ Gate 3: o `4-review/evaluator-report.md` existe mas está OCO —'));
361
- console.error(chalk.red(` falta a linha \`${COMPOSITE_SCORE_TOKEN} {n.n}\`, o veredito legível por máquina.`));
362
- console.error(chalk.gray(' Sem ela o gate-check não lê o score real e cai na média das auto-avaliações.'));
363
- console.error(chalk.gray(' Peça ao avaliador o relatório completo (morph-review §4) ou complete a linha.'));
343
+ console.error(chalk.red(`\n❌ Gate 3: o \`4-review/evaluator-report.md\` não tem \`${VERDICT_TOKEN} SHIP|FIX\`.`));
344
+ console.error(chalk.gray(' Peça ao avaliador o relatório completo (morph-review §4) ou retome-o por SendMessage.'));
364
345
  }
365
- console.error(chalk.gray(' O score composto da feature vem do avaliador (persona `evaluator`, contexto fresco)'));
366
- console.error(chalk.gray(' despachado pela morph-review, nunca de auto-avaliação de quem implementou.'));
346
+ console.error(chalk.gray(' O veredito vem do avaliador (persona `evaluator`, contexto fresco), nunca de quem implementou.'));
367
347
  console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
368
348
  console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
369
349
  process.exit(1);
@@ -371,30 +351,9 @@ export async function approveCommand(featureName, gateName, options = {}) {
371
351
  selfAssessed = true;
372
352
  }
373
353
 
374
- // REAVALIAÇÃO VISÍVEL. O `evaluator-report.md` é append-only, então uma
375
- // segunda rodada acrescenta um `**Composite-Score:**` ao fim e o arquivo
376
- // passa a carregar mais de um veredito. O gate decide pelo ÚLTIMO (o
377
- // vigente) — mas aprovar em silêncio um relatório com notas superadas
378
- // esconde do humano exatamente o fato que ele precisa ver: a nota MUDOU,
379
- // e numa das rodadas medidas em campo ela CAIU (8.9 → 8.3, quando uma
380
- // correção abriu um bloqueante).
381
- //
382
- // Aqui é o lugar: `approve` é a precondição humana do Gate 3 e a única
383
- // superfície onde uma pessoa lê o veredito antes de carimbá-lo. Sem este
384
- // bloco, `scoreTokens`/`superseded` seriam mitigação inerte — campo que
385
- // ninguém lê não mitiga nada.
386
- if (report.status === 'ok' && report.scoreTokens > 1) {
387
- console.log(chalk.yellow(
388
- `
389
- ⚠️ O relatório do avaliador tem ${report.scoreTokens} vereditos (o arquivo é append-only).`
390
- ));
391
- console.log(chalk.gray(` Vigente (o que o gate usa): ${report.score}`));
392
- console.log(chalk.gray(` Superado(s), na ordem do arquivo: ${report.superseded.join(', ')}`));
393
- if (report.superseded.some((n) => n > report.score)) {
394
- console.log(chalk.yellow(' A nota CAIU em relação a uma rodada anterior — confira o que a correção'));
395
- console.log(chalk.yellow(' abriu antes de aprovar; o relatório mantém a análise das duas rodadas.'));
396
- }
397
- console.log('');
354
+ if (report.status === 'ok' && report.superseded.length > 0) {
355
+ console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
356
+ console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
398
357
  }
399
358
  }
400
359
  }
@@ -65,6 +65,8 @@ export function buildPrTitle(feature, fj) {
65
65
  return prefix + (raw.length <= room ? raw : `${raw.slice(0, room - 1)}…`);
66
66
  }
67
67
 
68
+ const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `decisions.md`, use cases promovidos, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
69
+
68
70
  /**
69
71
  * `morph-spec finish <feature> --pr|--merge` — the single, symmetric close of a
70
72
  * feature's lifecycle, for BOTH the worktree flow and the common flow.
@@ -132,6 +134,8 @@ export async function finishCommand(feature, options = {}) {
132
134
  const emit = (obj) => {
133
135
  if (resumed) obj = { ...obj, resumed: true };
134
136
  if (obj?.finished === true) {
137
+ // The one path every finished:true takes; the --pr payload is built by hand, outside mergeOutput.
138
+ obj = { ...obj, memoryNote: MEMORY_NOTE };
135
139
  try {
136
140
  const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
137
141
  if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
@@ -3,11 +3,12 @@ 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 { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
6
+ import { evaluatorReportState, evaluatorMediaSection } 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';
10
10
  import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
11
+ import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
11
12
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
12
13
  import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
13
14
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
@@ -23,13 +24,8 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
23
24
  * - standardsViolations / designSystemDivergence: still an LLM's
24
25
  * natural-language read (see the R4 report — no deterministic detector
25
26
  * exists in JS yet).
26
- * - evalScore: escape hatch for the TRUE Gate-3 composite score. Since R4
27
- * the evaluator-report carries a machine-readable `**Composite-Score:**`
28
- * token that this command parses by itself (see evaluatorCompositeScore),
29
- * so the override is only needed for legacy reports without the token —
30
- * there the taskScores-average proxy is the fallback.
31
27
  */
32
- const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence', 'evalScore'];
28
+ const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
33
29
 
34
30
  /**
35
31
  * Os paths de standard que fazem uma feature ser "de midia".
@@ -95,31 +91,6 @@ function readTasks(featureName) {
95
91
  }
96
92
  }
97
93
 
98
- /**
99
- * Composite eval score PROXY: the average of `lastScore` across every task
100
- * recorded in `feature.taskScores`, rounded to 1 decimal. `lastScore` (not
101
- * `bestScore`) on purpose — a gate check reports current-state risk, and
102
- * `bestScore` can be a stale high-water mark from an attempt the code no
103
- * longer matches. "average" mirrors morph-apply's own Gate-3 risk-signal
104
- * prose ("Score médio < 9 no review-report" — SKILL.md §5).
105
- *
106
- * This is explicitly NOT the true Gate-3 composite score (see
107
- * OVERRIDABLE_SIGNAL_KEYS doc above) — undefined (key omitted) when no task
108
- * has been scored yet, so shouldPauseGate's `typeof === 'number'` check
109
- * naturally skips it instead of pausing on a false 0.
110
- *
111
- * @param {object} feature
112
- * @returns {number|undefined}
113
- */
114
- function compositeEvalScore(feature) {
115
- const scores = Object.values(feature.taskScores || {})
116
- .map((t) => t.lastScore)
117
- .filter((n) => typeof n === 'number');
118
- if (scores.length === 0) return undefined;
119
- const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
120
- return Math.round(avg * 10) / 10;
121
- }
122
-
123
94
  /**
124
95
  * "Esta feature mexeu em prompt ou em modelo?" — em duas metades, porque uma só
125
96
  * não distingue as duas situações que pedem ações diferentes.
@@ -202,78 +173,25 @@ function collectSignals(featureName, feature, cwd, gateName) {
202
173
  if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
203
174
 
204
175
  // Só onde o checklist decide algo: no fecho do design e no ship. Fato de
205
- // disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
176
+ // disco, fora de OVERRIDABLE_SIGNAL_KEYS.
206
177
  if (gateName === 'uiux' || gateName === 'review') {
207
178
  signals.screenChecklist = collectScreenChecklist(cwd, featureName, gateName);
208
179
  }
209
180
 
210
- const evalScore = compositeEvalScore(feature);
211
- if (evalScore !== undefined) {
212
- signals.evalScore = evalScore;
213
- signals.evalScoreSource = 'taskScores-proxy';
214
- }
215
-
216
- // No Gate 3, o score verdadeiro do avaliador independente (token
217
- // Composite-Score no evaluator-report.md) tem precedência sobre o proxy.
218
181
  if (gateName === 'review') {
219
- const trueComposite = evaluatorCompositeScore(cwd, featureName);
220
- if (trueComposite !== undefined) {
221
- signals.evalScore = trueComposite;
222
- signals.evalScoreSource = 'evaluator-report';
223
- }
224
-
225
- // PROVA DE MUTAÇÃO, derivada do MESMO módulo leitor do relatório. Um score
226
- // alto diz o que o avaliador ACHOU; a seção `## Mutações` diz se ele
227
- // PROVOU. Sem ela, um Gate 3 auto-passa sobre uma suíte que ninguém mostrou
228
- // que morde — o defeito que as rubricas de `evals/proof-rubrics/` existem
229
- // para nomear.
230
- //
231
- // `productionCode` viaja DENTRO do sinal para que risk-detector.js continue
232
- // puro sobre fatos já reunidos: a divisão é "gate-check COLETA do disco,
233
- // risk-detector DECIDE". Uma feature 100% `docs` não tem o que mutar, e
234
- // exigir prova dela seria pedir uma cerimônia vazia — que é como uma regra
235
- // vira ruído e depois é desligada.
236
- //
237
- // Task SEM `group` conta como código de produção: `group` é opcional no
238
- // schema e o default documentado é `backend`. O conservador aqui é assumir
239
- // que HÁ código — errar para o lado de pedir prova custa uma pausa; errar
240
- // para o outro lado devolve o gate cego.
241
- //
242
- // `tasks` vazio (plano ausente ou ilegível) ⇒ `productionCode: false` ⇒ NÃO
243
- // pausa. Fail-open deliberado: sem plano, não há como AFIRMAR que existe
244
- // código de produção, e uma regra nova não pode travar feature legada.
245
- //
246
- // Nada disto entra em OVERRIDABLE_SIGNAL_KEYS: são fatos de disco, como
247
- // `gate`/`workType`/`e2e*`. Um `--signals '{"mutationProof":{"section":true}}'`
248
- // compraria o Gate 3 exatamente como um `e2eStatus: "pass"` forjado.
249
- const proof = evaluatorProofState(cwd, featureName);
250
- signals.mutationProof = {
251
- section: proof.mutationSection,
252
- min: minProofScore(proof.proofScores),
253
- productionCode: tasks.some((t) => (t?.group ?? 'backend') !== 'docs'),
254
- };
255
-
256
- // PROVA DE MÍDIA, mesmo módulo leitor e mesma disciplina anti-spoof. As
257
- // quatro afirmações de uma feature de mídia (custo, revisão humana,
258
- // marcação, hot path não bloqueado) são propriedades ESTRUTURAIS, e por isso
259
- // `score` limita `architecture` e não `test-coverage` — daí `media` ficar
260
- // FORA do `min` acima.
261
- //
262
- // `featureGeneratesMedia` é determinístico e vem do DISCO, nunca de
263
- // julgamento: existe ao menos uma task cujo array `standards` cita um dos
264
- // três paths de standard de mídia. É o mesmo formato do `productionCode`
265
- // acima e usa um campo que o Gate 2 congelou. Mora numa função exportada
266
- // (acima) porque um predicado que decide se um guardrail arma precisa de
267
- // canário próprio — e porque o prefixo `framework/` precisa ser normalizado
268
- // ali, num lugar só.
269
- //
270
- // A isenção é automática e é o comportamento certo: a maioria das features
271
- // não gera mídia, e exigir a seção delas seria cerimônia vazia — que é como
272
- // uma regra vira ruído e depois é desligada. `tasks` vazio ⇒ false ⇒ não
273
- // pausa, o mesmo fail-open por ausência do sinal irmão.
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 };
274
193
  signals.mediaClaims = {
275
- section: proof.mediaSection,
276
- score: mediaProofScore(proof.proofScores),
194
+ section: evaluatorMediaSection(cwd, featureName),
277
195
  featureGeneratesMedia: featureGeneratesMedia(tasks),
278
196
  };
279
197
  }
@@ -283,7 +201,7 @@ function collectSignals(featureName, feature, cwd, gateName) {
283
201
  // stamp com sha null (checkout sem git) ou stamp ausente lê como NOT stale.
284
202
  signals.verification = verification;
285
203
  if (verification?.status) signals.verificationStatus = verification.status;
286
- signals.verificationStale = Boolean(verification?.sha && sha && verification.sha !== sha);
204
+ signals.verificationStale = treeMovedSince(cwd, verification?.sha, sha);
287
205
 
288
206
  // O nó e2e, separado do rollup de propósito: um `skip` NÃO reprova
289
207
  // `verification.status`, então sem estas duas chaves o gate lê 'pass' tanto
@@ -373,7 +291,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
373
291
  process.exit(1);
374
292
  }
375
293
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
376
- console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"evalScore":9.2}\'\n'));
294
+ console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"standardsViolations":[]}\'\n'));
377
295
  process.exit(1);
378
296
  }
379
297
 
@@ -389,13 +307,6 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
389
307
  if (key in parsed) overrides[key] = parsed[key];
390
308
  }
391
309
 
392
- // Um `evalScore` vindo de --signals substitui o VALOR mas não a
393
- // PROCEDÊNCIA: sem isto o merge cego abaixo deixaria `evalScoreSource`
394
- // com o que collectSignals escreveu ('taskScores-proxy'), e o sinal de
395
- // auditoria passaria a mentir sobre de onde veio o número. `signals-
396
- // override` não está em OVERRIDABLE_SIGNAL_KEYS — o caller não pode
397
- // forjá-lo, mesmo anti-spoof de `gate`/`workType`.
398
- if ('evalScore' in overrides) overrides.evalScoreSource = 'signals-override';
399
310
  }
400
311
 
401
312
  const cwd = process.cwd();
@@ -68,7 +68,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
68
68
  * [--signals <json>] [--trust <mode>]` — record the trust decision for a gate
69
69
  * under `gateDecisions` in the authoritative feature.json.
70
70
  *
71
- * Same philosophy as `morph-spec score`: the timestamp (`decidedAt`) is
71
+ * The timestamp (`decidedAt`) is
72
72
  * stamped server-side and the write is atomic (state-manager). This replaces
73
73
  * the manual Edit of feature.json from the skills — which collided with the
74
74
  * state-sync hook rewriting the file between the agent's Read and Edit
@@ -35,9 +35,15 @@ import { join } from 'path';
35
35
  import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
36
36
  import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
37
37
  import { atomicWriteJson } from '../../core/state/state-manager.js';
38
+ import { minimatch } from 'minimatch';
39
+ import { evaluatorVerdictFromText, VERDICTS } from '../../lib/evaluator-report.js';
40
+ import { evaluatorModelFor } from '../../lib/risk-detector.js';
41
+ import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-branch.js';
42
+ import { readMorphConfig } from '../../lib/tasks/test-runner.js';
43
+ import { featureGeneratesMedia } from './gate-check.js';
38
44
 
39
45
  /**
40
- * The four mandatory items of the report contract, as detectable markers.
46
+ * The mandatory items of the report contract, as detectable markers.
41
47
  *
42
48
  * HEURISTIC BY CONSTRUCTION, and deliberately loose: the contract asks for
43
49
  * content, not for a fixed template, and reports written before this command
@@ -48,10 +54,8 @@ import { atomicWriteJson } from '../../core/state/state-manager.js';
48
54
  const REPORT_MARKERS = [
49
55
  // (2) files touched — a list line carrying a path or a backticked identifier
50
56
  { id: 'files', re: /^\s*[-*]\s+.*[`/\\].*$/m },
51
- // (4) self-score 0-10
52
- { id: 'score', re: /score[^\n]*?\b(10|\d)(?:[.,]\d+)?\b/i },
53
- // (3) problems found (or an explicit "none")
54
- { id: 'problems', re: /problema|issue|blocker|nenhum|impedimento/i },
57
+ // (3) each doneCriteria, met or not, with its evidence
58
+ { id: 'criteria', re: /crit[ée]rio|doneCriteria|p[óo]s-condi[çc][ãa]o|atendid/i },
55
59
  ];
56
60
 
57
61
  /** (1) "what was done" — prose beyond the structural markers. */
@@ -60,9 +64,6 @@ const MIN_PROSE_CHARS = 80;
60
64
  /**
61
65
  * The partial-report marker of the dispatch contract (clause 5, ADR-004): a
62
66
  * sub-agent nearing its `maxTurns` ceiling writes `PARCIAL` on the FIRST line.
63
- * Read on purpose, never inferred from a missing score: the score marker above
64
- * is loose by design, and a partial saying "auto-score: pendente (0-10 no
65
- * final)" matches it — with the other items present it would audit as `ok`.
66
67
  * Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
67
68
  */
68
69
  const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
@@ -96,12 +97,12 @@ function evaluatorPath(cwd, feature) {
96
97
  * items can be found in it.
97
98
  */
98
99
  export function inspectReport(path) {
99
- if (!existsSync(path)) return { exists: false, complete: false, missing: ['what', 'files', 'problems', 'score'] };
100
+ if (!existsSync(path)) return { exists: false, complete: false, missing: ['what', 'files', 'criteria'] };
100
101
  let body = '';
101
102
  try {
102
103
  body = readFileSync(path, 'utf8');
103
104
  } catch {
104
- return { exists: true, complete: false, missing: ['what', 'files', 'problems', 'score'], unreadable: true };
105
+ return { exists: true, complete: false, missing: ['what', 'files', 'criteria'], unreadable: true };
105
106
  }
106
107
 
107
108
  const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
@@ -122,6 +123,29 @@ export function inspectReport(path) {
122
123
  return { exists: true, complete: missing.length === 0, missing, partial };
123
124
  }
124
125
 
126
+ /** The evaluator report is complete when it carries a readable verdict and is not partial. */
127
+ export function inspectEvaluatorReport(path) {
128
+ if (!existsSync(path)) return { exists: false, complete: false };
129
+ let body = '';
130
+ try { body = readFileSync(path, 'utf8'); } catch { return { exists: true, complete: false, unreadable: true }; }
131
+ const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
132
+ const partial = PARTIAL_MARKER.test(firstLine);
133
+ const { verdict } = evaluatorVerdictFromText(body);
134
+ return { exists: true, complete: !partial && VERDICTS.includes(verdict), partial, verdict: verdict ?? null };
135
+ }
136
+
137
+ function evaluatorDispatch(cwd, tasks) {
138
+ let changedFiles = [];
139
+ try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
140
+ const watch = readMorphConfig(cwd)?.project?.evals?.watch;
141
+ const globs = Array.isArray(watch) ? watch.filter((g) => typeof g === 'string' && g.trim()) : [];
142
+ return evaluatorModelFor({
143
+ changedFiles,
144
+ promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
145
+ generatesMedia: featureGeneratesMedia(tasks),
146
+ });
147
+ }
148
+
125
149
  /** How many of a task's declared outputs actually exist on disk. */
126
150
  function outputEvidence(cwd, taskOutputs) {
127
151
  const expected = Array.isArray(taskOutputs) ? taskOutputs : [];
@@ -312,16 +336,17 @@ function snapshotPath(cwd, feature) {
312
336
  * dispatched when there is one. `beforeEval` takes a new snapshot after the
313
337
  * comparison, so the output still shows what changed since the previous one.
314
338
  */
315
- function treeFor(cwd, feature, beforeEval) {
339
+ function treeFor(cwd, feature, beforeEval, tasks = []) {
316
340
  const path = snapshotPath(cwd, feature);
317
341
  let snapshot = null;
318
342
  try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
319
343
  const tree = treeState(cwd, { snapshot });
320
344
  if (!beforeEval) return { tree };
345
+ const evaluatorModel = evaluatorDispatch(cwd, tasks);
321
346
  const next = snapshotTree(cwd);
322
- if (!next) return { tree, beforeEval: null };
347
+ if (!next) return { tree, beforeEval: null, evaluatorModel };
323
348
  atomicWriteJson(path, next);
324
- return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length } };
349
+ return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, evaluatorModel };
325
350
  }
326
351
 
327
352
  /**
@@ -356,21 +381,21 @@ export function reportCheckCommand(feature, taskId, options = {}) {
356
381
  if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
357
382
  const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
358
383
  const clean = results.every((r) => r.verdict === 'ok');
359
- emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
384
+ emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
360
385
  }
361
386
 
362
387
  if (taskId) {
363
388
  const task = tasks.find((t) => t.id === taskId);
364
389
  if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
365
390
  const result = checkTask(cwd, feature, task);
366
- emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval) }, result.verdict === 'ok' ? 0 : 1);
391
+ emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval, tasks) }, result.verdict === 'ok' ? 0 : 1);
367
392
  }
368
393
 
369
394
  // Whole-feature audit: every DONE task owes a report; a pending one does not.
370
395
  const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
371
- const evaluator = inspectReport(evaluatorPath(cwd, feature));
396
+ const evaluator = inspectEvaluatorReport(evaluatorPath(cwd, feature));
372
397
  const clean = results.every((r) => r.verdict === 'ok');
373
- emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
398
+ emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
374
399
  }
375
400
 
376
401
  function printHuman(obj) {