@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- 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 +86 -21
- 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-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- 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 +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -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/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- 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 +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- 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/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- 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/dotnet-test-introspection.js +0 -632
- 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
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -25,27 +25,11 @@ import { samePathCase } from '../../lib/worktree/docker-audit.js';
|
|
|
25
25
|
|
|
26
26
|
const isWindows = platform() === 'win32';
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
* Resolved-path equality for a `cwd` string against the project root —
|
|
30
|
-
* `"./src/web"`, `"src/web/"` and (on win32) `"src\\web"` must compare equal
|
|
31
|
-
* to `"src/web"`. Case-insensitive on win32 only, through `samePathCase` of
|
|
32
|
-
* `src/lib/worktree/docker-audit.js` — the one copy of that rule (#87 F5): NTFS
|
|
33
|
-
* itself is case-insensitive, so two spellings that differ only in case are
|
|
34
|
-
* the same directory there and nowhere else.
|
|
35
|
-
*
|
|
36
|
-
* @param {string} root - Project root the cwd is relative to
|
|
37
|
-
* @param {string|null|undefined} cwd
|
|
38
|
-
* @returns {string}
|
|
39
|
-
*/
|
|
28
|
+
// NTFS is case-insensitive: case folding goes through `samePathCase`, the one copy of that rule.
|
|
40
29
|
function normCwd(root, cwd) {
|
|
41
30
|
return samePathCase(resolve(root, String(cwd ?? '.')).replace(/\\/g, '/'));
|
|
42
31
|
}
|
|
43
32
|
|
|
44
|
-
/**
|
|
45
|
-
* Check which required Claude Code plugins are installed.
|
|
46
|
-
* @param {Object} pluginsData - The `plugins` object from installed_plugins.json
|
|
47
|
-
* @returns {{ context7: boolean }}
|
|
48
|
-
*/
|
|
49
33
|
export function checkInstalledPlugins(pluginsData) {
|
|
50
34
|
const REQUIRED = ['context7'];
|
|
51
35
|
return Object.fromEntries(
|
|
@@ -54,9 +38,6 @@ export function checkInstalledPlugins(pluginsData) {
|
|
|
54
38
|
}
|
|
55
39
|
|
|
56
40
|
|
|
57
|
-
/**
|
|
58
|
-
* Check if morph-spec command is in PATH
|
|
59
|
-
*/
|
|
60
41
|
function isMorphSpecInPath() {
|
|
61
42
|
try {
|
|
62
43
|
const command = isWindows ? 'where morph-spec' : 'which morph-spec';
|
|
@@ -67,9 +48,6 @@ function isMorphSpecInPath() {
|
|
|
67
48
|
}
|
|
68
49
|
}
|
|
69
50
|
|
|
70
|
-
/**
|
|
71
|
-
* Detect if nvm-windows is installed
|
|
72
|
-
*/
|
|
73
51
|
function isUsingNvmWindows() {
|
|
74
52
|
if (!isWindows) return false;
|
|
75
53
|
|
|
@@ -81,9 +59,6 @@ function isUsingNvmWindows() {
|
|
|
81
59
|
}
|
|
82
60
|
}
|
|
83
61
|
|
|
84
|
-
/**
|
|
85
|
-
* Get npm global prefix
|
|
86
|
-
*/
|
|
87
62
|
function getNpmGlobalPrefix() {
|
|
88
63
|
try {
|
|
89
64
|
const prefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
@@ -93,12 +68,10 @@ function getNpmGlobalPrefix() {
|
|
|
93
68
|
}
|
|
94
69
|
}
|
|
95
70
|
|
|
96
|
-
// lib files (v5 — minimal surface)
|
|
97
71
|
const REQUIRED_LIB_FILES = [
|
|
98
72
|
'src/lib/stack-filter.js'
|
|
99
73
|
];
|
|
100
74
|
|
|
101
|
-
// command files (v5 CLI surface — installer-only)
|
|
102
75
|
const REQUIRED_COMMAND_FILES = [
|
|
103
76
|
'src/commands/project/init.js',
|
|
104
77
|
'src/commands/project/update.js',
|
|
@@ -112,20 +85,6 @@ const REQUIRED_COMMAND_FILES = [
|
|
|
112
85
|
'src/commands/context/context-refresh.js',
|
|
113
86
|
];
|
|
114
87
|
|
|
115
|
-
/**
|
|
116
|
-
* Validate a parsed `agents.json` against the v5 contract.
|
|
117
|
-
*
|
|
118
|
-
* Exported, and pure, so the tests can exercise the REAL rules. They used to
|
|
119
|
-
* re-implement this logic inline "because doctorCommand doesn't export
|
|
120
|
-
* individual check functions" — and the two promptly disagreed: the tests
|
|
121
|
-
* asserted a v4 document (version 4.x, `tier` and `active_phases` required)
|
|
122
|
-
* while this code treats exactly those fields as deprecated. Both stayed green
|
|
123
|
-
* because they never met. A canary that destroyed this whole check left the
|
|
124
|
-
* suite at 18/18.
|
|
125
|
-
*
|
|
126
|
-
* @param {Object} agentsData - parsed agents.json
|
|
127
|
-
* @returns {Array<{name: string, status: 'ok'|'warn'|'error', msg?: string}>}
|
|
128
|
-
*/
|
|
129
88
|
export function validateAgentsJson(agentsData) {
|
|
130
89
|
const checks = [];
|
|
131
90
|
const agentEntries = Object.entries(agentsData.agents || {})
|
|
@@ -133,7 +92,6 @@ export function validateAgentsJson(agentsData) {
|
|
|
133
92
|
const actualCount = agentEntries.length;
|
|
134
93
|
const declaredCount = agentsData.total_agents;
|
|
135
94
|
|
|
136
|
-
// 1: total_agents matches actual count
|
|
137
95
|
if (actualCount !== declaredCount) {
|
|
138
96
|
checks.push({
|
|
139
97
|
name: 'agents.json count',
|
|
@@ -144,7 +102,6 @@ export function validateAgentsJson(agentsData) {
|
|
|
144
102
|
checks.push({ name: 'agents.json count', status: 'ok', msg: `${actualCount} agents` });
|
|
145
103
|
}
|
|
146
104
|
|
|
147
|
-
// 2: required v5 personas present
|
|
148
105
|
const missingPersonas = REQUIRED_AGENTS.filter(id => !(agentsData.agents || {})[id]);
|
|
149
106
|
if (missingPersonas.length > 0) {
|
|
150
107
|
checks.push({
|
|
@@ -156,7 +113,6 @@ export function validateAgentsJson(agentsData) {
|
|
|
156
113
|
checks.push({ name: 'agents.json required personas', status: 'ok' });
|
|
157
114
|
}
|
|
158
115
|
|
|
159
|
-
// 3: v5 schema (flat personas, no tiers)
|
|
160
116
|
const version = agentsData.version || 'unknown';
|
|
161
117
|
const isV5 = version.startsWith('5.');
|
|
162
118
|
const v4Fields = [];
|
|
@@ -191,8 +147,7 @@ export function validateAgentsJson(agentsData) {
|
|
|
191
147
|
});
|
|
192
148
|
}
|
|
193
149
|
|
|
194
|
-
//
|
|
195
|
-
// value the installer refuses. ERROR, not warn: that persona is not generated.
|
|
150
|
+
// The installer's own rule, so this cannot pass a value it refuses. ERROR: that persona is not generated.
|
|
196
151
|
const maxTurnsErrors = agentEntries
|
|
197
152
|
.map(([id, data]) => maxTurnsError(id, data?.maxTurns))
|
|
198
153
|
.filter(Boolean);
|
|
@@ -200,7 +155,6 @@ export function validateAgentsJson(agentsData) {
|
|
|
200
155
|
? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
|
|
201
156
|
: { name: 'agents.json maxTurns', status: 'ok' });
|
|
202
157
|
|
|
203
|
-
// 5: surfaces — same installer rule, same severity: the persona is not generated.
|
|
204
158
|
const surfacesErrors = agentEntries
|
|
205
159
|
.map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
|
|
206
160
|
.filter(Boolean);
|
|
@@ -208,7 +162,6 @@ export function validateAgentsJson(agentsData) {
|
|
|
208
162
|
? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
|
|
209
163
|
: { name: 'agents.json surfaces', status: 'ok' });
|
|
210
164
|
|
|
211
|
-
// 6: effort — same installer rule, same severity.
|
|
212
165
|
const effortErrors = agentEntries
|
|
213
166
|
.map(([id, data]) => effortError(id, data?.effort))
|
|
214
167
|
.filter(Boolean);
|
|
@@ -228,12 +181,7 @@ export function validateAgentsJson(agentsData) {
|
|
|
228
181
|
return checks;
|
|
229
182
|
}
|
|
230
183
|
|
|
231
|
-
//
|
|
232
|
-
// Keep every entry pointing at a file that exists: a check that can never pass
|
|
233
|
-
// carries the same information as one that can never fail — none. This list
|
|
234
|
-
// still named two standards from a `workflows/` directory that no longer
|
|
235
|
-
// exists, which pinned `doctor --full` at a permanent 3/6 and made a real
|
|
236
|
-
// regression indistinguishable from the usual red.
|
|
184
|
+
// Every entry must point at an existing file: a check that can never pass reports nothing.
|
|
237
185
|
const FRAMEWORK_STANDARDS = [
|
|
238
186
|
'framework/standards/architecture/vertical-slice/vertical-slice.md',
|
|
239
187
|
'framework/standards/data/nosql/cosmos-db.md',
|
|
@@ -242,16 +190,13 @@ const FRAMEWORK_STANDARDS = [
|
|
|
242
190
|
'framework/standards/integration/mcp/mcp-tools.md'
|
|
243
191
|
];
|
|
244
192
|
|
|
245
|
-
// required personas expected in agents.json (v5 — the universal, stack-agnostic
|
|
246
|
-
// personas: evaluator (domain axis) and scout (stage axis))
|
|
247
193
|
const REQUIRED_AGENTS = [
|
|
248
|
-
'
|
|
194
|
+
'verifier',
|
|
195
|
+
'reviewer',
|
|
196
|
+
'security',
|
|
249
197
|
'scout'
|
|
250
198
|
];
|
|
251
199
|
|
|
252
|
-
/**
|
|
253
|
-
* Run full health checks
|
|
254
|
-
*/
|
|
255
200
|
async function doctorFullCommand(frameworkRoot) {
|
|
256
201
|
console.log(chalk.bold('\n🔬 MORPH-SPEC Full Health Check\n'));
|
|
257
202
|
console.log('─'.repeat(60));
|
|
@@ -270,15 +215,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
270
215
|
}
|
|
271
216
|
};
|
|
272
217
|
|
|
273
|
-
// Two layouts reach this command
|
|
274
|
-
//
|
|
275
|
-
// framework repo src/, framework/, no .morph/ at all
|
|
276
|
-
// installed project .morph/framework/, and the CLI in node_modules
|
|
277
|
-
//
|
|
278
|
-
// Half the checks assumed one layout and half the other, so `--full` could
|
|
279
|
-
// not pass anywhere: red in the repo (no .morph/) and red in a project (no
|
|
280
|
-
// src/). A check that can never pass reports exactly as much as one that can
|
|
281
|
-
// never fail. Each check now runs only where it means something.
|
|
218
|
+
// Two layouts reach this command (framework repo: src/, no .morph/; installed project:
|
|
219
|
+
// .morph/framework/, CLI in node_modules). Each check runs only where it means something.
|
|
282
220
|
const installedAgentsPath = join(frameworkRoot, '.morph/framework/agents.json');
|
|
283
221
|
const canonicalAgentsPath = join(frameworkRoot, 'framework/agents.json');
|
|
284
222
|
const inFrameworkRepo = !(await pathExists(installedAgentsPath))
|
|
@@ -287,9 +225,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
287
225
|
|
|
288
226
|
console.log(chalk.gray(`\n Contexto: ${inFrameworkRepo ? 'repo do framework' : 'projeto instalado'}`));
|
|
289
227
|
|
|
290
|
-
//
|
|
291
|
-
// An installed project has no src/ — the CLI lives in node_modules, and its
|
|
292
|
-
// integrity is npm's job, not the doctor's.
|
|
228
|
+
// An installed project has no src/: the CLI's integrity in node_modules is npm's job.
|
|
293
229
|
if (inFrameworkRepo) {
|
|
294
230
|
console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
|
|
295
231
|
const missingLibs = [];
|
|
@@ -310,9 +246,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
310
246
|
missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
|
|
311
247
|
}
|
|
312
248
|
|
|
313
|
-
// ── 3. Framework Standards ───────────────────────────────────────────────
|
|
314
|
-
// Same files, different root: the repo holds them at framework/standards/,
|
|
315
|
-
// an installed project at .morph/framework/standards/.
|
|
316
249
|
const stdsRoot = inFrameworkRepo ? frameworkRoot : join(frameworkRoot, '.morph');
|
|
317
250
|
console.log(chalk.cyan(`\n ${inFrameworkRepo ? '' : '.morph/'}framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
|
|
318
251
|
const missingStds = [];
|
|
@@ -323,7 +256,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
323
256
|
missingStds.length === 0, false,
|
|
324
257
|
missingStds.length > 0 ? `missing: ${missingStds.join(', ')}` : '');
|
|
325
258
|
|
|
326
|
-
// ── 5. Required Agents in agents.json ───────────────────────────────────
|
|
327
259
|
console.log(chalk.cyan(`\n ${inFrameworkRepo ? 'framework' : '.morph/framework'}/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
|
|
328
260
|
if (await pathExists(agentsPath)) {
|
|
329
261
|
try {
|
|
@@ -340,10 +272,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
340
272
|
check('agents.json', false, false, 'file not found');
|
|
341
273
|
}
|
|
342
274
|
|
|
343
|
-
//
|
|
344
|
-
// Only meaningful for an installed project: it compares what was installed
|
|
345
|
-
// against the canonical source. In the framework repo the two are the same
|
|
346
|
-
// file, so the comparison is vacuous and is skipped.
|
|
275
|
+
// Installed project only: in the framework repo source and installed copy are the same file.
|
|
347
276
|
if (!inFrameworkRepo && await pathExists(agentsPath) && await pathExists(canonicalAgentsPath)) {
|
|
348
277
|
try {
|
|
349
278
|
const installed = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
|
|
@@ -353,15 +282,10 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
353
282
|
const isFresh = installedVer === canonicalVer;
|
|
354
283
|
check(`agents.json version (${installedVer})`, isFresh, true,
|
|
355
284
|
isFresh ? '' : `stale — canonical is v${canonicalVer}, run 'morph-spec update' to sync`);
|
|
356
|
-
} catch {
|
|
285
|
+
} catch {}
|
|
357
286
|
}
|
|
358
287
|
|
|
359
|
-
//
|
|
360
|
-
// agents.json is the single source of truth; the .claude/agents/morph-*.md
|
|
361
|
-
// files are derived artifacts (ADR-001). Missing = broken dispatch (error);
|
|
362
|
-
// stale/hand-edited or orphan = drift (warning).
|
|
363
|
-
// Skipped in the framework repo: the generated `.claude/agents/morph-*.md`
|
|
364
|
-
// are an artifact of installing into a project, not of the source tree.
|
|
288
|
+
// Skipped in the framework repo: the generated `.claude/agents/morph-*.md` only exist in a project.
|
|
365
289
|
if (!inFrameworkRepo && await pathExists(agentsPath)) {
|
|
366
290
|
console.log(chalk.cyan('\n .claude/agents/ — Generated personas (drift vs agents.json)'));
|
|
367
291
|
try {
|
|
@@ -371,7 +295,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
371
295
|
try {
|
|
372
296
|
const rawCfg = (await fs.readFile(cfgPath, 'utf8')).replace(/^/, '');
|
|
373
297
|
projectTags = getProjectTags(JSON.parse(rawCfg));
|
|
374
|
-
} catch {
|
|
298
|
+
} catch {}
|
|
375
299
|
}
|
|
376
300
|
// The MCP registry comes from the package, like the installer's: the
|
|
377
301
|
// installed .morph/framework/ has no mcp/, and rendering without it
|
|
@@ -397,7 +321,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
397
321
|
}
|
|
398
322
|
}
|
|
399
323
|
|
|
400
|
-
// ── 6c. GitHub tooling — gh CLI (canonical; morph-spec does not use a GitHub MCP) ──
|
|
401
324
|
console.log(chalk.cyan('\n Tooling — GitHub via gh CLI'));
|
|
402
325
|
try {
|
|
403
326
|
execSync('gh --version', { stdio: 'pipe', timeout: 5000 });
|
|
@@ -407,7 +330,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
407
330
|
"GitHub ops (issues/PRs/releases) usam o gh CLI — instale https://cli.github.com e rode 'gh auth login'");
|
|
408
331
|
}
|
|
409
332
|
|
|
410
|
-
// ── 7. state.json schema version ────────────────────────────────────────
|
|
411
333
|
console.log(chalk.cyan('\n .morph/state.json — Schema Version'));
|
|
412
334
|
const statePath = join(frameworkRoot, '.morph/state.json');
|
|
413
335
|
if (await pathExists(statePath)) {
|
|
@@ -423,23 +345,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
423
345
|
check('state.json', false, true, 'not found (run: morph-spec init)');
|
|
424
346
|
}
|
|
425
347
|
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
// As outras duas camadas são o `config.schema.json` (declarativa) e
|
|
429
|
-
// `validateTestNodes()` chamada pelo `verify` (fail-closed, em uso). Esta
|
|
430
|
-
// aqui existe para o defeito chegar ANTES do gate: um `id` com typo em
|
|
431
|
-
// `tests[]` só se manifesta quando alguém roda `morph-spec verify` e lê um
|
|
432
|
-
// nó vermelho — o doctor o mostra na checagem de saúde, que é onde se olha
|
|
433
|
-
// primeiro. Reusa `validateTestNodes` (a MESMA autoridade que o verify
|
|
434
|
-
// consulta): uma segunda leitura do mesmo campo aqui seria uma segunda
|
|
435
|
-
// opinião sobre o que é um nó válido, e as duas divergiriam.
|
|
436
|
-
//
|
|
437
|
-
// PULA de verdade — sem check e sem cabeçalho — quando não há `config.json`
|
|
438
|
-
// ou quando `project.tests` não foi declarado. Um projeto sem declaração usa
|
|
439
|
-
// o fallback de quatro passos e não tem contrato a violar; este repositório
|
|
440
|
-
// é exatamente esse caso, e um check que não pode passar aqui é ruído, não
|
|
441
|
-
// achado (`test/commands/doctor.test.js`: `doctor --full` sem nenhum `✗`).
|
|
442
|
-
// WARN e nunca ERROR: a barreira fail-closed é o `verify`; o doctor informa.
|
|
348
|
+
// Mesma autoridade do `verify` (validateTestNodes). Pula sem `config.json` ou sem `project.tests`:
|
|
349
|
+
// não há contrato a violar, e `doctor --full` não pode ter ✗ aqui. WARN, nunca ERROR.
|
|
443
350
|
const testsConfigPath = morphConfigPath(frameworkRoot);
|
|
444
351
|
if (await pathExists(testsConfigPath)) {
|
|
445
352
|
let testsConfig = null;
|
|
@@ -458,12 +365,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
458
365
|
? broken.map((n) => n.reason ?? `invalid-test-node: ${n.id}`).join('; ')
|
|
459
366
|
: '');
|
|
460
367
|
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// WARN-only, never ✗: the fail-closed barrier is `verify`, this just informs.
|
|
464
|
-
// Compared by RESOLVED path, not raw string: "./src/web", "src/web/" and
|
|
465
|
-
// (win32) "src\\web" all address the same node as "src/web" and must not
|
|
466
|
-
// produce a spurious warning just because the spelling differs.
|
|
368
|
+
// A vitest/jest package.json that no declared node covers is a suite outside the gate (WARN only).
|
|
369
|
+
// Compared by RESOLVED path so a different spelling of the same dir doesn't warn.
|
|
467
370
|
const declaredCwds = new Set(declared.map((n) => normCwd(frameworkRoot, n.cwd)));
|
|
468
371
|
const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
|
|
469
372
|
.filter((pkg) => !declaredCwds.has(normCwd(frameworkRoot, pkg.cwd)));
|
|
@@ -474,10 +377,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
474
377
|
);
|
|
475
378
|
}
|
|
476
379
|
} else if (typeof testsConfig?.project?.testCommand === 'string' && testsConfig.project.testCommand.trim()) {
|
|
477
|
-
//
|
|
478
|
-
// string de shell), então cobertura aqui é só "o comando MENCIONA o
|
|
479
|
-
// runner do package.json" — não confirmar isso seria ruído para quem já
|
|
480
|
-
// roda vitest por dentro do testCommand.
|
|
380
|
+
// testCommand é opaco: cobertura aqui é só "o comando MENCIONA o runner do package.json".
|
|
481
381
|
const testCommand = testsConfig.project.testCommand;
|
|
482
382
|
const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
|
|
483
383
|
.filter((pkg) => !testCommand.includes(pkg.runner));
|
|
@@ -491,7 +391,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
491
391
|
}
|
|
492
392
|
}
|
|
493
393
|
|
|
494
|
-
// ── Display ──────────────────────────────────────────────────────────────
|
|
495
394
|
console.log('\n' + '─'.repeat(60));
|
|
496
395
|
console.log(chalk.bold('\nResults:\n'));
|
|
497
396
|
for (const c of checks) {
|
|
@@ -518,19 +417,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
518
417
|
}
|
|
519
418
|
}
|
|
520
419
|
|
|
521
|
-
|
|
522
|
-
* MCP health check: reads .mcp.json, settings.json, and settings.local.json
|
|
523
|
-
* and validates each configured MCP server's command binary and env var requirements.
|
|
524
|
-
*
|
|
525
|
-
* Strategy:
|
|
526
|
-
* - "ok" : command binary exists (or is a known runtime like npx/node) AND
|
|
527
|
-
* all env vars referenced in args/env are set in process.env
|
|
528
|
-
* - "warn" : binary found but env vars missing (MCP won't auth)
|
|
529
|
-
* - "missing": command binary not found in PATH
|
|
530
|
-
*
|
|
531
|
-
* NOTE: This does NOT test actual connectivity — that requires spawning the MCP.
|
|
532
|
-
* It's a static configuration health check that catches 80% of "MCP not working" cases.
|
|
533
|
-
*/
|
|
420
|
+
// Static config check only: connectivity would require spawning the MCP.
|
|
534
421
|
async function doctorMcpCommand(targetPath) {
|
|
535
422
|
logger.header('MCP Server Health Check');
|
|
536
423
|
logger.blank();
|
|
@@ -538,7 +425,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
538
425
|
const allMcpServers = {};
|
|
539
426
|
const sourcesFound = [];
|
|
540
427
|
|
|
541
|
-
// Primary: .mcp.json (Claude Code reads MCPs from here)
|
|
542
428
|
const mcpJsonPath = join(targetPath, '.mcp.json');
|
|
543
429
|
if (await pathExists(mcpJsonPath)) {
|
|
544
430
|
try {
|
|
@@ -549,10 +435,9 @@ async function doctorMcpCommand(targetPath) {
|
|
|
549
435
|
Object.assign(allMcpServers, mcps);
|
|
550
436
|
sourcesFound.push(`.mcp.json (${count})`);
|
|
551
437
|
}
|
|
552
|
-
} catch {
|
|
438
|
+
} catch {}
|
|
553
439
|
}
|
|
554
440
|
|
|
555
|
-
// Legacy: settings files (for backward compat)
|
|
556
441
|
const settingsPaths = [
|
|
557
442
|
join(targetPath, '.claude', 'settings.json'),
|
|
558
443
|
join(targetPath, '.claude', 'settings.local.json'),
|
|
@@ -573,7 +458,7 @@ async function doctorMcpCommand(targetPath) {
|
|
|
573
458
|
if (addedCount > 0) {
|
|
574
459
|
sourcesFound.push(`${relative(targetPath, sp)} (${addedCount})`);
|
|
575
460
|
}
|
|
576
|
-
} catch {
|
|
461
|
+
} catch {}
|
|
577
462
|
}
|
|
578
463
|
}
|
|
579
464
|
|
|
@@ -596,14 +481,11 @@ async function doctorMcpCommand(targetPath) {
|
|
|
596
481
|
let status = 'ok';
|
|
597
482
|
let isRemote = false;
|
|
598
483
|
|
|
599
|
-
// ── 0. Remote MCP detection ────────────────────────────────────────────
|
|
600
|
-
// Remote MCPs use HTTP transport (type: "http" or url property) — no binary to check
|
|
601
484
|
if (config.type === 'http' || (config.url && !config.command)) {
|
|
602
485
|
isRemote = true;
|
|
603
486
|
// Remote MCPs are always "ok" from a config perspective — auth happens at runtime
|
|
604
487
|
}
|
|
605
488
|
|
|
606
|
-
// ── 1. Binary check (stdio MCPs only) ──────────────────────────────────
|
|
607
489
|
const cmd = config.command || '';
|
|
608
490
|
if (!isRemote && cmd && !KNOWN_RUNTIMES.has(cmd)) {
|
|
609
491
|
try {
|
|
@@ -614,7 +496,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
614
496
|
}
|
|
615
497
|
}
|
|
616
498
|
|
|
617
|
-
// ── 2. Env vars from config.env ────────────────────────────────────────
|
|
618
499
|
for (const [key, val] of Object.entries(config.env || {})) {
|
|
619
500
|
if (typeof val === 'string' && (val.startsWith('${') || (val.startsWith('$') && !val.includes(' ')))) {
|
|
620
501
|
const varName = val.replace(/^\$\{?/, '').replace(/\}$/, '');
|
|
@@ -625,7 +506,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
625
506
|
}
|
|
626
507
|
}
|
|
627
508
|
|
|
628
|
-
// ── 3. Env refs in args array ──────────────────────────────────────────
|
|
629
509
|
const argsStr = (config.args || []).join(' ');
|
|
630
510
|
const envRefsInArgs = [...argsStr.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map(m => m[1]);
|
|
631
511
|
for (const varName of envRefsInArgs) {
|
|
@@ -638,7 +518,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
638
518
|
checks.push({ name, status, issues, cmd, isRemote });
|
|
639
519
|
}
|
|
640
520
|
|
|
641
|
-
// ── Display ────────────────────────────────────────────────────────────────
|
|
642
521
|
for (const c of checks) {
|
|
643
522
|
const typeLabel = c.isRemote
|
|
644
523
|
? chalk.gray(` [remote: ${allMcpServers[c.name]?.url || 'http'}]`)
|
|
@@ -677,23 +556,8 @@ async function doctorMcpCommand(targetPath) {
|
|
|
677
556
|
}
|
|
678
557
|
|
|
679
558
|
/**
|
|
680
|
-
* `
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
* Orphan worktrees used to accumulate invisibly: `fleet` only reports worktrees
|
|
684
|
-
* on a `morph/*` branch, so anything else never showed up anywhere.
|
|
685
|
-
*
|
|
686
|
-
* `--prune` runs the safe repairs: `git worktree prune` (admin records for
|
|
687
|
-
* directories that are already gone) and the removal of EMPTY unregistered
|
|
688
|
-
* residue directories, which by definition hold nothing that can be lost.
|
|
689
|
-
* Deleting a directory that still HOLDS something is reported with the exact
|
|
690
|
-
* command, never performed — same PROVE-or-DECLINE discipline as the finish
|
|
691
|
-
* teardown.
|
|
692
|
-
*
|
|
693
|
-
* `--prune` also applies the retention of the finish backups — the same policy
|
|
694
|
-
* every `finish` applies on its own, and the ONLY path that also applies it to
|
|
695
|
-
* the backups written before the retention existed (no manifest), counting
|
|
696
|
-
* them against the budget. Under `--json` its result rides in the JSON.
|
|
559
|
+
* `--prune` runs only safe repairs (`git worktree prune`, EMPTY unregistered dirs) and the
|
|
560
|
+
* finish-backup retention; a directory that still holds something is reported, never deleted.
|
|
697
561
|
*/
|
|
698
562
|
export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
699
563
|
const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
|
|
@@ -758,13 +622,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
758
622
|
}
|
|
759
623
|
|
|
760
624
|
const issues = auditWorktrees(targetPath);
|
|
761
|
-
// `
|
|
762
|
-
//
|
|
763
|
-
// and the docker findings from `docker-audit.js`: an orphan stack, a port
|
|
764
|
-
// held by another compose project) are REPORTED but are not failures — a
|
|
765
|
-
// `doctor --worktrees || ...` in CI must not go red because somebody has an
|
|
766
|
-
// open PR, a hand-made tree, or a container still up. The human path below
|
|
767
|
-
// splits the same way; both surfaces must agree on what counts as broken.
|
|
625
|
+
// `info`/`warn` entries are REPORTED, not failures: CI must not go red for an open PR, a hand-made
|
|
626
|
+
// tree or a running container. The human path below splits the same way.
|
|
768
627
|
const isProblem = (i) => !['info', 'warn'].includes(i.severity);
|
|
769
628
|
|
|
770
629
|
if (options.json) {
|
|
@@ -778,10 +637,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
778
637
|
return true;
|
|
779
638
|
}
|
|
780
639
|
|
|
781
|
-
// `unintegrated-work` is
|
|
782
|
-
//
|
|
783
|
-
// it as a problem is how a diagnostic starts nagging people into destroying
|
|
784
|
-
// things to make the warning go away.
|
|
640
|
+
// `unintegrated-work` is a worktree doing its job, not an inconsistency: counting it nags people
|
|
641
|
+
// into destroying work to silence the warning.
|
|
785
642
|
const informational = issues.filter((i) => !isProblem(i));
|
|
786
643
|
const problems = issues.filter(isProblem);
|
|
787
644
|
|
|
@@ -796,9 +653,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
796
653
|
console.log(` ${i.detail}`);
|
|
797
654
|
console.log(` → ${i.fix}`);
|
|
798
655
|
}
|
|
799
|
-
// An EMPTY unregistered
|
|
800
|
-
// be lost — so the hint must mention it, or the one issue `--prune` now fixes
|
|
801
|
-
// automatically would still read as a manual chore.
|
|
656
|
+
// An EMPTY unregistered dir is prunable too, so the hint must mention it.
|
|
802
657
|
const prunable = (i) => i.type === 'registered-missing'
|
|
803
658
|
|| i.type === 'dangling-registration'
|
|
804
659
|
|| (i.type === 'unregistered-dir' && i.empty);
|
|
@@ -815,29 +670,9 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
815
670
|
return false;
|
|
816
671
|
}
|
|
817
672
|
|
|
818
|
-
/** Dias a partir dos quais um PR aberto vira aviso. */
|
|
819
673
|
const PR_STALE_DAYS = 7;
|
|
820
674
|
|
|
821
|
-
/**
|
|
822
|
-
* As inconsistências de PR desta árvore.
|
|
823
|
-
*
|
|
824
|
-
* pr-awaiting-merge — PR aberto há muito, sem cópia local: trabalho em
|
|
825
|
-
* revisão que nenhuma superfície local reivindica.
|
|
826
|
-
* pr-merged-not-landed — o PR mergeou mas `archive/{f}` não está aqui. O
|
|
827
|
-
* archive viaja no diff do PR, então ou falta um
|
|
828
|
-
* `git pull`, ou ele não estava no diff. Branch
|
|
829
|
-
* `morph/*` cujo diff nunca tocou
|
|
830
|
-
* `.morph/{features,archive}/{f}/` não conta: o nome
|
|
831
|
-
* não prova feature (ver pr-files.js).
|
|
832
|
-
*
|
|
833
|
-
* Offline-safe: índice `null` devolve `null`, NUNCA lista vazia — lista vazia
|
|
834
|
-
* afirmaria "perguntei e não achei nada", que é diferente de "não pude
|
|
835
|
-
* perguntar". Achado #3: um `[]` disfarçado de "sem problema" é o que fazia
|
|
836
|
-
* `doctor` imprimir `✓ pull requests` mesmo sem conseguir consultar o `gh`.
|
|
837
|
-
*
|
|
838
|
-
* @returns {Array<{feature:string,type:string,detail:string}>|null}
|
|
839
|
-
* null = DESCONHECIDO (índice não pôde ser construído); nunca "sem PRs".
|
|
840
|
-
*/
|
|
675
|
+
/** `null` = DESCONHECIDO (o índice não pôde ser construído), nunca "sem PRs". */
|
|
841
676
|
function auditPullRequests(root) {
|
|
842
677
|
const index = buildPrIndex(root);
|
|
843
678
|
if (!index) return null;
|
|
@@ -872,26 +707,8 @@ function auditPullRequests(root) {
|
|
|
872
707
|
}
|
|
873
708
|
|
|
874
709
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* **Superfície própria, no molde do `--mcp`, e é decisão, não acidente.** O
|
|
878
|
-
* `--ai` NÃO entra no `--full` nem no `doctor` default:
|
|
879
|
-
*
|
|
880
|
-
* - o renderer do `--full` é `ok → ✓`, `warn → ⚠`, **`else → ✗`** — não tem o
|
|
881
|
-
* ramo `skip → ○`. Uma checagem de IA lá seria VERMELHA neste repositório
|
|
882
|
-
* mesmo dizendo "não verificado", e `test/commands/doctor.test.js` exige
|
|
883
|
-
* `doesNotMatch(out, /✗/)` para `doctor --full` aqui, com a justificativa
|
|
884
|
-
* escrita no próprio teste: *"every check must be satisfiable here — a
|
|
885
|
-
* permanently red one teaches nothing"*;
|
|
886
|
-
* - o `doctor` default é o comando mais rodado do dia, e cinco linhas `○`
|
|
887
|
-
* permanentes em todo projeto sem .NET é ruído com custo e sem informação.
|
|
888
|
-
*
|
|
889
|
-
* Exit code: 1 se houver ao menos um `error`; 0 caso contrário. `warn` e `skip`
|
|
890
|
-
* nunca mudam o código de saída.
|
|
891
|
-
*
|
|
892
|
-
* @param {string} targetPath
|
|
893
|
-
* @param {object} [options] - repassado a `collectAiChecks` (`config`, `deps`)
|
|
894
|
-
* @returns {Promise<Array>} as checagens, para o canário assere-las sem parsear texto
|
|
710
|
+
* Superfície própria, fora do `--full` (cujo renderer mostra `skip` como ✗) e do default.
|
|
711
|
+
* Exit 1 só com algum `error`; `warn` e `skip` nunca mudam o código de saída.
|
|
895
712
|
*/
|
|
896
713
|
export async function doctorAiCommand(targetPath, options = {}) {
|
|
897
714
|
const { collectAiChecks, aiExitCode, allSkipped } = await import('../../lib/ai/ai-checks.js');
|
|
@@ -904,9 +721,7 @@ export async function doctorAiCommand(targetPath, options = {}) {
|
|
|
904
721
|
const checks = collectAiChecks(targetPath, { config, deps: options.deps ?? {} });
|
|
905
722
|
|
|
906
723
|
if (allSkipped(checks)) {
|
|
907
|
-
//
|
|
908
|
-
// cinco `○` vazios — listar cinco vezes "não verificado" é o ruído que
|
|
909
|
-
// esta superfície existe para não produzir.
|
|
724
|
+
// Nenhuma checagem teve insumo: uma linha, não cinco `○` vazios.
|
|
910
725
|
const reasons = [...new Set(checks.map((c) => c.reason))];
|
|
911
726
|
for (const reason of reasons) {
|
|
912
727
|
console.log(chalk.gray(` ○ Nenhuma das ${checks.length} checagens teve insumo — ${reason}.`));
|
|
@@ -952,10 +767,8 @@ export async function doctorAiCommand(targetPath, options = {}) {
|
|
|
952
767
|
}
|
|
953
768
|
|
|
954
769
|
export async function doctorCommand(options = {}) {
|
|
955
|
-
// `--json
|
|
956
|
-
//
|
|
957
|
-
// where it asked for JSON — a caller has no way to notice a flag that was
|
|
958
|
-
// dropped in silence, so refuse instead of pretending.
|
|
770
|
+
// `--json`/`--prune` only mean something under `--worktrees` or `--procs`: refuse rather than
|
|
771
|
+
// drop the flag in silence and hand a script a banner where it asked for JSON.
|
|
959
772
|
const scoped = ['json', 'prune'].filter((f) => options[f]);
|
|
960
773
|
if (scoped.length && !options.worktrees && !options.procs) {
|
|
961
774
|
const flags = scoped.map((f) => `--${f}`).join(' e ');
|
|
@@ -978,10 +791,7 @@ export async function doctorCommand(options = {}) {
|
|
|
978
791
|
}
|
|
979
792
|
}
|
|
980
793
|
|
|
981
|
-
// `--ai`
|
|
982
|
-
// dois relatórios com dois códigos de saída no mesmo processo — o segundo
|
|
983
|
-
// apagaria o primeiro em silêncio. Mesmo molde da guarda acima: recusa em
|
|
984
|
-
// vez de fingir.
|
|
794
|
+
// `--ai` com outra superfície daria dois códigos de saída no mesmo processo: recusa.
|
|
985
795
|
if (options.ai) {
|
|
986
796
|
const conflicts = ['full', 'mcp', 'worktrees', 'procs'].filter((f) => options[f]);
|
|
987
797
|
if (conflicts.length) {
|
|
@@ -994,7 +804,6 @@ export async function doctorCommand(options = {}) {
|
|
|
994
804
|
return doctorAiCommand(process.cwd(), options);
|
|
995
805
|
}
|
|
996
806
|
|
|
997
|
-
// Reset mode: remove morph-managed settings entries
|
|
998
807
|
if (options.reset) {
|
|
999
808
|
const targetPath = process.cwd();
|
|
1000
809
|
logger.header('MORPH-SPEC Settings Reset');
|
|
@@ -1019,27 +828,18 @@ export async function doctorCommand(options = {}) {
|
|
|
1019
828
|
return;
|
|
1020
829
|
}
|
|
1021
830
|
|
|
1022
|
-
// full health check mode
|
|
1023
831
|
if (options.full) {
|
|
1024
832
|
const frameworkRoot = process.cwd();
|
|
1025
833
|
return doctorFullCommand(frameworkRoot);
|
|
1026
834
|
}
|
|
1027
835
|
|
|
1028
|
-
// MCP health check mode
|
|
1029
836
|
if (options.mcp) {
|
|
1030
837
|
return doctorMcpCommand(process.cwd());
|
|
1031
838
|
}
|
|
1032
839
|
|
|
1033
|
-
// Worktree reconciliation mode (C2)
|
|
1034
840
|
if (options.worktrees) {
|
|
1035
841
|
const clean = await doctorWorktreesCommand(process.cwd(), options);
|
|
1036
|
-
// The audit result
|
|
1037
|
-
// Commander discards an action's return value, so this reported success to
|
|
1038
|
-
// every script while printing "⚠ 5 inconsistência(s)" to the human — a
|
|
1039
|
-
// `doctor --worktrees || echo falhou` in CI or a pre-commit hook saw a
|
|
1040
|
-
// clean run with orphaned worktrees piling up. The flag-misuse branch
|
|
1041
|
-
// above already treats the exit code as this command's signal channel;
|
|
1042
|
-
// only the audit itself was left out of it.
|
|
842
|
+
// The audit result must reach the exit code: Commander discards an action's return value.
|
|
1043
843
|
if (!clean) process.exitCode = 1;
|
|
1044
844
|
return clean;
|
|
1045
845
|
}
|
|
@@ -1067,11 +867,9 @@ export async function doctorCommand(options = {}) {
|
|
|
1067
867
|
}
|
|
1068
868
|
};
|
|
1069
869
|
|
|
1070
|
-
// Check versions first
|
|
1071
870
|
const cliCheck = await checkCLIOutdated();
|
|
1072
871
|
const projectCheck = await checkProjectOutdated(targetPath);
|
|
1073
872
|
|
|
1074
|
-
// CLI Version
|
|
1075
873
|
if (cliCheck.latest) {
|
|
1076
874
|
if (cliCheck.isOutdated) {
|
|
1077
875
|
checks.push({
|
|
@@ -1094,7 +892,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1094
892
|
});
|
|
1095
893
|
}
|
|
1096
894
|
|
|
1097
|
-
// Project MORPH version
|
|
1098
895
|
if (projectCheck.current) {
|
|
1099
896
|
if (projectCheck.isOutdated) {
|
|
1100
897
|
checks.push({
|
|
@@ -1118,7 +915,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1118
915
|
hasWarnings = true;
|
|
1119
916
|
}
|
|
1120
917
|
|
|
1121
|
-
// Check if morph-spec is in PATH
|
|
1122
918
|
const inPath = isMorphSpecInPath();
|
|
1123
919
|
if (inPath) {
|
|
1124
920
|
checks.push({
|
|
@@ -1134,7 +930,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1134
930
|
hasWarnings = true;
|
|
1135
931
|
}
|
|
1136
932
|
|
|
1137
|
-
// Check CLAUDE.md has morph-spec @import
|
|
1138
933
|
const claudeMd = join(targetPath, 'CLAUDE.md');
|
|
1139
934
|
const claudeMdStatus = await checkClaudeMdImport(claudeMd);
|
|
1140
935
|
if (claudeMdStatus === 'ok') {
|
|
@@ -1147,7 +942,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1147
942
|
hasErrors = true;
|
|
1148
943
|
}
|
|
1149
944
|
|
|
1150
|
-
// Check .morph folder
|
|
1151
945
|
const morphPath = join(targetPath, '.morph');
|
|
1152
946
|
if (await pathExists(morphPath)) {
|
|
1153
947
|
checks.push({ name: '.morph/', status: 'ok' });
|
|
@@ -1156,7 +950,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1156
950
|
hasErrors = true;
|
|
1157
951
|
}
|
|
1158
952
|
|
|
1159
|
-
// Check config.json
|
|
1160
953
|
const configPath = join(morphPath, 'config', 'config.json');
|
|
1161
954
|
if (await pathExists(configPath)) {
|
|
1162
955
|
try {
|
|
@@ -1175,7 +968,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1175
968
|
hasErrors = true;
|
|
1176
969
|
}
|
|
1177
970
|
|
|
1178
|
-
// Check agents.json
|
|
1179
971
|
const agentsPath = join(morphPath, 'framework', 'agents.json');
|
|
1180
972
|
if (await pathExists(agentsPath)) {
|
|
1181
973
|
try {
|
|
@@ -1194,11 +986,9 @@ export async function doctorCommand(options = {}) {
|
|
|
1194
986
|
hasErrors = true;
|
|
1195
987
|
}
|
|
1196
988
|
|
|
1197
|
-
// Check standards (either in project .morph/framework/standards/ or in framework package)
|
|
1198
989
|
const standardsPath = join(morphPath, 'framework', 'standards');
|
|
1199
990
|
const frameworkStdsRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'standards');
|
|
1200
991
|
if (await pathExists(standardsPath)) {
|
|
1201
|
-
// Check for VSA standard (core to the framework)
|
|
1202
992
|
const vsaStd = await pathExists(join(standardsPath, 'architecture', 'vertical-slice', 'vertical-slice.md')) ||
|
|
1203
993
|
await pathExists(join(standardsPath, 'vertical-slice.md'));
|
|
1204
994
|
if (vsaStd) {
|
|
@@ -1208,7 +998,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1208
998
|
hasWarnings = true;
|
|
1209
999
|
}
|
|
1210
1000
|
} else if (await pathExists(frameworkStdsRoot)) {
|
|
1211
|
-
// Framework package has standards (dev/global install mode)
|
|
1212
1001
|
checks.push({ name: 'standards/', status: 'warn', msg: 'not installed in project (run morph-spec init)' });
|
|
1213
1002
|
hasWarnings = true;
|
|
1214
1003
|
} else {
|
|
@@ -1216,15 +1005,8 @@ export async function doctorCommand(options = {}) {
|
|
|
1216
1005
|
hasErrors = true;
|
|
1217
1006
|
}
|
|
1218
1007
|
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
// layered (template-registry.js / architecture-scanner.js resolveTemplatePath)
|
|
1222
|
-
// and falls back to the npm package's own framework/templates/ when the
|
|
1223
|
-
// project-local copy is absent, which is now the expected default rather
|
|
1224
|
-
// than an error. The project-local copy remains a supported override — if
|
|
1225
|
-
// present, report it as such. The "key templates" check further below
|
|
1226
|
-
// verifies the package copy itself is intact, which is the check that
|
|
1227
|
-
// actually matters now.
|
|
1008
|
+
// A missing project-local templates copy is the default (resolution falls back to the package's
|
|
1009
|
+
// framework/templates/); a local copy is a supported override.
|
|
1228
1010
|
const templatesPath = join(morphPath, 'framework', 'templates');
|
|
1229
1011
|
if (await pathExists(templatesPath)) {
|
|
1230
1012
|
checks.push({ name: 'templates/', status: 'ok', msg: 'local override present' });
|
|
@@ -1232,7 +1014,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1232
1014
|
checks.push({ name: 'templates/', status: 'ok', msg: 'resolved from package (no local override)' });
|
|
1233
1015
|
}
|
|
1234
1016
|
|
|
1235
|
-
// Check .claude folder
|
|
1236
1017
|
const claudePath = join(targetPath, '.claude');
|
|
1237
1018
|
if (await pathExists(claudePath)) {
|
|
1238
1019
|
const commandsPath = join(claudePath, 'commands');
|
|
@@ -1244,7 +1025,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1244
1025
|
if (hasCommands && hasSkills) {
|
|
1245
1026
|
checks.push({ name: '.claude/', status: 'ok' });
|
|
1246
1027
|
|
|
1247
|
-
// Check skills link status
|
|
1248
1028
|
const skillEntries = await fs.readdir(skillsPath, { withFileTypes: true });
|
|
1249
1029
|
const skillDirs = skillEntries.filter(e => e.isDirectory() || e.isSymbolicLink());
|
|
1250
1030
|
let linkedCount = 0;
|
|
@@ -1253,7 +1033,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1253
1033
|
try {
|
|
1254
1034
|
const stat = await fs.lstat(join(skillsPath, dir.name));
|
|
1255
1035
|
if (stat.isSymbolicLink()) linkedCount++;
|
|
1256
|
-
} catch {
|
|
1036
|
+
} catch {}
|
|
1257
1037
|
}
|
|
1258
1038
|
|
|
1259
1039
|
if (skillDirs.length > 0) {
|
|
@@ -1268,7 +1048,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1268
1048
|
}
|
|
1269
1049
|
}
|
|
1270
1050
|
|
|
1271
|
-
// Check .claude/rules/
|
|
1272
1051
|
const rulesPath = join(claudePath, 'rules');
|
|
1273
1052
|
if (await pathExists(rulesPath)) {
|
|
1274
1053
|
const ruleFiles = await fs.readdir(rulesPath);
|
|
@@ -1279,23 +1058,18 @@ export async function doctorCommand(options = {}) {
|
|
|
1279
1058
|
hasWarnings = true;
|
|
1280
1059
|
}
|
|
1281
1060
|
|
|
1282
|
-
// Check .claude/agents/
|
|
1283
1061
|
const agentsPathClaude = join(claudePath, 'agents');
|
|
1284
1062
|
if (await pathExists(agentsPathClaude)) {
|
|
1285
1063
|
const agentFiles = await fs.readdir(agentsPathClaude);
|
|
1286
1064
|
const morphAgents = agentFiles.filter(f => f.startsWith('morph-') && f.endsWith('.md'));
|
|
1287
1065
|
checks.push({ name: '.claude/agents/', status: 'ok', msg: `${morphAgents.length} agents` });
|
|
1288
1066
|
|
|
1289
|
-
//
|
|
1290
|
-
// yet) is NOT a skip: personas install-all by default, so with an
|
|
1291
|
-
// unknown stack ALL personas are expected (agentEligible semantics) —
|
|
1292
|
-
// the exact field failure where only morph-evaluator landed and every
|
|
1293
|
-
// domain dispatch fell back to general-purpose.
|
|
1067
|
+
// Empty tags (no config yet) is NOT a skip: personas install-all by default, so ALL are expected.
|
|
1294
1068
|
try {
|
|
1295
1069
|
let projectTags = [];
|
|
1296
1070
|
try {
|
|
1297
1071
|
projectTags = getProjectTags(await readJson(configPath));
|
|
1298
|
-
} catch {
|
|
1072
|
+
} catch {}
|
|
1299
1073
|
const tagsLabel = projectTags.length > 0 ? projectTags.join(', ') : 'all (stack unknown)';
|
|
1300
1074
|
const frameworkDir = join(import.meta.dirname, '..', '..', '..', 'framework');
|
|
1301
1075
|
const { missing } = checkMissingAgents(targetPath, frameworkDir, projectTags);
|
|
@@ -1310,12 +1084,11 @@ export async function doctorCommand(options = {}) {
|
|
|
1310
1084
|
} else {
|
|
1311
1085
|
checks.push({ name: 'agents completeness', status: 'ok', msg: `all agents for [${tagsLabel}]` });
|
|
1312
1086
|
}
|
|
1313
|
-
} catch {
|
|
1087
|
+
} catch {}
|
|
1314
1088
|
} else {
|
|
1315
1089
|
checks.push({ name: '.claude/agents/', status: 'warn', msg: 'missing — run morph-spec update' });
|
|
1316
1090
|
hasWarnings = true;
|
|
1317
1091
|
}
|
|
1318
|
-
// ── Hook health: verify hook scripts exist ───────────────────────────────
|
|
1319
1092
|
const settingsLocalPath = join(claudePath, 'settings.local.json');
|
|
1320
1093
|
if (await pathExists(settingsLocalPath)) {
|
|
1321
1094
|
try {
|
|
@@ -1351,7 +1124,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1351
1124
|
hasWarnings = true;
|
|
1352
1125
|
}
|
|
1353
1126
|
|
|
1354
|
-
// ── Skills cliVersion: warn if installed skills reference older CLI ──────
|
|
1355
1127
|
try {
|
|
1356
1128
|
const cliVersion = getInstalledCLIVersion();
|
|
1357
1129
|
if (cliVersion) {
|
|
@@ -1373,24 +1145,19 @@ export async function doctorCommand(options = {}) {
|
|
|
1373
1145
|
hasWarnings = true;
|
|
1374
1146
|
}
|
|
1375
1147
|
}
|
|
1376
|
-
} catch {
|
|
1148
|
+
} catch {}
|
|
1377
1149
|
|
|
1378
|
-
// ── Claude Code Plugins ──────────────────────────────────────────────────
|
|
1379
1150
|
console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
|
|
1380
1151
|
const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
1381
1152
|
let pluginData = { plugins: {} };
|
|
1382
1153
|
try {
|
|
1383
1154
|
pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
|
|
1384
|
-
} catch {
|
|
1155
|
+
} catch {}
|
|
1385
1156
|
const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
|
|
1386
1157
|
check('context7 plugin', pluginStatus.context7, false,
|
|
1387
1158
|
'Run: morph-spec install-plugin context7');
|
|
1388
1159
|
} else {
|
|
1389
|
-
// `.claude/`
|
|
1390
|
-
// partial install to the full-wipe incident this command exists to catch
|
|
1391
|
-
// (folder survives, 0 items — doctor used to shrug this off as a vague
|
|
1392
|
-
// warning with no fix). Count what IS still there: near-total loss is an
|
|
1393
|
-
// ERROR with the actionable command, not a warning that reads as optional.
|
|
1160
|
+
// `.claude/` without commands/ or skills/: count what is left; near-total loss is an ERROR with the fix.
|
|
1394
1161
|
const pieces = ['agents', 'rules', 'skills', 'commands'];
|
|
1395
1162
|
const present = [];
|
|
1396
1163
|
for (const piece of pieces) {
|
|
@@ -1420,7 +1187,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1420
1187
|
hasErrors = true;
|
|
1421
1188
|
}
|
|
1422
1189
|
|
|
1423
|
-
// Check state.json
|
|
1424
1190
|
const statePath = join(targetPath, '.morph', 'state.json');
|
|
1425
1191
|
if (await pathExists(statePath)) {
|
|
1426
1192
|
try {
|
|
@@ -1441,10 +1207,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1441
1207
|
hasWarnings = true;
|
|
1442
1208
|
}
|
|
1443
1209
|
|
|
1444
|
-
//
|
|
1445
|
-
// gate-order violations (N approved with N-1 pending), identity-less
|
|
1446
|
-
// features (no description), orphan folders and stale index entries. Warn
|
|
1447
|
-
// only (advisory); the per-feature detail is printed after the summary.
|
|
1210
|
+
// Warn only (advisory); the per-feature detail is printed after the summary.
|
|
1448
1211
|
const lifecycleIssues = auditFeatureLifecycle(targetPath);
|
|
1449
1212
|
if (lifecycleIssues.length === 0) {
|
|
1450
1213
|
checks.push({ name: 'feature lifecycle', status: 'ok' });
|
|
@@ -1457,13 +1220,8 @@ export async function doctorCommand(options = {}) {
|
|
|
1457
1220
|
hasWarnings = true;
|
|
1458
1221
|
}
|
|
1459
1222
|
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
// sem origin no GitHub, ou qualquer falha, `auditPullRequests` nunca lança
|
|
1463
|
-
// (achado #7 — mesma guarda fail-open que `fleet-scanner.js` já usa para a
|
|
1464
|
-
// síntese idêntica) e devolve `null` — achado #3: um índice desconhecido
|
|
1465
|
-
// renderiza como "não verificado", nunca como `✓ ok`, porque `[]` e
|
|
1466
|
-
// "não pude perguntar" não são a mesma coisa.
|
|
1223
|
+
// Offline-safe: `auditPullRequests` never throws and returns `null` when it cannot ask, which
|
|
1224
|
+
// renders as "não verificado", never `✓`.
|
|
1467
1225
|
let prIssues;
|
|
1468
1226
|
try {
|
|
1469
1227
|
prIssues = auditPullRequests(targetPath);
|
|
@@ -1483,8 +1241,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1483
1241
|
hasWarnings = true;
|
|
1484
1242
|
}
|
|
1485
1243
|
|
|
1486
|
-
//
|
|
1487
|
-
// usa o grafo tem de continuar com o doctor verde.
|
|
1244
|
+
// Opcional: nunca 'missing', um projeto que não usa o grafo segue com o doctor verde.
|
|
1488
1245
|
const graphAudit = auditGraph(targetPath);
|
|
1489
1246
|
if (graphAudit.status === 'warn') {
|
|
1490
1247
|
checks.push({ name: 'codebase graph', status: 'warn', msg: graphAudit.msg });
|
|
@@ -1504,15 +1261,12 @@ export async function doctorCommand(options = {}) {
|
|
|
1504
1261
|
checks.push(storeCheck);
|
|
1505
1262
|
if (storeCheck.status === 'warn') hasWarnings = true;
|
|
1506
1263
|
}
|
|
1507
|
-
} catch {
|
|
1264
|
+
} catch {}
|
|
1508
1265
|
|
|
1509
|
-
// Check key templates exist (in framework package, not project copy)
|
|
1510
1266
|
const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
|
|
1511
1267
|
const keyTemplates = [
|
|
1512
|
-
{ name: '
|
|
1513
|
-
{ name: '
|
|
1514
|
-
{ name: 'tasks.template.json', path: join(frameworkTemplatesRoot, 'tasks.template.json') },
|
|
1515
|
-
{ name: 'decisions.md', path: join(frameworkTemplatesRoot, 'feature', 'decisions.md') }
|
|
1268
|
+
{ name: 'plan.md', path: join(frameworkTemplatesRoot, 'docs', 'plan.md') },
|
|
1269
|
+
{ name: 'ui-mockups.md', path: join(frameworkTemplatesRoot, 'docs', 'ui-mockups.md') }
|
|
1516
1270
|
];
|
|
1517
1271
|
const missingTemplates = [];
|
|
1518
1272
|
for (const template of keyTemplates) {
|
|
@@ -1531,7 +1285,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1531
1285
|
hasErrors = true;
|
|
1532
1286
|
}
|
|
1533
1287
|
|
|
1534
|
-
// Check Node.js version
|
|
1535
1288
|
const nodeVersion = process.version;
|
|
1536
1289
|
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
1537
1290
|
if (majorVersion >= 18) {
|
|
@@ -1541,7 +1294,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1541
1294
|
hasWarnings = true;
|
|
1542
1295
|
}
|
|
1543
1296
|
|
|
1544
|
-
// Display results
|
|
1545
1297
|
for (const check of checks) {
|
|
1546
1298
|
if (check.status === 'ok') {
|
|
1547
1299
|
console.log(chalk.green(` ✓ ${check.name}`));
|
|
@@ -1552,15 +1304,11 @@ export async function doctorCommand(options = {}) {
|
|
|
1552
1304
|
} else if (check.status === 'error') {
|
|
1553
1305
|
console.log(chalk.red(` ✗ ${check.name}`) + chalk.gray(` (${check.msg})`));
|
|
1554
1306
|
} else if (check.status === 'skip') {
|
|
1555
|
-
// "Não verificado"
|
|
1556
|
-
// um `gh` indisponível não pode deixar o doctor mais barulhento nem mudar
|
|
1557
|
-
// o exit code (achado #3).
|
|
1307
|
+
// "Não verificado" nunca soma em hasWarnings/hasErrors: um `gh` indisponível não muda o exit code.
|
|
1558
1308
|
console.log(chalk.gray(` ○ ${check.name}`) + chalk.gray(check.msg ? ` (${check.msg})` : ''));
|
|
1559
1309
|
}
|
|
1560
1310
|
}
|
|
1561
1311
|
|
|
1562
|
-
// Per-feature detail for the lifecycle inconsistencies (item: doctor lists
|
|
1563
|
-
// what's already wrong in the repo, e.g. plan approved with proposal pending).
|
|
1564
1312
|
if (lifecycleIssues.length) {
|
|
1565
1313
|
logger.blank();
|
|
1566
1314
|
console.log(chalk.yellow(' Feature lifecycle inconsistencies:'));
|
|
@@ -1570,8 +1318,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1570
1318
|
console.log(chalk.gray(" Fix: 'morph-spec approve' (order), '--description' on create, or 'morph-spec delete' for dead scaffolds."));
|
|
1571
1319
|
}
|
|
1572
1320
|
|
|
1573
|
-
// Per-feature detail for the PR inconsistencies (item: pendurado ou merge
|
|
1574
|
-
// que não pousou).
|
|
1575
1321
|
if (prIssues?.length) {
|
|
1576
1322
|
logger.blank();
|
|
1577
1323
|
console.log(chalk.yellow(' Pull requests pendentes:'));
|