@polymorphism-tech/morph-spec 8.47.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -1,330 +1,199 @@
1
1
  /**
2
- * `morph-spec report-check <feature> [task] [--json]` — the deterministic half
3
- * of the missing-report protocol (B4).
2
+ * `morph-spec report-check <feature> [slice...] [--json]` — the deterministic
3
+ * half of the missing-report protocol.
4
4
  *
5
- * MORPH.md already made the sub-agent's canonical report file the authoritative
6
- * return value ("o ARQUIVO é o retorno, a última mensagem é cortesia"), but it
7
- * never said what to do when the file is not there. The naive reading — "no
8
- * report, so the agent failed" — is wrong and expensive: the agent may have
9
- * done every bit of the work and lost the means to record it (observed: a
10
- * worktree removed out from under a running sub-agent).
11
- *
12
- * So this command answers the question the orchestrator actually has, and
13
- * answers it from evidence rather than from the agent's own word: **did the
14
- * WORK land, independently of whether the REPORT did?**
5
+ * The slice report file is the sub-agent's authoritative return value; the
6
+ * final message is a courtesy. This command answers what the orchestrator
7
+ * needs when it resumes control, from evidence rather than from the agent's
8
+ * own word: **did the WORK land, independently of whether the REPORT did?**
15
9
  *
16
10
  * verdict action
17
11
  * ───────────────────────────── ──────────────────
18
12
  * ok proceed
19
- * report-incomplete complete-degraded
13
+ * report-stale refresh-from-evidence
14
+ * report-incomplete resume | complete-degraded
20
15
  * work-landed-report-missing accept-degraded
21
16
  * no-work-no-report redispatch
22
17
  * report-missing-evidence-unknown redispatch (conservative default)
23
18
  *
24
- * Exit 0 only for `ok` — anything needing an action exits non-zero so a caller
25
- * can branch on it. `--json` always prints the machine-readable verdict.
19
+ * The observed evidence is keyed by the dispatch: the agent whose `agentId`
20
+ * wrote the report owns the window, and its writes after the report make the
21
+ * report stale. A report written inline (no sub-agent) is judged by its
22
+ * markers only.
26
23
  *
27
- * The output also carries `tree` (see tree-state.js): the working tree the
28
- * orchestrator would otherwise ask git for, call by call, right after the same
29
- * dispatch. It is information only and never changes the verdict.
24
+ * Exit 0 only for `ok`. The output also carries `tree` (see tree-state.js),
25
+ * information only — it never changes the verdict.
30
26
  */
31
27
 
32
28
  import chalk from 'chalk';
33
29
  import { existsSync, readFileSync, statSync } from 'fs';
34
30
  import { join } from 'path';
