@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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.sh +11 -11
- 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 +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/rules/nextjs-standards.md +1 -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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/testing.md +10 -4
- 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/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- 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 +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- 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 +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- 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,8 +190,6 @@ const FRAMEWORK_STANDARDS = [
|
|
|
242
190
|
'framework/standards/integration/mcp/mcp-tools.md'
|
|
243
191
|
];
|
|
244
192
|
|
|
245
|
-
// required personas expected in agents.json — the universal, stack-agnostic
|
|
246
|
-
// ones: the three review roles and scout (stage axis)
|
|
247
193
|
const REQUIRED_AGENTS = [
|
|
248
194
|
'verifier',
|
|
249
195
|
'reviewer',
|
|
@@ -251,9 +197,6 @@ const REQUIRED_AGENTS = [
|
|
|
251
197
|
'scout'
|
|
252
198
|
];
|
|
253
199
|
|
|
254
|
-
/**
|
|
255
|
-
* Run full health checks
|
|
256
|
-
*/
|
|
257
200
|
async function doctorFullCommand(frameworkRoot) {
|
|
258
201
|
console.log(chalk.bold('\n🔬 MORPH-SPEC Full Health Check\n'));
|
|
259
202
|
console.log('─'.repeat(60));
|
|
@@ -272,15 +215,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
272
215
|
}
|
|
273
216
|
};
|
|
274
217
|
|
|
275
|
-
// Two layouts reach this command
|
|
276
|
-
//
|
|
277
|
-
// framework repo src/, framework/, no .morph/ at all
|
|
278
|
-
// installed project .morph/framework/, and the CLI in node_modules
|
|
279
|
-
//
|
|
280
|
-
// Half the checks assumed one layout and half the other, so `--full` could
|
|
281
|
-
// not pass anywhere: red in the repo (no .morph/) and red in a project (no
|
|
282
|
-
// src/). A check that can never pass reports exactly as much as one that can
|
|
283
|
-
// 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.
|
|
284
220
|
const installedAgentsPath = join(frameworkRoot, '.morph/framework/agents.json');
|
|
285
221
|
const canonicalAgentsPath = join(frameworkRoot, 'framework/agents.json');
|
|
286
222
|
const inFrameworkRepo = !(await pathExists(installedAgentsPath))
|
|
@@ -289,9 +225,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
289
225
|
|
|
290
226
|
console.log(chalk.gray(`\n Contexto: ${inFrameworkRepo ? 'repo do framework' : 'projeto instalado'}`));
|
|
291
227
|
|
|
292
|
-
//
|
|
293
|
-
// An installed project has no src/ — the CLI lives in node_modules, and its
|
|
294
|
-
// 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.
|
|
295
229
|
if (inFrameworkRepo) {
|
|
296
230
|
console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
|
|
297
231
|
const missingLibs = [];
|
|
@@ -312,9 +246,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
312
246
|
missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
|
|
313
247
|
}
|
|
314
248
|
|
|
315
|
-
// ── 3. Framework Standards ───────────────────────────────────────────────
|
|
316
|
-
// Same files, different root: the repo holds them at framework/standards/,
|
|
317
|
-
// an installed project at .morph/framework/standards/.
|
|
318
249
|
const stdsRoot = inFrameworkRepo ? frameworkRoot : join(frameworkRoot, '.morph');
|
|
319
250
|
console.log(chalk.cyan(`\n ${inFrameworkRepo ? '' : '.morph/'}framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
|
|
320
251
|
const missingStds = [];
|
|
@@ -325,7 +256,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
325
256
|
missingStds.length === 0, false,
|
|
326
257
|
missingStds.length > 0 ? `missing: ${missingStds.join(', ')}` : '');
|
|
327
258
|
|
|
328
|
-
// ── 5. Required Agents in agents.json ───────────────────────────────────
|
|
329
259
|
console.log(chalk.cyan(`\n ${inFrameworkRepo ? 'framework' : '.morph/framework'}/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
|
|
330
260
|
if (await pathExists(agentsPath)) {
|
|
331
261
|
try {
|
|
@@ -342,10 +272,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
342
272
|
check('agents.json', false, false, 'file not found');
|
|
343
273
|
}
|
|
344
274
|
|
|
345
|
-
//
|
|
346
|
-
// Only meaningful for an installed project: it compares what was installed
|
|
347
|
-
// against the canonical source. In the framework repo the two are the same
|
|
348
|
-
// 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.
|
|
349
276
|
if (!inFrameworkRepo && await pathExists(agentsPath) && await pathExists(canonicalAgentsPath)) {
|
|
350
277
|
try {
|
|
351
278
|
const installed = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
|
|
@@ -355,15 +282,10 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
355
282
|
const isFresh = installedVer === canonicalVer;
|
|
356
283
|
check(`agents.json version (${installedVer})`, isFresh, true,
|
|
357
284
|
isFresh ? '' : `stale — canonical is v${canonicalVer}, run 'morph-spec update' to sync`);
|
|
358
|
-
} catch {
|
|
285
|
+
} catch {}
|
|
359
286
|
}
|
|
360
287
|
|
|
361
|
-
//
|
|
362
|
-
// agents.json is the single source of truth; the .claude/agents/morph-*.md
|
|
363
|
-
// files are derived artifacts (ADR-001). Missing = broken dispatch (error);
|
|
364
|
-
// stale/hand-edited or orphan = drift (warning).
|
|
365
|
-
// Skipped in the framework repo: the generated `.claude/agents/morph-*.md`
|
|
366
|
-
// 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.
|
|
367
289
|
if (!inFrameworkRepo && await pathExists(agentsPath)) {
|
|
368
290
|
console.log(chalk.cyan('\n .claude/agents/ — Generated personas (drift vs agents.json)'));
|
|
369
291
|
try {
|
|
@@ -373,7 +295,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
373
295
|
try {
|
|
374
296
|
const rawCfg = (await fs.readFile(cfgPath, 'utf8')).replace(/^/, '');
|
|
375
297
|
projectTags = getProjectTags(JSON.parse(rawCfg));
|
|
376
|
-
} catch {
|
|
298
|
+
} catch {}
|
|
377
299
|
}
|
|
378
300
|
// The MCP registry comes from the package, like the installer's: the
|
|
379
301
|
// installed .morph/framework/ has no mcp/, and rendering without it
|
|
@@ -399,7 +321,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
399
321
|
}
|
|
400
322
|
}
|
|
401
323
|
|
|
402
|
-
// ── 6c. GitHub tooling — gh CLI (canonical; morph-spec does not use a GitHub MCP) ──
|
|
403
324
|
console.log(chalk.cyan('\n Tooling — GitHub via gh CLI'));
|
|
404
325
|
try {
|
|
405
326
|
execSync('gh --version', { stdio: 'pipe', timeout: 5000 });
|
|
@@ -409,7 +330,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
409
330
|
"GitHub ops (issues/PRs/releases) usam o gh CLI — instale https://cli.github.com e rode 'gh auth login'");
|
|
410
331
|
}
|
|
411
332
|
|
|
412
|
-
// ── 7. state.json schema version ────────────────────────────────────────
|
|
413
333
|
console.log(chalk.cyan('\n .morph/state.json — Schema Version'));
|
|
414
334
|
const statePath = join(frameworkRoot, '.morph/state.json');
|
|
415
335
|
if (await pathExists(statePath)) {
|
|
@@ -425,23 +345,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
425
345
|
check('state.json', false, true, 'not found (run: morph-spec init)');
|
|
426
346
|
}
|
|
427
347
|
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
// As outras duas camadas são o `config.schema.json` (declarativa) e
|
|
431
|
-
// `validateTestNodes()` chamada pelo `verify` (fail-closed, em uso). Esta
|
|
432
|
-
// aqui existe para o defeito chegar ANTES do gate: um `id` com typo em
|
|
433
|
-
// `tests[]` só se manifesta quando alguém roda `morph-spec verify` e lê um
|
|
434
|
-
// nó vermelho — o doctor o mostra na checagem de saúde, que é onde se olha
|
|
435
|
-
// primeiro. Reusa `validateTestNodes` (a MESMA autoridade que o verify
|
|
436
|
-
// consulta): uma segunda leitura do mesmo campo aqui seria uma segunda
|
|
437
|
-
// opinião sobre o que é um nó válido, e as duas divergiriam.
|
|
438
|
-
//
|
|
439
|
-
// PULA de verdade — sem check e sem cabeçalho — quando não há `config.json`
|
|
440
|
-
// ou quando `project.tests` não foi declarado. Um projeto sem declaração usa
|
|
441
|
-
// o fallback de quatro passos e não tem contrato a violar; este repositório
|
|
442
|
-
// é exatamente esse caso, e um check que não pode passar aqui é ruído, não
|
|
443
|
-
// achado (`test/commands/doctor.test.js`: `doctor --full` sem nenhum `✗`).
|
|
444
|
-
// 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.
|
|
445
350
|
const testsConfigPath = morphConfigPath(frameworkRoot);
|
|
446
351
|
if (await pathExists(testsConfigPath)) {
|
|
447
352
|
let testsConfig = null;
|
|
@@ -460,12 +365,8 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
460
365
|
? broken.map((n) => n.reason ?? `invalid-test-node: ${n.id}`).join('; ')
|
|
461
366
|
: '');
|
|
462
367
|
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// WARN-only, never ✗: the fail-closed barrier is `verify`, this just informs.
|
|
466
|
-
// Compared by RESOLVED path, not raw string: "./src/web", "src/web/" and
|
|
467
|
-
// (win32) "src\\web" all address the same node as "src/web" and must not
|
|
468
|
-
// 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.
|
|
469
370
|
const declaredCwds = new Set(declared.map((n) => normCwd(frameworkRoot, n.cwd)));
|
|
470
371
|
const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
|
|
471
372
|
.filter((pkg) => !declaredCwds.has(normCwd(frameworkRoot, pkg.cwd)));
|
|
@@ -476,10 +377,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
476
377
|
);
|
|
477
378
|
}
|
|
478
379
|
} else if (typeof testsConfig?.project?.testCommand === 'string' && testsConfig.project.testCommand.trim()) {
|
|
479
|
-
//
|
|
480
|
-
// string de shell), então cobertura aqui é só "o comando MENCIONA o
|
|
481
|
-
// runner do package.json" — não confirmar isso seria ruído para quem já
|
|
482
|
-
// roda vitest por dentro do testCommand.
|
|
380
|
+
// testCommand é opaco: cobertura aqui é só "o comando MENCIONA o runner do package.json".
|
|
483
381
|
const testCommand = testsConfig.project.testCommand;
|
|
484
382
|
const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
|
|
485
383
|
.filter((pkg) => !testCommand.includes(pkg.runner));
|
|
@@ -493,7 +391,6 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
493
391
|
}
|
|
494
392
|
}
|
|
495
393
|
|
|
496
|
-
// ── Display ──────────────────────────────────────────────────────────────
|
|
497
394
|
console.log('\n' + '─'.repeat(60));
|
|
498
395
|
console.log(chalk.bold('\nResults:\n'));
|
|
499
396
|
for (const c of checks) {
|
|
@@ -520,19 +417,7 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
520
417
|
}
|
|
521
418
|
}
|
|
522
419
|
|
|
523
|
-
|
|
524
|
-
* MCP health check: reads .mcp.json, settings.json, and settings.local.json
|
|
525
|
-
* and validates each configured MCP server's command binary and env var requirements.
|
|
526
|
-
*
|
|
527
|
-
* Strategy:
|
|
528
|
-
* - "ok" : command binary exists (or is a known runtime like npx/node) AND
|
|
529
|
-
* all env vars referenced in args/env are set in process.env
|
|
530
|
-
* - "warn" : binary found but env vars missing (MCP won't auth)
|
|
531
|
-
* - "missing": command binary not found in PATH
|
|
532
|
-
*
|
|
533
|
-
* NOTE: This does NOT test actual connectivity — that requires spawning the MCP.
|
|
534
|
-
* It's a static configuration health check that catches 80% of "MCP not working" cases.
|
|
535
|
-
*/
|
|
420
|
+
// Static config check only: connectivity would require spawning the MCP.
|
|
536
421
|
async function doctorMcpCommand(targetPath) {
|
|
537
422
|
logger.header('MCP Server Health Check');
|
|
538
423
|
logger.blank();
|
|
@@ -540,7 +425,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
540
425
|
const allMcpServers = {};
|
|
541
426
|
const sourcesFound = [];
|
|
542
427
|
|
|
543
|
-
// Primary: .mcp.json (Claude Code reads MCPs from here)
|
|
544
428
|
const mcpJsonPath = join(targetPath, '.mcp.json');
|
|
545
429
|
if (await pathExists(mcpJsonPath)) {
|
|
546
430
|
try {
|
|
@@ -551,10 +435,9 @@ async function doctorMcpCommand(targetPath) {
|
|
|
551
435
|
Object.assign(allMcpServers, mcps);
|
|
552
436
|
sourcesFound.push(`.mcp.json (${count})`);
|
|
553
437
|
}
|
|
554
|
-
} catch {
|
|
438
|
+
} catch {}
|
|
555
439
|
}
|
|
556
440
|
|
|
557
|
-
// Legacy: settings files (for backward compat)
|
|
558
441
|
const settingsPaths = [
|
|
559
442
|
join(targetPath, '.claude', 'settings.json'),
|
|
560
443
|
join(targetPath, '.claude', 'settings.local.json'),
|
|
@@ -575,7 +458,7 @@ async function doctorMcpCommand(targetPath) {
|
|
|
575
458
|
if (addedCount > 0) {
|
|
576
459
|
sourcesFound.push(`${relative(targetPath, sp)} (${addedCount})`);
|
|
577
460
|
}
|
|
578
|
-
} catch {
|
|
461
|
+
} catch {}
|
|
579
462
|
}
|
|
580
463
|
}
|
|
581
464
|
|
|
@@ -598,14 +481,11 @@ async function doctorMcpCommand(targetPath) {
|
|
|
598
481
|
let status = 'ok';
|
|
599
482
|
let isRemote = false;
|
|
600
483
|
|
|
601
|
-
// ── 0. Remote MCP detection ────────────────────────────────────────────
|
|
602
|
-
// Remote MCPs use HTTP transport (type: "http" or url property) — no binary to check
|
|
603
484
|
if (config.type === 'http' || (config.url && !config.command)) {
|
|
604
485
|
isRemote = true;
|
|
605
486
|
// Remote MCPs are always "ok" from a config perspective — auth happens at runtime
|
|
606
487
|
}
|
|
607
488
|
|
|
608
|
-
// ── 1. Binary check (stdio MCPs only) ──────────────────────────────────
|
|
609
489
|
const cmd = config.command || '';
|
|
610
490
|
if (!isRemote && cmd && !KNOWN_RUNTIMES.has(cmd)) {
|
|
611
491
|
try {
|
|
@@ -616,7 +496,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
616
496
|
}
|
|
617
497
|
}
|
|
618
498
|
|
|
619
|
-
// ── 2. Env vars from config.env ────────────────────────────────────────
|
|
620
499
|
for (const [key, val] of Object.entries(config.env || {})) {
|
|
621
500
|
if (typeof val === 'string' && (val.startsWith('${') || (val.startsWith('$') && !val.includes(' ')))) {
|
|
622
501
|
const varName = val.replace(/^\$\{?/, '').replace(/\}$/, '');
|
|
@@ -627,7 +506,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
627
506
|
}
|
|
628
507
|
}
|
|
629
508
|
|
|
630
|
-
// ── 3. Env refs in args array ──────────────────────────────────────────
|
|
631
509
|
const argsStr = (config.args || []).join(' ');
|
|
632
510
|
const envRefsInArgs = [...argsStr.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map(m => m[1]);
|
|
633
511
|
for (const varName of envRefsInArgs) {
|
|
@@ -640,7 +518,6 @@ async function doctorMcpCommand(targetPath) {
|
|
|
640
518
|
checks.push({ name, status, issues, cmd, isRemote });
|
|
641
519
|
}
|
|
642
520
|
|
|
643
|
-
// ── Display ────────────────────────────────────────────────────────────────
|
|
644
521
|
for (const c of checks) {
|
|
645
522
|
const typeLabel = c.isRemote
|
|
646
523
|
? chalk.gray(` [remote: ${allMcpServers[c.name]?.url || 'http'}]`)
|
|
@@ -679,23 +556,8 @@ async function doctorMcpCommand(targetPath) {
|
|
|
679
556
|
}
|
|
680
557
|
|
|
681
558
|
/**
|
|
682
|
-
* `
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* Orphan worktrees used to accumulate invisibly: `fleet` only reports worktrees
|
|
686
|
-
* on a `morph/*` branch, so anything else never showed up anywhere.
|
|
687
|
-
*
|
|
688
|
-
* `--prune` runs the safe repairs: `git worktree prune` (admin records for
|
|
689
|
-
* directories that are already gone) and the removal of EMPTY unregistered
|
|
690
|
-
* residue directories, which by definition hold nothing that can be lost.
|
|
691
|
-
* Deleting a directory that still HOLDS something is reported with the exact
|
|
692
|
-
* command, never performed — same PROVE-or-DECLINE discipline as the finish
|
|
693
|
-
* teardown.
|
|
694
|
-
*
|
|
695
|
-
* `--prune` also applies the retention of the finish backups — the same policy
|
|
696
|
-
* every `finish` applies on its own, and the ONLY path that also applies it to
|
|
697
|
-
* the backups written before the retention existed (no manifest), counting
|
|
698
|
-
* 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.
|
|
699
561
|
*/
|
|
700
562
|
export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
701
563
|
const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
|
|
@@ -760,13 +622,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
760
622
|
}
|
|
761
623
|
|
|
762
624
|
const issues = auditWorktrees(targetPath);
|
|
763
|
-
// `
|
|
764
|
-
//
|
|
765
|
-
// and the docker findings from `docker-audit.js`: an orphan stack, a port
|
|
766
|
-
// held by another compose project) are REPORTED but are not failures — a
|
|
767
|
-
// `doctor --worktrees || ...` in CI must not go red because somebody has an
|
|
768
|
-
// open PR, a hand-made tree, or a container still up. The human path below
|
|
769
|
-
// 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.
|
|
770
627
|
const isProblem = (i) => !['info', 'warn'].includes(i.severity);
|
|
771
628
|
|
|
772
629
|
if (options.json) {
|
|
@@ -780,10 +637,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
780
637
|
return true;
|
|
781
638
|
}
|
|
782
639
|
|
|
783
|
-
// `unintegrated-work` is
|
|
784
|
-
//
|
|
785
|
-
// it as a problem is how a diagnostic starts nagging people into destroying
|
|
786
|
-
// 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.
|
|
787
642
|
const informational = issues.filter((i) => !isProblem(i));
|
|
788
643
|
const problems = issues.filter(isProblem);
|
|
789
644
|
|
|
@@ -798,9 +653,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
798
653
|
console.log(` ${i.detail}`);
|
|
799
654
|
console.log(` → ${i.fix}`);
|
|
800
655
|
}
|
|
801
|
-
// An EMPTY unregistered
|
|
802
|
-
// be lost — so the hint must mention it, or the one issue `--prune` now fixes
|
|
803
|
-
// automatically would still read as a manual chore.
|
|
656
|
+
// An EMPTY unregistered dir is prunable too, so the hint must mention it.
|
|
804
657
|
const prunable = (i) => i.type === 'registered-missing'
|
|
805
658
|
|| i.type === 'dangling-registration'
|
|
806
659
|
|| (i.type === 'unregistered-dir' && i.empty);
|
|
@@ -817,29 +670,9 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
817
670
|
return false;
|
|
818
671
|
}
|
|
819
672
|
|
|
820
|
-
/** Dias a partir dos quais um PR aberto vira aviso. */
|
|
821
673
|
const PR_STALE_DAYS = 7;
|
|
822
674
|
|
|
823
|
-
/**
|
|
824
|
-
* As inconsistências de PR desta árvore.
|
|
825
|
-
*
|
|
826
|
-
* pr-awaiting-merge — PR aberto há muito, sem cópia local: trabalho em
|
|
827
|
-
* revisão que nenhuma superfície local reivindica.
|
|
828
|
-
* pr-merged-not-landed — o PR mergeou mas `archive/{f}` não está aqui. O
|
|
829
|
-
* archive viaja no diff do PR, então ou falta um
|
|
830
|
-
* `git pull`, ou ele não estava no diff. Branch
|
|
831
|
-
* `morph/*` cujo diff nunca tocou
|
|
832
|
-
* `.morph/{features,archive}/{f}/` não conta: o nome
|
|
833
|
-
* não prova feature (ver pr-files.js).
|
|
834
|
-
*
|
|
835
|
-
* Offline-safe: índice `null` devolve `null`, NUNCA lista vazia — lista vazia
|
|
836
|
-
* afirmaria "perguntei e não achei nada", que é diferente de "não pude
|
|
837
|
-
* perguntar". Achado #3: um `[]` disfarçado de "sem problema" é o que fazia
|
|
838
|
-
* `doctor` imprimir `✓ pull requests` mesmo sem conseguir consultar o `gh`.
|
|
839
|
-
*
|
|
840
|
-
* @returns {Array<{feature:string,type:string,detail:string}>|null}
|
|
841
|
-
* null = DESCONHECIDO (índice não pôde ser construído); nunca "sem PRs".
|
|
842
|
-
*/
|
|
675
|
+
/** `null` = DESCONHECIDO (o índice não pôde ser construído), nunca "sem PRs". */
|
|
843
676
|
function auditPullRequests(root) {
|
|
844
677
|
const index = buildPrIndex(root);
|
|
845
678
|
if (!index) return null;
|
|
@@ -874,26 +707,8 @@ function auditPullRequests(root) {
|
|
|
874
707
|
}
|
|
875
708
|
|
|
876
709
|
/**
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* **Superfície própria, no molde do `--mcp`, e é decisão, não acidente.** O
|
|
880
|
-
* `--ai` NÃO entra no `--full` nem no `doctor` default:
|
|
881
|
-
*
|
|
882
|
-
* - o renderer do `--full` é `ok → ✓`, `warn → ⚠`, **`else → ✗`** — não tem o
|
|
883
|
-
* ramo `skip → ○`. Uma checagem de IA lá seria VERMELHA neste repositório
|
|
884
|
-
* mesmo dizendo "não verificado", e `test/commands/doctor.test.js` exige
|
|
885
|
-
* `doesNotMatch(out, /✗/)` para `doctor --full` aqui, com a justificativa
|
|
886
|
-
* escrita no próprio teste: *"every check must be satisfiable here — a
|
|
887
|
-
* permanently red one teaches nothing"*;
|
|
888
|
-
* - o `doctor` default é o comando mais rodado do dia, e cinco linhas `○`
|
|
889
|
-
* permanentes em todo projeto sem .NET é ruído com custo e sem informação.
|
|
890
|
-
*
|
|
891
|
-
* Exit code: 1 se houver ao menos um `error`; 0 caso contrário. `warn` e `skip`
|
|
892
|
-
* nunca mudam o código de saída.
|
|
893
|
-
*
|
|
894
|
-
* @param {string} targetPath
|
|
895
|
-
* @param {object} [options] - repassado a `collectAiChecks` (`config`, `deps`)
|
|
896
|
-
* @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.
|
|
897
712
|
*/
|
|
898
713
|
export async function doctorAiCommand(targetPath, options = {}) {
|
|
899
714
|
const { collectAiChecks, aiExitCode, allSkipped } = await import('../../lib/ai/ai-checks.js');
|
|
@@ -906,9 +721,7 @@ export async function doctorAiCommand(targetPath, options = {}) {
|
|
|
906
721
|
const checks = collectAiChecks(targetPath, { config, deps: options.deps ?? {} });
|
|
907
722
|
|
|
908
723
|
if (allSkipped(checks)) {
|
|
909
|
-
//
|
|
910
|
-
// cinco `○` vazios — listar cinco vezes "não verificado" é o ruído que
|
|
911
|
-
// esta superfície existe para não produzir.
|
|
724
|
+
// Nenhuma checagem teve insumo: uma linha, não cinco `○` vazios.
|
|
912
725
|
const reasons = [...new Set(checks.map((c) => c.reason))];
|
|
913
726
|
for (const reason of reasons) {
|
|
914
727
|
console.log(chalk.gray(` ○ Nenhuma das ${checks.length} checagens teve insumo — ${reason}.`));
|
|
@@ -954,10 +767,8 @@ export async function doctorAiCommand(targetPath, options = {}) {
|
|
|
954
767
|
}
|
|
955
768
|
|
|
956
769
|
export async function doctorCommand(options = {}) {
|
|
957
|
-
// `--json
|
|
958
|
-
//
|
|
959
|
-
// where it asked for JSON — a caller has no way to notice a flag that was
|
|
960
|
-
// 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.
|
|
961
772
|
const scoped = ['json', 'prune'].filter((f) => options[f]);
|
|
962
773
|
if (scoped.length && !options.worktrees && !options.procs) {
|
|
963
774
|
const flags = scoped.map((f) => `--${f}`).join(' e ');
|
|
@@ -980,10 +791,7 @@ export async function doctorCommand(options = {}) {
|
|
|
980
791
|
}
|
|
981
792
|
}
|
|
982
793
|
|
|
983
|
-
// `--ai`
|
|
984
|
-
// dois relatórios com dois códigos de saída no mesmo processo — o segundo
|
|
985
|
-
// apagaria o primeiro em silêncio. Mesmo molde da guarda acima: recusa em
|
|
986
|
-
// vez de fingir.
|
|
794
|
+
// `--ai` com outra superfície daria dois códigos de saída no mesmo processo: recusa.
|
|
987
795
|
if (options.ai) {
|
|
988
796
|
const conflicts = ['full', 'mcp', 'worktrees', 'procs'].filter((f) => options[f]);
|
|
989
797
|
if (conflicts.length) {
|
|
@@ -996,7 +804,6 @@ export async function doctorCommand(options = {}) {
|
|
|
996
804
|
return doctorAiCommand(process.cwd(), options);
|
|
997
805
|
}
|
|
998
806
|
|
|
999
|
-
// Reset mode: remove morph-managed settings entries
|
|
1000
807
|
if (options.reset) {
|
|
1001
808
|
const targetPath = process.cwd();
|
|
1002
809
|
logger.header('MORPH-SPEC Settings Reset');
|
|
@@ -1021,27 +828,18 @@ export async function doctorCommand(options = {}) {
|
|
|
1021
828
|
return;
|
|
1022
829
|
}
|
|
1023
830
|
|
|
1024
|
-
// full health check mode
|
|
1025
831
|
if (options.full) {
|
|
1026
832
|
const frameworkRoot = process.cwd();
|
|
1027
833
|
return doctorFullCommand(frameworkRoot);
|
|
1028
834
|
}
|
|
1029
835
|
|
|
1030
|
-
// MCP health check mode
|
|
1031
836
|
if (options.mcp) {
|
|
1032
837
|
return doctorMcpCommand(process.cwd());
|
|
1033
838
|
}
|
|
1034
839
|
|
|
1035
|
-
// Worktree reconciliation mode (C2)
|
|
1036
840
|
if (options.worktrees) {
|
|
1037
841
|
const clean = await doctorWorktreesCommand(process.cwd(), options);
|
|
1038
|
-
// The audit result
|
|
1039
|
-
// Commander discards an action's return value, so this reported success to
|
|
1040
|
-
// every script while printing "⚠ 5 inconsistência(s)" to the human — a
|
|
1041
|
-
// `doctor --worktrees || echo falhou` in CI or a pre-commit hook saw a
|
|
1042
|
-
// clean run with orphaned worktrees piling up. The flag-misuse branch
|
|
1043
|
-
// above already treats the exit code as this command's signal channel;
|
|
1044
|
-
// 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.
|
|
1045
843
|
if (!clean) process.exitCode = 1;
|
|
1046
844
|
return clean;
|
|
1047
845
|
}
|
|
@@ -1069,11 +867,9 @@ export async function doctorCommand(options = {}) {
|
|
|
1069
867
|
}
|
|
1070
868
|
};
|
|
1071
869
|
|
|
1072
|
-
// Check versions first
|
|
1073
870
|
const cliCheck = await checkCLIOutdated();
|
|
1074
871
|
const projectCheck = await checkProjectOutdated(targetPath);
|
|
1075
872
|
|
|
1076
|
-
// CLI Version
|
|
1077
873
|
if (cliCheck.latest) {
|
|
1078
874
|
if (cliCheck.isOutdated) {
|
|
1079
875
|
checks.push({
|
|
@@ -1096,7 +892,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1096
892
|
});
|
|
1097
893
|
}
|
|
1098
894
|
|
|
1099
|
-
// Project MORPH version
|
|
1100
895
|
if (projectCheck.current) {
|
|
1101
896
|
if (projectCheck.isOutdated) {
|
|
1102
897
|
checks.push({
|
|
@@ -1120,7 +915,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1120
915
|
hasWarnings = true;
|
|
1121
916
|
}
|
|
1122
917
|
|
|
1123
|
-
// Check if morph-spec is in PATH
|
|
1124
918
|
const inPath = isMorphSpecInPath();
|
|
1125
919
|
if (inPath) {
|
|
1126
920
|
checks.push({
|
|
@@ -1136,7 +930,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1136
930
|
hasWarnings = true;
|
|
1137
931
|
}
|
|
1138
932
|
|
|
1139
|
-
// Check CLAUDE.md has morph-spec @import
|
|
1140
933
|
const claudeMd = join(targetPath, 'CLAUDE.md');
|
|
1141
934
|
const claudeMdStatus = await checkClaudeMdImport(claudeMd);
|
|
1142
935
|
if (claudeMdStatus === 'ok') {
|
|
@@ -1149,7 +942,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1149
942
|
hasErrors = true;
|
|
1150
943
|
}
|
|
1151
944
|
|
|
1152
|
-
// Check .morph folder
|
|
1153
945
|
const morphPath = join(targetPath, '.morph');
|
|
1154
946
|
if (await pathExists(morphPath)) {
|
|
1155
947
|
checks.push({ name: '.morph/', status: 'ok' });
|
|
@@ -1158,7 +950,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1158
950
|
hasErrors = true;
|
|
1159
951
|
}
|
|
1160
952
|
|
|
1161
|
-
// Check config.json
|
|
1162
953
|
const configPath = join(morphPath, 'config', 'config.json');
|
|
1163
954
|
if (await pathExists(configPath)) {
|
|
1164
955
|
try {
|
|
@@ -1177,7 +968,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1177
968
|
hasErrors = true;
|
|
1178
969
|
}
|
|
1179
970
|
|
|
1180
|
-
// Check agents.json
|
|
1181
971
|
const agentsPath = join(morphPath, 'framework', 'agents.json');
|
|
1182
972
|
if (await pathExists(agentsPath)) {
|
|
1183
973
|
try {
|
|
@@ -1196,11 +986,9 @@ export async function doctorCommand(options = {}) {
|
|
|
1196
986
|
hasErrors = true;
|
|
1197
987
|
}
|
|
1198
988
|
|
|
1199
|
-
// Check standards (either in project .morph/framework/standards/ or in framework package)
|
|
1200
989
|
const standardsPath = join(morphPath, 'framework', 'standards');
|
|
1201
990
|
const frameworkStdsRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'standards');
|
|
1202
991
|
if (await pathExists(standardsPath)) {
|
|
1203
|
-
// Check for VSA standard (core to the framework)
|
|
1204
992
|
const vsaStd = await pathExists(join(standardsPath, 'architecture', 'vertical-slice', 'vertical-slice.md')) ||
|
|
1205
993
|
await pathExists(join(standardsPath, 'vertical-slice.md'));
|
|
1206
994
|
if (vsaStd) {
|
|
@@ -1210,7 +998,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1210
998
|
hasWarnings = true;
|
|
1211
999
|
}
|
|
1212
1000
|
} else if (await pathExists(frameworkStdsRoot)) {
|
|
1213
|
-
// Framework package has standards (dev/global install mode)
|
|
1214
1001
|
checks.push({ name: 'standards/', status: 'warn', msg: 'not installed in project (run morph-spec init)' });
|
|
1215
1002
|
hasWarnings = true;
|
|
1216
1003
|
} else {
|
|
@@ -1218,15 +1005,8 @@ export async function doctorCommand(options = {}) {
|
|
|
1218
1005
|
hasErrors = true;
|
|
1219
1006
|
}
|
|
1220
1007
|
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
// layered (template-registry.js / architecture-scanner.js resolveTemplatePath)
|
|
1224
|
-
// and falls back to the npm package's own framework/templates/ when the
|
|
1225
|
-
// project-local copy is absent, which is now the expected default rather
|
|
1226
|
-
// than an error. The project-local copy remains a supported override — if
|
|
1227
|
-
// present, report it as such. The "key templates" check further below
|
|
1228
|
-
// verifies the package copy itself is intact, which is the check that
|
|
1229
|
-
// 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.
|
|
1230
1010
|
const templatesPath = join(morphPath, 'framework', 'templates');
|
|
1231
1011
|
if (await pathExists(templatesPath)) {
|
|
1232
1012
|
checks.push({ name: 'templates/', status: 'ok', msg: 'local override present' });
|
|
@@ -1234,7 +1014,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1234
1014
|
checks.push({ name: 'templates/', status: 'ok', msg: 'resolved from package (no local override)' });
|
|
1235
1015
|
}
|
|
1236
1016
|
|
|
1237
|
-
// Check .claude folder
|
|
1238
1017
|
const claudePath = join(targetPath, '.claude');
|
|
1239
1018
|
if (await pathExists(claudePath)) {
|
|
1240
1019
|
const commandsPath = join(claudePath, 'commands');
|
|
@@ -1246,7 +1025,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1246
1025
|
if (hasCommands && hasSkills) {
|
|
1247
1026
|
checks.push({ name: '.claude/', status: 'ok' });
|
|
1248
1027
|
|
|
1249
|
-
// Check skills link status
|
|
1250
1028
|
const skillEntries = await fs.readdir(skillsPath, { withFileTypes: true });
|
|
1251
1029
|
const skillDirs = skillEntries.filter(e => e.isDirectory() || e.isSymbolicLink());
|
|
1252
1030
|
let linkedCount = 0;
|
|
@@ -1255,7 +1033,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1255
1033
|
try {
|
|
1256
1034
|
const stat = await fs.lstat(join(skillsPath, dir.name));
|
|
1257
1035
|
if (stat.isSymbolicLink()) linkedCount++;
|
|
1258
|
-
} catch {
|
|
1036
|
+
} catch {}
|
|
1259
1037
|
}
|
|
1260
1038
|
|
|
1261
1039
|
if (skillDirs.length > 0) {
|
|
@@ -1270,7 +1048,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1270
1048
|
}
|
|
1271
1049
|
}
|
|
1272
1050
|
|
|
1273
|
-
// Check .claude/rules/
|
|
1274
1051
|
const rulesPath = join(claudePath, 'rules');
|
|
1275
1052
|
if (await pathExists(rulesPath)) {
|
|
1276
1053
|
const ruleFiles = await fs.readdir(rulesPath);
|
|
@@ -1281,23 +1058,18 @@ export async function doctorCommand(options = {}) {
|
|
|
1281
1058
|
hasWarnings = true;
|
|
1282
1059
|
}
|
|
1283
1060
|
|
|
1284
|
-
// Check .claude/agents/
|
|
1285
1061
|
const agentsPathClaude = join(claudePath, 'agents');
|
|
1286
1062
|
if (await pathExists(agentsPathClaude)) {
|
|
1287
1063
|
const agentFiles = await fs.readdir(agentsPathClaude);
|
|
1288
1064
|
const morphAgents = agentFiles.filter(f => f.startsWith('morph-') && f.endsWith('.md'));
|
|
1289
1065
|
checks.push({ name: '.claude/agents/', status: 'ok', msg: `${morphAgents.length} agents` });
|
|
1290
1066
|
|
|
1291
|
-
//
|
|
1292
|
-
// yet) is NOT a skip: personas install-all by default, so with an
|
|
1293
|
-
// unknown stack ALL personas are expected (agentEligible semantics) —
|
|
1294
|
-
// the exact field failure where only morph-verifier landed and every
|
|
1295
|
-
// 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.
|
|
1296
1068
|
try {
|
|
1297
1069
|
let projectTags = [];
|
|
1298
1070
|
try {
|
|
1299
1071
|
projectTags = getProjectTags(await readJson(configPath));
|
|
1300
|
-
} catch {
|
|
1072
|
+
} catch {}
|
|
1301
1073
|
const tagsLabel = projectTags.length > 0 ? projectTags.join(', ') : 'all (stack unknown)';
|
|
1302
1074
|
const frameworkDir = join(import.meta.dirname, '..', '..', '..', 'framework');
|
|
1303
1075
|
const { missing } = checkMissingAgents(targetPath, frameworkDir, projectTags);
|
|
@@ -1312,12 +1084,11 @@ export async function doctorCommand(options = {}) {
|
|
|
1312
1084
|
} else {
|
|
1313
1085
|
checks.push({ name: 'agents completeness', status: 'ok', msg: `all agents for [${tagsLabel}]` });
|
|
1314
1086
|
}
|
|
1315
|
-
} catch {
|
|
1087
|
+
} catch {}
|
|
1316
1088
|
} else {
|
|
1317
1089
|
checks.push({ name: '.claude/agents/', status: 'warn', msg: 'missing — run morph-spec update' });
|
|
1318
1090
|
hasWarnings = true;
|
|
1319
1091
|
}
|
|
1320
|
-
// ── Hook health: verify hook scripts exist ───────────────────────────────
|
|
1321
1092
|
const settingsLocalPath = join(claudePath, 'settings.local.json');
|
|
1322
1093
|
if (await pathExists(settingsLocalPath)) {
|
|
1323
1094
|
try {
|
|
@@ -1353,7 +1124,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1353
1124
|
hasWarnings = true;
|
|
1354
1125
|
}
|
|
1355
1126
|
|
|
1356
|
-
// ── Skills cliVersion: warn if installed skills reference older CLI ──────
|
|
1357
1127
|
try {
|
|
1358
1128
|
const cliVersion = getInstalledCLIVersion();
|
|
1359
1129
|
if (cliVersion) {
|
|
@@ -1375,24 +1145,19 @@ export async function doctorCommand(options = {}) {
|
|
|
1375
1145
|
hasWarnings = true;
|
|
1376
1146
|
}
|
|
1377
1147
|
}
|
|
1378
|
-
} catch {
|
|
1148
|
+
} catch {}
|
|
1379
1149
|
|
|
1380
|
-
// ── Claude Code Plugins ──────────────────────────────────────────────────
|
|
1381
1150
|
console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
|
|
1382
1151
|
const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
1383
1152
|
let pluginData = { plugins: {} };
|
|
1384
1153
|
try {
|
|
1385
1154
|
pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
|
|
1386
|
-
} catch {
|
|
1155
|
+
} catch {}
|
|
1387
1156
|
const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
|
|
1388
1157
|
check('context7 plugin', pluginStatus.context7, false,
|
|
1389
1158
|
'Run: morph-spec install-plugin context7');
|
|
1390
1159
|
} else {
|
|
1391
|
-
// `.claude/`
|
|
1392
|
-
// partial install to the full-wipe incident this command exists to catch
|
|
1393
|
-
// (folder survives, 0 items — doctor used to shrug this off as a vague
|
|
1394
|
-
// warning with no fix). Count what IS still there: near-total loss is an
|
|
1395
|
-
// 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.
|
|
1396
1161
|
const pieces = ['agents', 'rules', 'skills', 'commands'];
|
|
1397
1162
|
const present = [];
|
|
1398
1163
|
for (const piece of pieces) {
|
|
@@ -1422,7 +1187,6 @@ export async function doctorCommand(options = {}) {
|
|
|
1422
1187
|
hasErrors = true;
|
|
1423
1188
|
}
|
|
1424
1189
|
|
|
1425
|
-
// Check state.json
|
|
1426
1190
|
const statePath = join(targetPath, '.morph', 'state.json');
|
|
1427
1191
|
if (await pathExists(statePath)) {
|
|
1428
1192
|
try {
|
|
@@ -1443,10 +1207,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1443
1207
|
hasWarnings = true;
|
|
1444
1208
|
}
|
|
1445
1209
|
|
|
1446
|
-
//
|
|
1447
|
-
// gate-order violations (N approved with N-1 pending), identity-less
|
|
1448
|
-
// features (no description), orphan folders and stale index entries. Warn
|
|
1449
|
-
// only (advisory); the per-feature detail is printed after the summary.
|
|
1210
|
+
// Warn only (advisory); the per-feature detail is printed after the summary.
|
|
1450
1211
|
const lifecycleIssues = auditFeatureLifecycle(targetPath);
|
|
1451
1212
|
if (lifecycleIssues.length === 0) {
|
|
1452
1213
|
checks.push({ name: 'feature lifecycle', status: 'ok' });
|
|
@@ -1459,13 +1220,8 @@ export async function doctorCommand(options = {}) {
|
|
|
1459
1220
|
hasWarnings = true;
|
|
1460
1221
|
}
|
|
1461
1222
|
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
// sem origin no GitHub, ou qualquer falha, `auditPullRequests` nunca lança
|
|
1465
|
-
// (achado #7 — mesma guarda fail-open que `fleet-scanner.js` já usa para a
|
|
1466
|
-
// síntese idêntica) e devolve `null` — achado #3: um índice desconhecido
|
|
1467
|
-
// renderiza como "não verificado", nunca como `✓ ok`, porque `[]` e
|
|
1468
|
-
// "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 `✓`.
|
|
1469
1225
|
let prIssues;
|
|
1470
1226
|
try {
|
|
1471
1227
|
prIssues = auditPullRequests(targetPath);
|
|
@@ -1485,8 +1241,7 @@ export async function doctorCommand(options = {}) {
|
|
|
1485
1241
|
hasWarnings = true;
|
|
1486
1242
|
}
|
|
1487
1243
|
|
|
1488
|
-
//
|
|
1489
|
-
// 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.
|
|
1490
1245
|
const graphAudit = auditGraph(targetPath);
|
|
1491
1246
|
if (graphAudit.status === 'warn') {
|
|
1492
1247
|
checks.push({ name: 'codebase graph', status: 'warn', msg: graphAudit.msg });
|
|
@@ -1506,9 +1261,8 @@ export async function doctorCommand(options = {}) {
|
|
|
1506
1261
|
checks.push(storeCheck);
|
|
1507
1262
|
if (storeCheck.status === 'warn') hasWarnings = true;
|
|
1508
1263
|
}
|
|
1509
|
-
} catch {
|
|
1264
|
+
} catch {}
|
|
1510
1265
|
|
|
1511
|
-
// Check key templates exist (in framework package, not project copy)
|
|
1512
1266
|
const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
|
|
1513
1267
|
const keyTemplates = [
|
|
1514
1268
|
{ name: 'plan.md', path: join(frameworkTemplatesRoot, 'docs', 'plan.md') },
|
|
@@ -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. review approved with plan 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:'));
|