@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.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -1,330 +1,199 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `morph-spec report-check <feature> [
|
|
3
|
-
* of the missing-report protocol
|
|
2
|
+
* `morph-spec report-check <feature> [slice...] [--json]` — the deterministic
|
|
3
|
+
* half of the missing-report protocol.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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-
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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)
|
|
28
|
-
*
|
|
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 {
|
|
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 {
|
|
40
|
-
import {
|
|
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 {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
190
|
-
const norm =
|
|
191
|
-
const root =
|
|
91
|
+
function toTreeRelative(target, treePath) {
|
|
92
|
+
const norm = slash(target);
|
|
93
|
+
const root = slash(treePath).replace(/\/+$/, '');
|
|
192
94
|
if (!root) return norm;
|
|
193
|
-
//
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
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(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
const
|
|
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:
|
|
269
|
-
undeclared: observed.filter((
|
|
270
|
-
unobserved: [
|
|
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
|
-
/**
|
|
153
|
+
/** Is the report older than the last work its own dispatch observed? */
|
|
275
154
|
function isReportStale(reportFsPath, observed) {
|
|
276
|
-
if (!observed?.
|
|
155
|
+
if (!observed?.agentId || !observed.lastWriteAt) return false;
|
|
277
156
|
try {
|
|
278
|
-
|
|
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,
|
|
164
|
+
function decide(report, observed, stale) {
|
|
287
165
|
if (report.exists && report.complete) {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
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
|
-
//
|
|
298
|
-
//
|
|
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 (
|
|
303
|
-
|
|
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
|
|
313
|
-
const fsPath =
|
|
314
|
-
const report = inspectReport(fsPath);
|
|
315
|
-
const
|
|
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 {
|
|
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:
|
|
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
|
|
325
|
-
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a
|
|
326
|
-
'accept-degraded': 'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório
|
|
327
|
-
redispatch: 're-despache a
|
|
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
|
|
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,
|
|
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
|
|
214
|
+
const reviewModels = reviewDispatch(cwd, feature, slices);
|
|
346
215
|
const next = snapshotTree(cwd);
|
|
347
|
-
if (!next) return { tree, beforeEval: null,
|
|
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 },
|
|
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
|
|
355
|
-
*
|
|
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,
|
|
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
|
|
367
|
-
if (!
|
|
368
|
-
emit({ error: `feature "${feature}"
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
409
|
-
|
|
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(` → ${
|
|
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
|
-
|
|
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
|
-
` ${
|
|
434
|
-
+
|
|
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
|
}
|