35
- import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
31
+ import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
32
+ import { sliceReportPath, inspectReport, REPORT_MARKERS } from '../../../framework/hooks/shared/slice-progress.js';
33
+ import { loadTelemetryAllTrees, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
36
34
  import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
37
35
  import { atomicWriteJson } from '../../core/state/state-manager.js';
38
36
  import { minimatch } from 'minimatch';
39
- import { evaluatorVerdictFromText, VERDICTS } from '../../lib/evaluator-report.js';
40
- import { evaluatorModelFor } from '../../lib/risk-detector.js';
37
+ import { reviewReports, reviewFileName } from '../../lib/review-reports.js';
38
+ import { verifierModelFor } from '../../lib/risk-detector.js';
41
39
  import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-branch.js';
42
40
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
43
- import { featureGeneratesMedia } from './gate-check.js';
44
-
45
- /**
46
- * The mandatory items of the report contract, as detectable markers.
47
- *
48
- * HEURISTIC BY CONSTRUCTION, and deliberately loose: the contract asks for
49
- * content, not for a fixed template, and reports written before this command
50
- * existed must still audit cleanly. A false "complete" is the cheap error here
51
- * (the orchestrator reads the report anyway); a false "incomplete" would send
52
- * it re-dispatching finished work, which is the expensive one.
53
- */
54
- const REPORT_MARKERS = [
55
- // (2) files touched — a list line carrying a path or a backticked identifier
56
- { id: 'files', re: /^\s*[-*]\s+.*[`/\\].*$/m },
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 },
59
- ];
60
-
61
- /** (1) "what was done" — prose beyond the structural markers. */
62
- const MIN_PROSE_CHARS = 80;
63
-
64
- /**
65
- * The partial-report marker of the dispatch contract (clause 5, ADR-004): a
66
- * sub-agent nearing its `maxTurns` ceiling writes `PARCIAL` on the FIRST line.
67
- * Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
68
- */
69
- const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
70
-
71
- function reportPath(cwd, feature, taskId) {
72
- return join(cwd, '.morph', 'features', feature, '3-implement', 'reports', `${taskId}.md`);
73
- }
74
-
75
- /**
76
- * `.morph/logs/task-times.json` — `{ [feature]: { [taskId]: {status,start,end} } }`,
77
- * escrito pelo hook `trace-autogen` nas transições de status da task. É a
78
- * janela temporal de cada task; sem ela não há como recortar quais writes do
79
- * `events.jsonl` pertencem a qual task.
80
- */
81
- function readTaskTimes(cwd) {
82
- try {
83
- const p = join(cwd, '.morph', 'logs', 'task-times.json');
84
- if (!existsSync(p)) return null;
85
- return JSON.parse(readFileSync(p, 'utf8'));
86
- } catch {
87
- return null;
41
+ import { readPlanChecks } from '../../lib/plan/checks.js';
42
+ import { featureGeneratesMedia, collectReviewRequirement } from './gate-check.js';
43
+
44
+ export { inspectReport, REPORT_MARKERS };
45
+
46
+ /** Each review report: complete when it carries a readable verdict and is not partial. */
47
+ export function inspectReviewReports(cwd, feature) {
48
+ const out = {};
49
+ for (const [role, r] of Object.entries(reviewReports(cwd, feature))) {
50
+ out[role] = r.status === 'missing'
51
+ ? { exists: false, complete: false, partial: false, verdict: null }
52
+ : { exists: true, complete: r.status === 'ok', partial: r.status === 'partial', verdict: r.verdict ?? null };
88
53
  }
54
+ return out;
89
55
  }
90
56
 
91
- function evaluatorPath(cwd, feature) {
92
- return join(cwd, '.morph', 'features', feature, '4-review', 'evaluator-report.md');
93
- }
94
-
95
- /**
96
- * Inspect one report file: does it exist, and which of the four mandatory
97
- * items can be found in it.
98
- */
99
- export function inspectReport(path) {
100
- if (!existsSync(path)) return { exists: false, complete: false, missing: ['what', 'files', 'criteria'] };
101
- let body = '';
102
- try {
103
- body = readFileSync(path, 'utf8');
104
- } catch {
105
- return { exists: true, complete: false, missing: ['what', 'files', 'criteria'], unreadable: true };
106
- }
107
-
108
- const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
109
- // Prose = the body minus headings and list lines, so a bare file list does
110
- // not pass as "what was done".
111
- const prose = body
112
- .split('\n')
113
- .filter((l) => !/^\s*(#|[-*]\s)/.test(l))
114
- .join(' ')
115
- .replace(/\s+/g, ' ')
116
- .trim();
117
- if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
118
-
119
- const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
120
- const partial = PARTIAL_MARKER.test(firstLine);
121
- if (partial) missing.push('partial');
122
-
123
- return { exists: true, complete: missing.length === 0, missing, partial };
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) {
57
+ /** Who reviews and on which model: the verifier routes to opus on prompt/model or media; security is dispatched only when required. */
58
+ function reviewDispatch(cwd, feature, slices) {
138
59
  let changedFiles = [];
139
60
  try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
140
61
  const watch = readMorphConfig(cwd)?.project?.evals?.watch;
141
62
  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
-
149
- /** How many of a task's declared outputs actually exist on disk. */
150
- function outputEvidence(cwd, taskOutputs) {
151
- const expected = Array.isArray(taskOutputs) ? taskOutputs : [];
152
- const present = expected.filter((rel) => {
153
- try {
154
- return existsSync(join(cwd, rel));
155
- } catch {
156
- return false;
157
- }
158
- }).length;
159
- return { outputsExpected: expected.length, outputsPresent: present };
63
+ let workType;
64
+ try { workType = JSON.parse(readFileSync(join(cwd, '.morph', 'features', feature, 'feature.json'), 'utf8')).workType; } catch { workType = undefined; }
65
+ const { required, security } = collectReviewRequirement(cwd, { workType });
66
+ return {
67
+ required,
68
+ verifier: verifierModelFor({
69
+ promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
70
+ generatesMedia: featureGeneratesMedia(slices),
71
+ }),
72
+ reviewer: { model: 'sonnet', reasons: [] },
73
+ security: { required: security.required, model: 'opus', reasons: security.reasons },
74
+ };
160
75
  }
161
76
 
162
77
  /**
163
- * Folga entre o `mtime` do relatório e o `ts` do último write observado.
164
- *
165
- * São DOIS RELÓGIOS: `mtime` vem do filesystem, `ts` vem de
166
- * `new Date().toISOString()` dentro do hook de telemetria. O write do próprio
167
- * relatório e o evento que o registra caem a milissegundos de distância, com
168
- * arredondamentos diferentes — sem folga, um relatório perfeitamente fresco
169
- * seria acusado de velho. Dois segundos são muito mais que o skew real e muito
170
- * menos que qualquer trabalho de verdade feito depois de relatar.
78
+ * Two clocks: the report's mtime comes from the filesystem, the event's `ts`
79
+ * from the telemetry hook. Two seconds absorb their skew and are far less
80
+ * than any real work done after reporting.
171
81
  */
172
82
  export const STALE_TOLERANCE_MS = 2000;
173
83
 
84
+ const slash = (p) => String(p || '').replace(/\\/g, '/');
85
+
174
86
  /**
175
- * Traz o target do evento para a MESMA forma que `task.outputs`.
176
- *
177
- * Os dois lados chegam em convenções diferentes, e cruzá-los crus não casa
178
- * nunca: `event.target` é o `file_path` como a ferramenta o recebeu, e
179
- * `Write`/`Edit` exigem caminho ABSOLUTO (`telemetry-log.js` só normaliza as
180
- * barras, não relativiza); `task.outputs` é relativo à raiz — é assim que
181
- * `outputEvidence` o resolve, com `join(cwd, rel)`. Sem este passo,
182
- * `undeclared`/`unobserved` listariam TUDO em toda task, e um sinal que sempre
183
- * grita é um sinal que se aprende a ignorar.
184
- *
185
- * Um target que não fica sob a raiz (outra árvore) volta como veio, em vez de
186
- * virar `../../..` — fora da árvore ele é evidência legítima de escrita alheia,
187
- * não um output que faltou declarar.
87
+ * The event target relative to the tree that logged it, the same shape as the
88
+ * slice's `arquivos:`. `Write`/`Edit` targets are absolute; a target outside
89
+ * that tree comes back as it was.
188
90
  */
189
- function toRepoRelative(target, cwd) {
190
- const norm = String(target || '').replace(/\\/g, '/');
191
- const root = String(cwd || '').replace(/\\/g, '/').replace(/\/+$/, '');
91
+ function toTreeRelative(target, treePath) {
92
+ const norm = slash(target);
93
+ const root = slash(treePath).replace(/\/+$/, '');
192
94
  if (!root) return norm;
193
- // Prefixo comparado sem caixa: no Windows a mesma raiz chega como `R:/repo`
194
- // ou `r:/repo` conforme quem chamou o comando.
95
+ // Case-insensitive prefix: on Windows the same root arrives as `R:/repo` or `r:/repo`.
195
96
  return norm.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? norm.slice(root.length + 1) : norm;
196
97
  }
197
98
 
198
- /**
199
- * Arquivos que NÃO contam como "trabalho feito depois do relatório".
200
- *
201
- * Sem este recorte o veredito vira máquina de falso positivo, e um aviso que
202
- * dispara à toa é um aviso que se aprende a ignorar:
203
- * - o próprio relatório: gravá-lo É um Write, e ele cai na janela da task por
204
- * construção — todo relatório bem-comportado pareceria stale;
205
- * - qualquer coisa sob `.morph/`: `tasks.json`, `feature.json`, `recap.md`,
206
- * `decisions.md` são escritas do ORQUESTRADOR, depois que o sub-agent
207
- * terminou. São o fluxo normal, não trabalho não relatado.
208
- *
209
- * `includes` e não `startsWith`, de propósito: o alvo já vem relativizado, mas
210
- * um write no `.morph/` de OUTRA árvore não fica sob esta raiz e continua
211
- * absoluto — `startsWith('.morph/')` deixaria esse passar como trabalho. Não há
212
- * código-fonte sob `.morph/` para o `includes` pegar por engano: a pasta é
213
- * estado, por design.
214
- */
215
- function isBookkeepingWrite(target, reportRel) {
216
- if (!target) return true;
217
- if (target === reportRel) return true;
218
- return target.includes('.morph/');
99
+ const reportSuffix = (feature, sliceId) => `.morph/features/${feature}/3-implement/reports/${sliceId}.md`.toLowerCase();
100
+
101
+ /** Every successful Write/Edit of every tree of this repo, oldest first. */
102
+ function loadWrites(cwd) {
103
+ let trees = [];
104
+ try { trees = loadTelemetryAllTrees(cwd); } catch { trees = []; }
105
+ const events = [];
106
+ for (const tree of trees) {
107
+ for (const e of tree.events) {
108
+ if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
109
+ if (isFailedToolCall(e)) continue;
110
+ const ts = Date.parse(e?.ts ?? '');
111
+ if (!Number.isFinite(ts)) continue;
112
+ events.push({ ...e, rel: toTreeRelative(e.target, tree.path), tsMs: ts });
113
+ }
114
+ }
115
+ return { available: trees.some((t) => t.events.length > 0), writes: dedupeEvents(events) };
219
116
  }
220
117
 
221
118
  /**
222
- * Evidência OBSERVADA: arquivos que a task de fato escreveu, segundo a
223
- * telemetria — não segundo o que o agente declarou em `outputs`.
119
+ * Evidence OBSERVED in the telemetry, never the agent's own word.
224
120
  *
225
- * A distinção é o ponto todo. `outputs` é auto-declarado pelo agente, e
226
- * `trace-autogen` lê a MESMA lista — as duas checagens que pareciam
227
- * independentes eram a mesma afirmação lida duas vezes. `events.jsonl` registra
228
- * cada `Write`/`Edit` no momento em que aconteceu, com `ts`; `task-times.json`
229
- * dá a janela `[start, end]` da task. Cruzando os dois sai o que de fato foi
230
- * tocado, sem depender da palavra de ninguém.
231
- *
232
- * Fail-open: sem telemetria (projeto legado, hook desligado) devolve
233
- * `{ available: false }` e o comando volta ao comportamento anterior.
121
+ * The dispatch that owns the slice is the sub-agent that wrote its report —
122
+ * the latest such write carrying an `agentId`; an orchestrator edit made
123
+ * afterwards does not hide it. Without a report, any write to one of the
124
+ * slice's files says the work landed.
234
125
  */
235
- function observedEvidence(cwd, feature, task) {
236
- const times = readTaskTimes(cwd)?.[feature]?.[task.id];
237
- if (!times?.start) return { available: false };
238
-
239
- const events = readTelemetryEvents(cwd);
240
- if (!events.length) return { available: false };
241
-
242
- const startMs = Date.parse(times.start);
243
- // Janela aberta à direita quando a task não fechou: uma task ainda em
244
- // andamento tem trabalho observável, e recusar auditá-la esconde exatamente
245
- // o caso do relatório escrito cedo demais.
246
- const endMs = times.end ? Date.parse(times.end) : Number.POSITIVE_INFINITY;
247
- if (!Number.isFinite(startMs)) return { available: false };
248
-
249
- const reportRel = `.morph/features/${feature}/3-implement/reports/${task.id}.md`;
250
- const writes = [];
251
- for (const e of events) {
252
- if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
253
- if (isFailedToolCall(e)) continue;
254
- const ts = Date.parse(e?.ts ?? '');
255
- if (!Number.isFinite(ts) || ts < startMs || ts > endMs) continue;
256
- const target = toRepoRelative(e.target, cwd);
257
- if (isBookkeepingWrite(target, reportRel)) continue;
258
- writes.push({ target, ts });
126
+ function observedEvidence(telemetry, feature, slice) {
127
+ if (!telemetry.available) return { available: false };
128
+ const suffix = reportSuffix(feature, slice.id);
129
+ const isReport = (w) => slash(w.target).toLowerCase().endsWith(suffix);
130
+ const declared = new Set((slice.files ?? []).map((f) => slash(f).replace(/^\.\//, '').toLowerCase()));
131
+ const isSliceFile = (w) => declared.has(w.rel.toLowerCase());
132
+
133
+ const reportWrites = telemetry.writes.filter((w) => isReport(w) && w.agentId);
134
+ const agentId = reportWrites.length ? reportWrites[reportWrites.length - 1].agentId : null;
135
+ if (!agentId) {
136
+ return { available: true, agentId: null, sliceWrites: telemetry.writes.filter(isSliceFile).length };
259
137
  }
260
138
 
261
- const declared = new Set((Array.isArray(task.outputs) ? task.outputs : [])
262
- .map((p) => String(p).replace(/\\/g, '/')));
263
- const observed = [...new Set(writes.map((w) => w.target))];
264
-
139
+ const work = telemetry.writes.filter((w) => w.agentId === agentId && !isReport(w) && !w.rel.includes('.morph/'));
140
+ const observed = [...new Set(work.map((w) => w.rel))];
141
+ const observedKeys = new Set(observed.map((f) => f.toLowerCase()));
265
142
  return {
266
143
  available: true,
144
+ agentId,
267
145
  observed,
268
- lastWriteAt: writes.length ? new Date(Math.max(...writes.map((w) => w.ts))).toISOString() : null,
269
- undeclared: observed.filter((p) => !declared.has(p)),
270
- unobserved: [...declared].filter((p) => !observed.includes(p)),
146
+ lastWriteAt: work.length ? new Date(Math.max(...work.map((w) => w.tsMs))).toISOString() : null,
147
+ undeclared: observed.filter((f) => !declared.has(f.toLowerCase())),
148
+ unobserved: (slice.files ?? []).filter((f) => !observedKeys.has(slash(f).replace(/^\.\//, '').toLowerCase())),
149
+ sliceWrites: work.filter(isSliceFile).length,
271
150
  };
272
151
  }
273
152
 
274
- /** O relatório é mais velho que o último trabalho observado da própria task? */
153
+ /** Is the report older than the last work its own dispatch observed? */
275
154
  function isReportStale(reportFsPath, observed) {
276
- if (!observed?.available || !observed.lastWriteAt) return false;
155
+ if (!observed?.agentId || !observed.lastWriteAt) return false;
277
156
  try {
278
- const writtenAt = statSync(reportFsPath).mtimeMs;
279
- return writtenAt + STALE_TOLERANCE_MS < Date.parse(observed.lastWriteAt);
157
+ return statSync(reportFsPath).mtimeMs + STALE_TOLERANCE_MS < Date.parse(observed.lastWriteAt);
280
158
  } catch {
281
159
  return false;
282
160
  }
283
161
  }
284
162
 
285
163
  /** Combine report state + evidence into a verdict and the action it implies. */
286
- function decide(report, evidence, stale) {
164
+ function decide(report, observed, stale) {
287
165
  if (report.exists && report.complete) {
288
- // A integridade estrutural (os 4 itens presentes) responde "o relatório
289
- // está completo?", nunca "o relatório ainda é verdade?". Caso real: um
290
- // sub-agent gravou o esqueleto dizendo "teste ainda não escrito — em
291
- // andamento", seguiu trabalhando, escreveu os 9 testes e foi cortado. O
292
- // arquivo ficou completo e MENTINDO, e saía `ok` aqui.
166
+ // Structural integrity answers "is the report complete?", never "is it
167
+ // still true?": an agent can write it early, keep working and be cut off.
293
168
  return stale
294
169
  ? { verdict: 'report-stale', action: 'refresh-from-evidence' }
295
170
  : { verdict: 'ok', action: 'proceed' };
296
171
  }
297
- // A PARCIAL report is an agent that stopped short (turn ceiling): the work
298
- // resumes from its last `wip` commit — completing it DEGRADADO would stamp
299
- // unfinished work as done.
172
+ // PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
173
+ // `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
300
174
  if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
301
175
  if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
302
- if (evidence.outputsExpected === 0) {
303
- // No declared outputs means no evidence either way. Re-dispatching costs a
304
- // sub-agent; silently accepting a phantom completion costs correctness.
305
- return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
306
- }
307
- return evidence.outputsPresent > 0
176
+ if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
177
+ return observed.sliceWrites > 0
308
178
  ? { verdict: 'work-landed-report-missing', action: 'accept-degraded' }
309
179
  : { verdict: 'no-work-no-report', action: 'redispatch' };
310
180
  }
311
181
 
312
- function checkTask(cwd, feature, task) {
313
- const fsPath = reportPath(cwd, feature, task.id);
314
- const report = inspectReport(fsPath);
315
- const evidence = outputEvidence(cwd, task.outputs);
316
- const observed = observedEvidence(cwd, feature, task);
182
+ function checkSlice(cwd, feature, slice, telemetry) {
183
+ const fsPath = sliceReportPath(join(cwd, '.morph', 'features', feature), slice.id);
184
+ const report = inspectReport(fsPath, { covers: slice.covers ?? [] });
185
+ const observed = observedEvidence(telemetry, feature, slice);
317
186
  const stale = report.exists && isReportStale(fsPath, observed);
318
- return { task: task.id, report, evidence, observed, ...decide(report, evidence, stale) };
187
+ return { slice: slice.id, report, observed, ...decide(report, observed, stale) };
319
188
  }
320
189
 
321
190
  const ACTION_HINT = {
322
191
  proceed: 'relatório íntegro — siga o fluxo normal.',
323
- 'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: a task seguiu escrevendo arquivos depois que ele foi gravado, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
324
- 'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre a falha de protocolo no recap.',
325
- resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a task NÃO está pronta. Não carimbe DEGRADADO nem pontue — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
326
- 'accept-degraded': 'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório canônico a partir da mensagem de retorno, carimbado como DEGRADADO, e registre a falha de protocolo no recap e nas `notes` da task.',
327
- redispatch: 're-despache a task UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
192
+ 'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
193
+ 'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
194
+ resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
195
+ 'accept-degraded': 'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
196
+ redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
328
197
  };
329
198
 
330
199
  function snapshotPath(cwd, feature) {
@@ -332,30 +201,29 @@ function snapshotPath(cwd, feature) {
332
201
  }
333
202
 
334
203
  /**
335
- * The tree block, compared with the snapshot taken before the evaluator was
204
+ * The tree block, compared with the snapshot taken before the review agents were
336
205
  * dispatched when there is one. `beforeEval` takes a new snapshot after the
337
206
  * comparison, so the output still shows what changed since the previous one.
338
207
  */
339
- function treeFor(cwd, feature, beforeEval, tasks = []) {
208
+ function treeFor(cwd, feature, beforeEval, slices = []) {
340
209
  const path = snapshotPath(cwd, feature);
341
210
  let snapshot = null;
342
211
  try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
343
212
  const tree = treeState(cwd, { snapshot });
344
213
  if (!beforeEval) return { tree };
345
- const evaluatorModel = evaluatorDispatch(cwd, tasks);
214
+ const reviewModels = reviewDispatch(cwd, feature, slices);
346
215
  const next = snapshotTree(cwd);
347
- if (!next) return { tree, beforeEval: null, evaluatorModel };
216
+ if (!next) return { tree, beforeEval: null, reviewModels };
348
217
  atomicWriteJson(path, next);
349
- return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, evaluatorModel };
218
+ return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
350
219
  }
351
220
 
352
221
  /**
353
222
  * @param {string} feature
354
- * @param {string|string[]|undefined} taskId - several ids audit a parallel
355
- * batch whose tasks are not `done` yet, which the whole-feature mode skips
356
- * @param {{ json?: boolean, cwd?: string }} [options]
223
+ * @param {string[]|undefined} sliceIds - audits only these slices; none audits every slice
224
+ * @param {{ json?: boolean, cwd?: string, beforeEval?: boolean }} [options]
357
225
  */
358
- export function reportCheckCommand(feature, taskId, options = {}) {
226
+ export function reportCheckCommand(feature, sliceIds, options = {}) {
359
227
  const cwd = options.cwd || process.cwd();
360
228
  const emit = (obj, code) => {
361
229
  if (options.json) console.log(JSON.stringify(obj, null, 2));
@@ -363,39 +231,24 @@ export function reportCheckCommand(feature, taskId, options = {}) {
363
231
  process.exit(code);
364
232
  };
365
233
 
366
- const tasksPath = join(cwd, '.morph', 'features', feature, '2-plan', 'tasks.json');
367
- if (!existsSync(tasksPath)) {
368
- emit({ error: `feature "${feature}" não tem 2-plan/tasks.json — nada a auditar`, feature }, 1);
234
+ const { status, checks } = readPlanChecks(cwd, feature);
235
+ if (!checks) {
236
+ emit({ error: `feature "${feature}" sem 2-plan/checks.json legível (${status}) — rode morph-spec plan-check ${feature}`, feature }, 1);
369
237
  }
370
-
371
- let tasks = [];
372
- try {
373
- const parsed = JSON.parse(readFileSync(tasksPath, 'utf8'));
374
- tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
375
- } catch {
376
- emit({ error: `2-plan/tasks.json de "${feature}" está corrompido`, feature }, 1);
377
- }
378
-
379
- if (Array.isArray(taskId)) {
380
- const unknown = taskId.filter((id) => !tasks.some((t) => t.id === id));
381
- if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
382
- const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
383
- const clean = results.every((r) => r.verdict === 'ok');
384
- emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
385
- }
386
-
387
- if (taskId) {
388
- const task = tasks.find((t) => t.id === taskId);
389
- if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
390
- const result = checkTask(cwd, feature, task);
391
- emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval, tasks) }, result.verdict === 'ok' ? 0 : 1);
392
- }
393
-
394
- // Whole-feature audit: every DONE task owes a report; a pending one does not.
395
- const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
396
- const evaluator = inspectEvaluatorReport(evaluatorPath(cwd, feature));
238
+ const slices = checks.slices;
239
+ const ids = Array.isArray(sliceIds) ? sliceIds : sliceIds ? [sliceIds] : [];
240
+ const unknown = ids.filter((id) => !slices.some((s) => s.id === id));
241
+ if (unknown.length) emit({ error: `fatia(s) ${unknown.join(', ')} não existe(m) no plano de ${feature}`, feature }, 1);
242
+
243
+ const telemetry = loadWrites(cwd);
244
+ const chosen = ids.length ? ids.map((id) => slices.find((s) => s.id === id)) : slices;
245
+ const results = chosen.map((s) => {
246
+ const r = checkSlice(cwd, feature, s, telemetry);
247
+ return { ...r, hint: ACTION_HINT[r.action] };
248
+ });
397
249
  const clean = results.every((r) => r.verdict === 'ok');
398
- emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
250
+ const reviews = ids.length ? undefined : inspectReviewReports(cwd, feature);
251
+ emit({ feature, results, ...(reviews ? { reviews } : {}), clean, ...treeFor(cwd, feature, options.beforeEval, slices) }, clean ? 0 : 1);
399
252
  }
400
253
 
401
254
  function printHuman(obj) {
@@ -403,37 +256,30 @@ function printHuman(obj) {
403
256
  console.error(chalk.red(`\n❌ ${obj.error}\n`));
404
257
  return;
405
258
  }
406
- const line = (r) => {
259
+ console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
260
+ if (!obj.results.length) console.log(chalk.gray(' nenhuma fatia no plano — nada a auditar'));
261
+ for (const r of obj.results) {
407
262
  const icon = r.verdict === 'ok' ? chalk.green('✓') : chalk.yellow('⚠');
408
- const ev = r.evidence.outputsExpected
409
- ? `outputs ${r.evidence.outputsPresent}/${r.evidence.outputsExpected}`
410
- : 'outputs não declarados';
411
- console.log(` ${icon} ${String(r.task).padEnd(6)} ${r.verdict.padEnd(32)} ${ev}`);
263
+ const who = r.observed.agentId ? `agente ${String(r.observed.agentId).slice(0, 8)}` : (r.observed.available ? 'sem dispatch' : 'sem telemetria');
264
+ console.log(` ${icon} ${String(r.slice).padEnd(5)} ${r.verdict.padEnd(32)} ${who}`);
412
265
  if (r.verdict !== 'ok') {
413
266
  if (r.report.exists && r.report.missing.length) {
414
267
  console.log(chalk.gray(` faltando no relatório: ${r.report.missing.join(', ')}`));
415
268
  }
416
- console.log(chalk.gray(` → ${ACTION_HINT[r.action]}`));
269
+ console.log(chalk.gray(` → ${r.hint}`));
417
270
  }
418
- };
419
-
420
- if (obj.task) {
421
- console.log(chalk.bold(`\n📄 report-check ${obj.feature} ${obj.task}`));
422
- line(obj);
423
- printTree(obj.tree);
424
- console.log('');
425
- return;
426
271
  }
427
-
428
- console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
429
- if (!obj.results.length) console.log(chalk.gray(' nenhuma task done — nada a auditar'));
430
- for (const r of obj.results) line(r);
431
- if (obj.evaluator) {
272
+ for (const [role, r] of Object.entries(obj.reviews ?? {})) {
273
+ if (!r.exists && role === 'security') continue;
432
274
  console.log(
433
- ` ${obj.evaluator.exists ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
434
- + `evaluator-report.md ${obj.evaluator.exists ? (obj.evaluator.complete ? 'íntegro' : obj.evaluator.partial ? 'PARCIAL' : 'incompleto') : 'ausente'}`,
275
+ ` ${r.complete ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
276
+ + `${reviewFileName(role)} ${r.exists ? (r.complete ? `íntegro (${r.verdict})` : r.partial ? 'PARCIAL' : 'incompleto') : 'ausente'}`,
435
277
  );
436
278
  }
279
+ if (obj.reviewModels) {
280
+ const m = obj.reviewModels;
281
+ console.log(chalk.gray(` dispatch: verifier ${m.verifier.model}, reviewer ${m.reviewer.model}${m.security.required ? `, security ${m.security.model}` : ''}`));
282
+ }
437
283
  printTree(obj.tree);
438
284
  console.log('');
439
285
  }