@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- 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 +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -7,6 +7,7 @@ import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, deriv
|
|
|
7
7
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
8
8
|
import { evaluatorReportState, COMPOSITE_SCORE_TOKEN } from '../../lib/evaluator-report.js';
|
|
9
9
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
10
|
+
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The literal token `review-report.md` carries to declare which tree it
|
|
@@ -308,6 +309,20 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
308
309
|
process.exit(1);
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// The same backstop when no gate-check ran: the pause it WOULD record for
|
|
313
|
+
// the screen checklist, by the predicate advance.js reads too. `--force`
|
|
314
|
+
// does not bypass it, for the reason above.
|
|
315
|
+
if (gateName === 'uiux' && approvedBy === 'auto') {
|
|
316
|
+
const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
|
|
317
|
+
if (block) {
|
|
318
|
+
console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
|
|
319
|
+
console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
|
|
320
|
+
console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
|
|
321
|
+
console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
311
326
|
let selfAssessed = false;
|
|
312
327
|
if (gateName === 'review') {
|
|
313
328
|
if (feature.workType === 'hotfix') {
|
|
@@ -3,6 +3,8 @@ import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from '
|
|
|
3
3
|
import { join, resolve, sep } from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { promoteUseCases, SPECS_USECASES_PATHSPEC } from '../../lib/specs/usecase-promoter.js';
|
|
6
|
+
import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
|
|
7
|
+
import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
|
|
6
8
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
7
9
|
|
|
8
10
|
/** Refuse a move whose resolved target escapes `scopeDir`. */
|
|
@@ -64,7 +66,11 @@ function readReviewApproved(fjPath) {
|
|
|
64
66
|
* @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
|
|
65
67
|
* @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
|
|
66
68
|
* committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
|
|
67
|
-
* refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}
|
|
69
|
+
* refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
|
|
70
|
+
* keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
|
|
71
|
+
* supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
|
|
72
|
+
* revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
|
|
73
|
+
* adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
|
|
68
74
|
*/
|
|
69
75
|
export function runArchive(feature, opts = {}) {
|
|
70
76
|
const cwd = opts.cwd || process.cwd();
|
|
@@ -158,10 +164,12 @@ export function runArchive(feature, opts = {}) {
|
|
|
158
164
|
// never to a refused close.
|
|
159
165
|
let promotedUseCases = [];
|
|
160
166
|
let refusedUseCases = [];
|
|
167
|
+
let keptSuperseded = [];
|
|
161
168
|
try {
|
|
162
|
-
const { promoted, refused } = promoteUseCases(cwd, archivePath);
|
|
169
|
+
const { promoted, refused, keptSuperseded: kept } = promoteUseCases(cwd, archivePath);
|
|
163
170
|
promotedUseCases = promoted;
|
|
164
171
|
refusedUseCases = refused;
|
|
172
|
+
keptSuperseded = kept;
|
|
165
173
|
if (promoted.length && useGit && staged) {
|
|
166
174
|
// This staging exists for the NEW files: promoted use cases are untracked,
|
|
167
175
|
// and the scoped `commit -- <pathspec>` below only knows paths git already
|
|
@@ -173,17 +181,39 @@ export function runArchive(feature, opts = {}) {
|
|
|
173
181
|
}
|
|
174
182
|
} catch { /* promotion must never block archiving */ }
|
|
175
183
|
|
|
184
|
+
// 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
|
|
185
|
+
// turn `superseded`, and the ADR index gains/loses files. After the
|
|
186
|
+
// promotion, so a substitute promoted just now is already in place (and
|
|
187
|
+
// one it refused is known not to be); same commit, same fail-open contract.
|
|
188
|
+
let lifecycle = { superseded: [], adrIndex: null, problems: [] };
|
|
189
|
+
try {
|
|
190
|
+
lifecycle = applySpecRevocations(cwd, archivePath, { refused: refusedUseCases });
|
|
191
|
+
} catch { /* revocation must never block archiving */ }
|
|
192
|
+
const acervoChanged = promotedUseCases.length > 0 || lifecycle.superseded.length > 0;
|
|
193
|
+
const adrIndexChanged = lifecycle.adrIndex?.changed === true;
|
|
194
|
+
if (useGit && staged) {
|
|
195
|
+
if (lifecycle.superseded.length) {
|
|
196
|
+
try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
197
|
+
}
|
|
198
|
+
if (adrIndexChanged) {
|
|
199
|
+
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
// 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
|
|
177
204
|
// (same anti-sweep discipline as `finish`'s worktree teardown). This is what
|
|
178
205
|
// lets the archived state travel with the branch on `finish` (archive-before-merge).
|
|
179
|
-
// The acervo
|
|
180
|
-
// pathspec that matches nothing, and that refusal would leave the
|
|
181
|
-
// and break the checkout/merge that follows.
|
|
206
|
+
// The acervo pathspecs join only when something under them changed: git
|
|
207
|
+
// refuses a pathspec that matches nothing, and that refusal would leave the
|
|
208
|
+
// tree dirty and break the checkout/merge that follows.
|
|
182
209
|
let committed = false;
|
|
183
210
|
if (useGit && staged) {
|
|
184
|
-
const pathspecs =
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
const pathspecs = [
|
|
212
|
+
featPathspec,
|
|
213
|
+
archivePathspec,
|
|
214
|
+
...(acervoChanged ? [SPECS_USECASES_PATHSPEC] : []),
|
|
215
|
+
...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
|
|
216
|
+
];
|
|
187
217
|
try {
|
|
188
218
|
execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
|
|
189
219
|
committed = true;
|
|
@@ -206,7 +236,14 @@ export function runArchive(feature, opts = {}) {
|
|
|
206
236
|
} catch { /* index is derivable — non-fatal */ }
|
|
207
237
|
}
|
|
208
238
|
|
|
209
|
-
|
|
239
|
+
const adr = lifecycle.adrIndex;
|
|
240
|
+
return {
|
|
241
|
+
ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases,
|
|
242
|
+
keptSuperseded,
|
|
243
|
+
supersededUseCases: lifecycle.superseded,
|
|
244
|
+
revocationProblems: lifecycle.problems,
|
|
245
|
+
adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
|
|
246
|
+
};
|
|
210
247
|
}
|
|
211
248
|
|
|
212
249
|
/**
|
|
@@ -253,6 +290,22 @@ export async function archiveCommand(featureName, options = {}) {
|
|
|
253
290
|
console.log(chalk.yellow(` ${r.message}`));
|
|
254
291
|
}
|
|
255
292
|
}
|
|
293
|
+
for (const k of result.keptSuperseded ?? []) {
|
|
294
|
+
console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
|
|
295
|
+
}
|
|
296
|
+
for (const s of result.supersededUseCases ?? []) {
|
|
297
|
+
console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
|
|
298
|
+
}
|
|
299
|
+
if (result.adrIndex) {
|
|
300
|
+
const { added, revoked, narrowed } = result.adrIndex;
|
|
301
|
+
console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
|
|
302
|
+
}
|
|
303
|
+
if (result.revocationProblems?.length) {
|
|
304
|
+
console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do spec.md NÃO aplicada(s):`));
|
|
305
|
+
for (const p of result.revocationProblems) {
|
|
306
|
+
console.log(chalk.yellow(` ${p.message}`));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
256
309
|
if (result.committed) console.log(chalk.gray(` Move committed on the current branch.\n`));
|
|
257
310
|
else if (result.staged) console.log(chalk.gray(` Move staged in git; commit it when ready.\n`));
|
|
258
311
|
else console.log('');
|
|
@@ -21,6 +21,8 @@ import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js'
|
|
|
21
21
|
import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
|
|
22
22
|
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
23
23
|
import { runArchive } from './archive.js';
|
|
24
|
+
import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
25
|
+
import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
|
|
24
26
|
|
|
25
27
|
/** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
|
|
26
28
|
const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
|
|
@@ -119,6 +121,14 @@ export async function finishCommand(feature, options = {}) {
|
|
|
119
121
|
// would otherwise get a silent merge they never asked for.
|
|
120
122
|
let resumed = false;
|
|
121
123
|
|
|
124
|
+
// ONE stamp for every backup this run writes: the shed's and the teardown's
|
|
125
|
+
// folders then count as a single backup under the retention.
|
|
126
|
+
const backupStamp = newBackupStamp();
|
|
127
|
+
|
|
128
|
+
// The backup retention rides the same single point, AFTER this run's own
|
|
129
|
+
// backups exist (the shed and the teardown both precede every success emit).
|
|
130
|
+
// This run's stamp is protected: the prune never takes the backup it just
|
|
131
|
+
// made room for, even when backups stamped by a clock running ahead outrank it.
|
|
122
132
|
const emit = (obj) => {
|
|
123
133
|
if (resumed) obj = { ...obj, resumed: true };
|
|
124
134
|
if (obj?.finished === true) {
|
|
@@ -126,6 +136,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
126
136
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
127
137
|
if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
|
|
128
138
|
} catch { /* fail-open */ }
|
|
139
|
+
const retention = enforceBackupRetention(root, { protectStamps: [backupStamp] });
|
|
140
|
+
if (retention) obj = { ...obj, backupRetention: retention };
|
|
129
141
|
}
|
|
130
142
|
console.log(JSON.stringify(obj));
|
|
131
143
|
};
|
|
@@ -413,7 +425,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
413
425
|
emit({ finished: false, error: 'status-failed', feature });
|
|
414
426
|
process.exit(1);
|
|
415
427
|
}
|
|
416
|
-
if (dirty.
|
|
428
|
+
if (withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim())).length) {
|
|
417
429
|
emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
|
|
418
430
|
process.exit(1);
|
|
419
431
|
}
|
|
@@ -475,6 +487,9 @@ export async function finishCommand(feature, options = {}) {
|
|
|
475
487
|
}
|
|
476
488
|
|
|
477
489
|
// ── ARCHIVE (inside morph/{f}) ────────────────────────────────────────────
|
|
490
|
+
// Every refusal is behind us: the leftover of a killed `graph refresh` goes now,
|
|
491
|
+
// before the archive — never before a precondition that could still refuse.
|
|
492
|
+
dropLeftoverIgnore(cleanCwd);
|
|
478
493
|
const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
|
|
479
494
|
if (!arch.ok) {
|
|
480
495
|
emit({ finished: false, error: `archive-${arch.error}`, feature });
|
|
@@ -489,9 +504,9 @@ export async function finishCommand(feature, options = {}) {
|
|
|
489
504
|
|
|
490
505
|
// ── INTEGRATE ─────────────────────────────────────────────────────────────
|
|
491
506
|
if (mode === 'merge') {
|
|
492
|
-
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch });
|
|
507
|
+
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
|
|
493
508
|
}
|
|
494
|
-
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch });
|
|
509
|
+
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
|
|
495
510
|
}
|
|
496
511
|
|
|
497
512
|
/**
|
|
@@ -582,6 +597,30 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
582
597
|
|
|
583
598
|
if (defaultBranch) out.defaultBranch = defaultBranch;
|
|
584
599
|
|
|
600
|
+
// A retenção dos backups também é mutação do finish real — a AUTOMÁTICA, que
|
|
601
|
+
// não toca backup sem manifesto (esses só o `doctor --worktrees --prune`
|
|
602
|
+
// poda). A contagem é a de AGORA: o backup que este finish gravar ainda pode
|
|
603
|
+
// empurrar mais um para fora.
|
|
604
|
+
try {
|
|
605
|
+
const retention = inspectFinishBackups(root);
|
|
606
|
+
if (retention.policy.skipped) {
|
|
607
|
+
out.backupRetention = { skipped: retention.policy.skipped };
|
|
608
|
+
warnings.push(`a retenção dos backups do finish será PULADA: ${retention.policy.skipped}`);
|
|
609
|
+
} else if (retention.outsideRetention || retention.overdueSecrets.length || retention.legacy) {
|
|
610
|
+
out.backupRetention = {
|
|
611
|
+
outsideRetention: retention.outsideRetention,
|
|
612
|
+
overdueSecrets: retention.overdueSecrets.length,
|
|
613
|
+
...(retention.legacy ? { legacyPending: retention.legacy } : {}),
|
|
614
|
+
};
|
|
615
|
+
if (retention.outsideRetention || retention.overdueSecrets.length) {
|
|
616
|
+
out.wouldDo.push(
|
|
617
|
+
`aplicar a retenção de .git/morph-spec/finish-backup: apagar ${retention.outsideRetention} backup(s) fora da política`
|
|
618
|
+
+ ` e ${retention.overdueSecrets.length} arquivo(s) .env* além de ${retention.policy.secretMaxAgeDays} dias`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch { /* o pré-voo nunca lança */ }
|
|
623
|
+
|
|
585
624
|
// ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
|
|
586
625
|
if (worktreePath) {
|
|
587
626
|
out.worktreePath = worktreePath.replace(/\\/g, '/');
|
|
@@ -664,14 +703,14 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
664
703
|
|
|
665
704
|
// ── merge integration (local, no push) ───────────────────────────────────────
|
|
666
705
|
|
|
667
|
-
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch }) {
|
|
706
|
+
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
|
|
668
707
|
const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
|
|
669
708
|
const ucNote = promotedNote(arch);
|
|
670
709
|
|
|
671
710
|
if (isWorktreeFlow) {
|
|
672
711
|
// The root stays on the default; shed this feature's own root-side scaffold,
|
|
673
712
|
// then guard the whole root before landing the merge commit.
|
|
674
|
-
const { shedBackup } = shedRootFeatureScope(root, feature);
|
|
713
|
+
const { shedBackup } = shedRootFeatureScope(root, feature, { stamp: backupStamp });
|
|
675
714
|
const backupNote = shedBackup ? { shedBackup } : {};
|
|
676
715
|
|
|
677
716
|
// Final gate before the merge commit lands. The pre-archive check above
|
|
@@ -721,7 +760,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
721
760
|
const deferred = sessionAnchoredInWorktree(worktreePath);
|
|
722
761
|
const teardown = deferred
|
|
723
762
|
? null
|
|
724
|
-
: await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
|
|
763
|
+
: await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
|
|
725
764
|
dropFromRootIndex(root, feature);
|
|
726
765
|
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
|
|
727
766
|
process.exit(0);
|
|
@@ -776,7 +815,7 @@ export function prNumberFromUrl(url) {
|
|
|
776
815
|
|
|
777
816
|
// ── PR integration (push + gh, no local merge) ───────────────────────────────
|
|
778
817
|
|
|
779
|
-
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch }) {
|
|
818
|
+
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp }) {
|
|
780
819
|
try {
|
|
781
820
|
execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
|
|
782
821
|
} catch (err) {
|
|
@@ -812,13 +851,13 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
812
851
|
// later merges (carrying features/{f} → archive/{f}), the default would hold
|
|
813
852
|
// the feature in BOTH features/ and archive/ at once — active and archived.
|
|
814
853
|
// The branch/PR owns the authoritative copy; the root scaffold is transient.
|
|
815
|
-
shedBackup = shedRootFeatureScope(root, feature).shedBackup;
|
|
854
|
+
shedBackup = shedRootFeatureScope(root, feature, { stamp: backupStamp }).shedBackup;
|
|
816
855
|
// Same deferral as --merge: never tear down the worktree the running
|
|
817
856
|
// session is anchored in (see sessionAnchoredInWorktree).
|
|
818
857
|
if (sessionAnchoredInWorktree(worktreePath)) {
|
|
819
858
|
teardownDeferred = true;
|
|
820
859
|
} else {
|
|
821
|
-
teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false });
|
|
860
|
+
teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false, stamp: backupStamp });
|
|
822
861
|
}
|
|
823
862
|
// The shed removed the root's features/{f} scaffold from disk, but runArchive
|
|
824
863
|
// dropped the feature only from the WORKTREE's index (its cwd). Drop it from
|
|
@@ -874,16 +913,25 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
874
913
|
// ── output helpers ────────────────────────────────────────────────────────
|
|
875
914
|
|
|
876
915
|
/**
|
|
877
|
-
* Report the
|
|
878
|
-
* any
|
|
879
|
-
* `usecase-promoter.js`'s reservation-backed guard)
|
|
880
|
-
*
|
|
881
|
-
*
|
|
916
|
+
* Report what the archive did to the acervo: use cases promoted into
|
|
917
|
+
* `.morph/specs/usecases/`, any REFUSED for a numbering collision
|
|
918
|
+
* (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
|
|
919
|
+
* promoted copies that stayed superseded because the acervo said so
|
|
920
|
+
* (`keptSuperseded`), the ones the feature's spec.md revoked
|
|
921
|
+
* (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
|
|
922
|
+
* revocations it could not apply
|
|
923
|
+
* (`revocationProblems`). Every key is absent when there is nothing to report,
|
|
924
|
+
* so a feature that touches no acervo keeps the exact output shape it had
|
|
925
|
+
* before any of them existed.
|
|
882
926
|
*/
|
|
883
927
|
function promotedNote(arch) {
|
|
884
928
|
return {
|
|
885
929
|
...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
|
|
886
930
|
...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
|
|
931
|
+
...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
|
|
932
|
+
...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
|
|
933
|
+
...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
|
|
934
|
+
...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
|
|
887
935
|
};
|
|
888
936
|
}
|
|
889
937
|
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
|
|
7
|
+
import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
|
|
7
8
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
8
9
|
import { minimatch } from 'minimatch';
|
|
9
10
|
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
@@ -195,6 +196,12 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
195
196
|
designSystemDivergence: false,
|
|
196
197
|
};
|
|
197
198
|
|
|
199
|
+
// Só onde o checklist decide algo: no fecho do design e no ship. Fato de
|
|
200
|
+
// disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
|
|
201
|
+
if (gateName === 'uiux' || gateName === 'review') {
|
|
202
|
+
signals.screenChecklist = collectScreenChecklist(cwd, featureName, gateName);
|
|
203
|
+
}
|
|
204
|
+
|
|
198
205
|
const evalScore = compositeEvalScore(feature);
|
|
199
206
|
if (evalScore !== undefined) {
|
|
200
207
|
signals.evalScore = evalScore;
|
|
@@ -392,7 +399,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
392
399
|
const trust = options.trust || readTrustFromConfig();
|
|
393
400
|
const { pause, reason } = shouldPauseGate({ trust }, signals);
|
|
394
401
|
|
|
395
|
-
const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust });
|
|
402
|
+
const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust, cwd });
|
|
396
403
|
|
|
397
404
|
if (options.json) {
|
|
398
405
|
console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
5
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
6
|
+
import { mockupsHash } from '../../lib/specs/screen-checklist.js';
|
|
6
7
|
|
|
7
8
|
export const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
|
|
8
9
|
|
|
@@ -42,17 +43,20 @@ export function readTrustFromConfig() {
|
|
|
42
43
|
*
|
|
43
44
|
* @param {string} featureName
|
|
44
45
|
* @param {string} gateName
|
|
45
|
-
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string }} decision
|
|
46
|
-
* @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string }>}
|
|
47
|
-
* the exact entry written (decidedAt is server-side, generated here).
|
|
46
|
+
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string }} decision
|
|
47
|
+
* @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string, mockupsHash?: string|null }>}
|
|
48
|
+
* the exact entry written (decidedAt is server-side, generated here). A uiux
|
|
49
|
+
* decision also carries the hash of the mockup it saw: it answers that
|
|
50
|
+
* mockup's pause, not the next one's.
|
|
48
51
|
*/
|
|
49
|
-
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust } = {}) {
|
|
52
|
+
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd() } = {}) {
|
|
50
53
|
const entry = {
|
|
51
54
|
pause,
|
|
52
55
|
reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
53
56
|
signals,
|
|
54
57
|
trust: trust || readTrustFromConfig(),
|
|
55
58
|
decidedAt: new Date().toISOString(),
|
|
59
|
+
...(gateName === 'uiux' ? { mockupsHash: mockupsHash(cwd, featureName) } : {}),
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
@@ -23,12 +23,18 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Exit 0 only for `ok` — anything needing an action exits non-zero so a caller
|
|
25
25
|
* can branch on it. `--json` always prints the machine-readable verdict.
|
|
26
|
+
*
|
|
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.
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
import chalk from 'chalk';
|
|
29
33
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
30
34
|
import { join } from 'path';
|
|
31
|
-
import { readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
35
|
+
import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
36
|
+
import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
|
|
37
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* The four mandatory items of the report contract, as detectable markers.
|
|
@@ -51,6 +57,16 @@ const REPORT_MARKERS = [
|
|
|
51
57
|
/** (1) "what was done" — prose beyond the structural markers. */
|
|
52
58
|
const MIN_PROSE_CHARS = 80;
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The partial-report marker of the dispatch contract (clause 5, ADR-004): a
|
|
62
|
+
* 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
|
+
* Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
|
|
67
|
+
*/
|
|
68
|
+
const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
69
|
+
|
|
54
70
|
function reportPath(cwd, feature, taskId) {
|
|
55
71
|
return join(cwd, '.morph', 'features', feature, '3-implement', 'reports', `${taskId}.md`);
|
|
56
72
|
}
|
|
@@ -99,7 +115,11 @@ export function inspectReport(path) {
|
|
|
99
115
|
.trim();
|
|
100
116
|
if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
119
|
+
const partial = PARTIAL_MARKER.test(firstLine);
|
|
120
|
+
if (partial) missing.push('partial');
|
|
121
|
+
|
|
122
|
+
return { exists: true, complete: missing.length === 0, missing, partial };
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/** How many of a task's declared outputs actually exist on disk. */
|
|
@@ -206,6 +226,7 @@ function observedEvidence(cwd, feature, task) {
|
|
|
206
226
|
const writes = [];
|
|
207
227
|
for (const e of events) {
|
|
208
228
|
if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
|
|
229
|
+
if (isFailedToolCall(e)) continue;
|
|
209
230
|
const ts = Date.parse(e?.ts ?? '');
|
|
210
231
|
if (!Number.isFinite(ts) || ts < startMs || ts > endMs) continue;
|
|
211
232
|
const target = toRepoRelative(e.target, cwd);
|
|
@@ -249,6 +270,10 @@ function decide(report, evidence, stale) {
|
|
|
249
270
|
? { verdict: 'report-stale', action: 'refresh-from-evidence' }
|
|
250
271
|
: { verdict: 'ok', action: 'proceed' };
|
|
251
272
|
}
|
|
273
|
+
// A PARCIAL report is an agent that stopped short (turn ceiling): the work
|
|
274
|
+
// resumes from its last `wip` commit — completing it DEGRADADO would stamp
|
|
275
|
+
// unfinished work as done.
|
|
276
|
+
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
252
277
|
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
253
278
|
if (evidence.outputsExpected === 0) {
|
|
254
279
|
// No declared outputs means no evidence either way. Re-dispatching costs a
|
|
@@ -273,13 +298,36 @@ const ACTION_HINT = {
|
|
|
273
298
|
proceed: 'relatório íntegro — siga o fluxo normal.',
|
|
274
299
|
'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.',
|
|
275
300
|
'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.',
|
|
301
|
+
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`.',
|
|
276
302
|
'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.',
|
|
277
303
|
redispatch: 're-despache a task UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
278
304
|
};
|
|
279
305
|
|
|
306
|
+
function snapshotPath(cwd, feature) {
|
|
307
|
+
return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The tree block, compared with the snapshot taken before the evaluator was
|
|
312
|
+
* dispatched when there is one. `beforeEval` takes a new snapshot after the
|
|
313
|
+
* comparison, so the output still shows what changed since the previous one.
|
|
314
|
+
*/
|
|
315
|
+
function treeFor(cwd, feature, beforeEval) {
|
|
316
|
+
const path = snapshotPath(cwd, feature);
|
|
317
|
+
let snapshot = null;
|
|
318
|
+
try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
|
|
319
|
+
const tree = treeState(cwd, { snapshot });
|
|
320
|
+
if (!beforeEval) return { tree };
|
|
321
|
+
const next = snapshotTree(cwd);
|
|
322
|
+
if (!next) return { tree, beforeEval: null };
|
|
323
|
+
atomicWriteJson(path, next);
|
|
324
|
+
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length } };
|
|
325
|
+
}
|
|
326
|
+
|
|
280
327
|
/**
|
|
281
328
|
* @param {string} feature
|
|
282
|
-
* @param {string|undefined} taskId
|
|
329
|
+
* @param {string|string[]|undefined} taskId - several ids audit a parallel
|
|
330
|
+
* batch whose tasks are not `done` yet, which the whole-feature mode skips
|
|
283
331
|
* @param {{ json?: boolean, cwd?: string }} [options]
|
|
284
332
|
*/
|
|
285
333
|
export function reportCheckCommand(feature, taskId, options = {}) {
|
|
@@ -303,18 +351,26 @@ export function reportCheckCommand(feature, taskId, options = {}) {
|
|
|
303
351
|
emit({ error: `2-plan/tasks.json de "${feature}" está corrompido`, feature }, 1);
|
|
304
352
|
}
|
|
305
353
|
|
|
354
|
+
if (Array.isArray(taskId)) {
|
|
355
|
+
const unknown = taskId.filter((id) => !tasks.some((t) => t.id === id));
|
|
356
|
+
if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
|
|
357
|
+
const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
|
|
358
|
+
const clean = results.every((r) => r.verdict === 'ok');
|
|
359
|
+
emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
360
|
+
}
|
|
361
|
+
|
|
306
362
|
if (taskId) {
|
|
307
363
|
const task = tasks.find((t) => t.id === taskId);
|
|
308
364
|
if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
|
|
309
365
|
const result = checkTask(cwd, feature, task);
|
|
310
|
-
emit({ feature, ...result, hint: ACTION_HINT[result.action] }, result.verdict === 'ok' ? 0 : 1);
|
|
366
|
+
emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval) }, result.verdict === 'ok' ? 0 : 1);
|
|
311
367
|
}
|
|
312
368
|
|
|
313
369
|
// Whole-feature audit: every DONE task owes a report; a pending one does not.
|
|
314
370
|
const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
|
|
315
371
|
const evaluator = inspectReport(evaluatorPath(cwd, feature));
|
|
316
372
|
const clean = results.every((r) => r.verdict === 'ok');
|
|
317
|
-
emit({ feature, results, evaluator, clean }, clean ? 0 : 1);
|
|
373
|
+
emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
318
374
|
}
|
|
319
375
|
|
|
320
376
|
function printHuman(obj) {
|
|
@@ -339,6 +395,7 @@ function printHuman(obj) {
|
|
|
339
395
|
if (obj.task) {
|
|
340
396
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature} ${obj.task}`));
|
|
341
397
|
line(obj);
|
|
398
|
+
printTree(obj.tree);
|
|
342
399
|
console.log('');
|
|
343
400
|
return;
|
|
344
401
|
}
|
|
@@ -346,9 +403,33 @@ function printHuman(obj) {
|
|
|
346
403
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
|
|
347
404
|
if (!obj.results.length) console.log(chalk.gray(' nenhuma task done — nada a auditar'));
|
|
348
405
|
for (const r of obj.results) line(r);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
406
|
+
if (obj.evaluator) {
|
|
407
|
+
console.log(
|
|
408
|
+
` ${obj.evaluator.exists ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
|
|
409
|
+
+ `evaluator-report.md ${obj.evaluator.exists ? (obj.evaluator.complete ? 'íntegro' : obj.evaluator.partial ? 'PARCIAL' : 'incompleto') : 'ausente'}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
printTree(obj.tree);
|
|
353
413
|
console.log('');
|
|
354
414
|
}
|
|
415
|
+
|
|
416
|
+
function printTree(tree) {
|
|
417
|
+
if (!tree) {
|
|
418
|
+
console.log(chalk.gray(' árvore: fora de um repositório git — não conferida'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const stat = tree.diffstat ? ` (+${tree.diffstat.insertions} −${tree.diffstat.deletions})` : '';
|
|
422
|
+
console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
|
|
423
|
+
for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
|
|
424
|
+
if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
|
|
425
|
+
if (tree.markers.length) {
|
|
426
|
+
console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
|
|
427
|
+
}
|
|
428
|
+
if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
|
|
429
|
+
const since = tree.sinceBeforeEval;
|
|
430
|
+
if (since) {
|
|
431
|
+
console.log(since.changedCount
|
|
432
|
+
? chalk.yellow(` ⚠ desde a foto de ${since.at}: ${since.changedCount} arquivo(s) mudaram — ${since.changed.join(', ')}`)
|
|
433
|
+
: chalk.green(` ✓ nada mudou fora de .morph/ desde a foto de ${since.at}`));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* how `morph-plan` gets that protection; omitting it degrades to "reserved,
|
|
12
12
|
* no owner recorded" — still collision-free, just unable to name the
|
|
13
13
|
* conflicting feature if one ever surfaces.
|
|
14
|
+
*
|
|
15
|
+
* `morph-spec usecase check <feature>` — see `usecaseCheckCommand` below.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import chalk from 'chalk';
|
|
@@ -18,6 +20,11 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
18
20
|
import { join } from 'path';
|
|
19
21
|
import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
|
|
20
22
|
import { reserveUseCases } from '../../lib/specs/usecase-registry.js';
|
|
23
|
+
import { checkUseCases } from '../../lib/specs/usecase-check.js';
|
|
24
|
+
import { checkLifecycle } from '../../lib/specs/acervo-lifecycle.js';
|
|
25
|
+
import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
|
|
26
|
+
import { isValidFeatureName } from '../project/worktree.js';
|
|
27
|
+
import { printLifecycleWarnings } from '../tasks/dag.js';
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Best-effort feature-name inference when `--feature` is omitted: only when
|
|
@@ -75,3 +82,62 @@ export function usecaseReserveCommand(options = {}) {
|
|
|
75
82
|
else console.log('');
|
|
76
83
|
process.exit(0);
|
|
77
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `morph-spec usecase check <feature> [--json]` — the use-case half of `dag`,
|
|
88
|
+
* runnable before `tasks.json` exists: the feature's use cases and the
|
|
89
|
+
* revocations its `spec.md` declares, against the acervo's lifecycle. With a
|
|
90
|
+
* `tasks.json`, also the task → use case plumbing `dag` checks.
|
|
91
|
+
*
|
|
92
|
+
* Exit 1 only for what `dag` also refuses (a task pointing at a use case that
|
|
93
|
+
* does not exist, a tasks.json that does not validate); lifecycle findings are
|
|
94
|
+
* warnings.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} feature
|
|
97
|
+
* @param {{ json?: boolean, cwd?: string }} [options]
|
|
98
|
+
*/
|
|
99
|
+
export async function usecaseCheckCommand(feature, options = {}) {
|
|
100
|
+
const cwd = options.cwd || process.cwd();
|
|
101
|
+
const fail = (error) => {
|
|
102
|
+
if (options.json) console.log(JSON.stringify({ feature, error }));
|
|
103
|
+
else console.error(chalk.red(`\n❌ ${error}\n`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!isValidFeatureName(feature)) fail(`nome de feature inválido: "${feature}"`);
|
|
108
|
+
const featureDir = join(cwd, '.morph', 'features', feature);
|
|
109
|
+
if (!existsSync(featureDir)) fail(`feature não encontrada: .morph/features/${feature}`);
|
|
110
|
+
|
|
111
|
+
const tasksPath = join(featureDir, '2-plan', 'tasks.json');
|
|
112
|
+
let tasksData = null;
|
|
113
|
+
if (existsSync(tasksPath)) {
|
|
114
|
+
try {
|
|
115
|
+
({ data: tasksData } = await parseTasksJsonLenient(tasksPath));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
fail(`tasks.json inválido: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const useCases = tasksData ? checkUseCases(tasksData, join(featureDir, '2-plan', 'usecases')) : null;
|
|
122
|
+
const lifecycle = checkLifecycle({ cwd, featureDir, tasksData });
|
|
123
|
+
const broken = useCases?.errors.length > 0;
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify({ feature, useCases, lifecycle }, null, 2));
|
|
127
|
+
process.exit(broken ? 1 : 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (broken) {
|
|
131
|
+
console.error(chalk.red(`\n❌ Use cases — ${useCases.errors.length} erro(s)\n`));
|
|
132
|
+
for (const e of useCases.errors) console.error(chalk.red(` ${e}`));
|
|
133
|
+
}
|
|
134
|
+
if (useCases?.warnings.length) {
|
|
135
|
+
console.error(chalk.yellow(`\n⚠ Use cases — ${useCases.warnings.length} aviso(s)`));
|
|
136
|
+
for (const w of useCases.warnings) console.error(chalk.yellow(` ${w}`));
|
|
137
|
+
}
|
|
138
|
+
printLifecycleWarnings(lifecycle);
|
|
139
|
+
if (!broken && !useCases?.warnings.length && !lifecycle.warnings.length) {
|
|
140
|
+
console.log(chalk.green(`\n✅ ${feature}: use cases e acervo sem achados\n`));
|
|
141
|
+
}
|
|
142
|
+
process.exit(broken ? 1 : 0);
|
|
143
|
+
}
|