@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
|
@@ -1,69 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agents Installer (v5)
|
|
3
|
-
*
|
|
4
|
-
* Installs morph-spec domain personas as native Claude Code subagents
|
|
5
|
-
* in .claude/agents/morph-{id}.md.
|
|
6
|
-
*
|
|
7
|
-
* agents.json is the SINGLE SOURCE OF TRUTH (ADR-001, docs/specs/
|
|
8
|
-
* dispatchable-personas/decisions.md). The .md files are DERIVED artifacts:
|
|
9
|
-
* generated with a MANAGED header, regenerated on update/--reinstall, and
|
|
10
|
-
* drift-checked by `morph-spec doctor` (checkAgentsDrift). Never hand-edit
|
|
11
|
-
* them — that is exactly the v4 dual-source drift the v5 migration removed.
|
|
12
|
-
*
|
|
13
|
-
* Canonical naming: subagent_type = `morph-{id}` — the frontmatter `name`
|
|
14
|
-
* matches the filename 1:1, and the skills dispatch that exact type.
|
|
15
|
-
*
|
|
16
|
-
* In v5 the harness has 8 personas (no tiers) across two axes — domain
|
|
17
|
-
* (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer,
|
|
18
|
-
* infra-engineer, evaluator) and stage (scout, read-only diagnosis before
|
|
19
|
-
* implementation). Each persona is a subagent the Specialist (Claude in
|
|
20
|
-
* implementer role) can dispatch. The 3 roles (Orchestrator/Specialist/
|
|
21
|
-
* Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
1
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
25
2
|
import { join } from 'path';
|
|
26
3
|
import { shouldInstall } from '../lib/stack-filter.js';
|
|
27
4
|
import { realEffects } from '../lib/installers/effects.js';
|
|
28
5
|
|
|
29
|
-
/** Default tool scope for domain personas (override per agent via agents.json `tools`). */
|
|
30
6
|
export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
|
|
31
7
|
|
|
32
|
-
/**
|
|
33
|
-
* A persona that can change files: its effective tool scope grants Write or Edit.
|
|
34
|
-
* Derived from the tools, so a persona made read-only stops being one.
|
|
35
|
-
*
|
|
36
|
-
* @param {object} agent - agents.json entry
|
|
37
|
-
* @returns {boolean}
|
|
38
|
-
*/
|
|
8
|
+
/** Derived from the effective tool scope (Write or Edit), so a persona made read-only stops being one. */
|
|
39
9
|
export function isImplementer(agent) {
|
|
40
10
|
const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
|
|
41
11
|
return tools.includes('Write') || tools.includes('Edit');
|
|
42
12
|
}
|
|
43
13
|
|
|
44
|
-
|
|
45
|
-
* Turn ceiling for a persona that omits agents.json `maxTurns` (ADR-004).
|
|
46
|
-
*
|
|
47
|
-
* 20 is the value the generator hardcoded before the field existed — the v4
|
|
48
|
-
* Tier-2 default (commit 93aad05f) that survived the v5 flattening with no
|
|
49
|
-
* recorded rationale. Keeping it as the default is what lets an old
|
|
50
|
-
* agents.json render byte-for-byte the same .md as before.
|
|
51
|
-
*/
|
|
14
|
+
// The value hardcoded before the field existed: an agents.json without `maxTurns` renders byte-for-byte as before.
|
|
52
15
|
export const DEFAULT_MAX_TURNS = 20;
|
|
53
16
|
|
|
54
|
-
/** Share of the ceiling (percent) at which a sub-agent checkpoints (partial report). */
|
|
55
17
|
const CHECKPOINT_PERCENT = 70;
|
|
56
18
|
|
|
57
19
|
/**
|
|
58
|
-
* The ONE rule
|
|
59
|
-
*
|
|
60
|
-
* Check 6 — so the same agents.json can never pass in one and fail in another.
|
|
61
|
-
*
|
|
62
|
-
* Absent (`undefined`) is valid: it means "use the default". Anything present
|
|
63
|
-
* must be an integer >= 1. `null` is NOT absent — whoever wants the default
|
|
64
|
-
* removes the field.
|
|
65
|
-
*
|
|
66
|
-
* @param {unknown} value - the raw agents.json value
|
|
20
|
+
* The ONE validity rule, shared by installAgents, checkAgentsDrift, doctor and validate-framework.
|
|
21
|
+
* `undefined` means default; `null` is NOT absent.
|
|
67
22
|
* @returns {string|null} what was expected, or null when valid
|
|
68
23
|
*/
|
|
69
24
|
export function maxTurnsProblem(value) {
|
|
@@ -71,14 +26,6 @@ export function maxTurnsProblem(value) {
|
|
|
71
26
|
return Number.isInteger(value) && value >= 1 ? null : 'esperado inteiro >= 1';
|
|
72
27
|
}
|
|
73
28
|
|
|
74
|
-
/**
|
|
75
|
-
* The full message for an invalid `maxTurns` — persona, value received, what
|
|
76
|
-
* is expected and how to get the default back. Every reader prints this one.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} id - persona id (agents.json key)
|
|
79
|
-
* @param {unknown} value - the raw agents.json value
|
|
80
|
-
* @returns {string|null} null when valid
|
|
81
|
-
*/
|
|
82
29
|
export function maxTurnsError(id, value) {
|
|
83
30
|
const problem = maxTurnsProblem(value);
|
|
84
31
|
if (!problem) return null;
|
|
@@ -89,41 +36,20 @@ export function maxTurnsError(id, value) {
|
|
|
89
36
|
`Remova o campo para usar o default ${DEFAULT_MAX_TURNS}.`;
|
|
90
37
|
}
|
|
91
38
|
|
|
92
|
-
/**
|
|
93
|
-
* Resolve the ceiling a persona is generated with: its `maxTurns`, or the
|
|
94
|
-
* default when absent. Throws `maxTurnsError()` on an invalid value.
|
|
95
|
-
*
|
|
96
|
-
* @param {object} agent - agents.json entry with `id` merged in
|
|
97
|
-
* @returns {number}
|
|
98
|
-
*/
|
|
99
39
|
export function resolveMaxTurns(agent) {
|
|
100
40
|
const error = maxTurnsError(agent.id, agent.maxTurns);
|
|
101
41
|
if (error) throw new Error(error);
|
|
102
42
|
return agent.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
103
43
|
}
|
|
104
44
|
|
|
105
|
-
/** Values Claude Code accepts in a subagent's `effort` frontmatter. */
|
|
106
45
|
export const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
107
46
|
|
|
108
|
-
|
|
109
|
-
* The ONE rule for a valid `effort` — same four readers as `maxTurnsProblem`.
|
|
110
|
-
* Absent means the frontmatter carries no `effort:` line and the sub-agent
|
|
111
|
-
* inherits the session's effort, byte-for-byte what the generator emitted
|
|
112
|
-
* before the field existed.
|
|
113
|
-
*
|
|
114
|
-
* @param {unknown} value - the raw agents.json value
|
|
115
|
-
* @returns {string|null} what was expected, or null when valid
|
|
116
|
-
*/
|
|
47
|
+
// Absent: no `effort:` line, so the sub-agent inherits the session effort.
|
|
117
48
|
export function effortProblem(value) {
|
|
118
49
|
if (value === undefined) return null;
|
|
119
50
|
return VALID_EFFORTS.includes(value) ? null : `esperado um de ${VALID_EFFORTS.join('|')}`;
|
|
120
51
|
}
|
|
121
52
|
|
|
122
|
-
/**
|
|
123
|
-
* @param {string} id - persona id (agents.json key)
|
|
124
|
-
* @param {unknown} value - the raw agents.json value
|
|
125
|
-
* @returns {string|null} null when valid
|
|
126
|
-
*/
|
|
127
53
|
export function effortError(id, value) {
|
|
128
54
|
const problem = effortProblem(value);
|
|
129
55
|
if (!problem) return null;
|
|
@@ -131,26 +57,12 @@ export function effortError(id, value) {
|
|
|
131
57
|
'Remova o campo para herdar o effort da sessão.';
|
|
132
58
|
}
|
|
133
59
|
|
|
134
|
-
/** A surface id is the key the mockup checklist names (`Superfície: admin-app`). */
|
|
135
60
|
const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
136
61
|
|
|
137
62
|
const SURFACE_KEYS = new Set(['when', 'standards']);
|
|
138
63
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
* persona's standards by the kind of screen it works on
|
|
142
|
-
* (`{ "admin-app": { when, standards: [ids] } }`). Same four readers as
|
|
143
|
-
* `maxTurnsProblem`, for the same reason.
|
|
144
|
-
*
|
|
145
|
-
* Absent (`undefined`) is valid: the persona reads only `standards`. A standard
|
|
146
|
-
* listed both in `standards` (read on every surface) and in a surface is
|
|
147
|
-
* refused: that is exactly how a "separated" persona keeps loading marketing
|
|
148
|
-
* standards on a CRUD screen without anyone noticing.
|
|
149
|
-
*
|
|
150
|
-
* @param {unknown} surfaces - the raw agents.json value
|
|
151
|
-
* @param {unknown} [standards] - the persona's universal `standards`
|
|
152
|
-
* @returns {string|null} what is wrong, or null when valid
|
|
153
|
-
*/
|
|
64
|
+
// A standard both universal and in a surface is refused: the persona would keep
|
|
65
|
+
// loading it on every surface without anyone noticing.
|
|
154
66
|
export function surfacesProblem(surfaces, standards = []) {
|
|
155
67
|
if (surfaces === undefined) return null;
|
|
156
68
|
if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
|
|
@@ -185,14 +97,6 @@ export function surfacesProblem(surfaces, standards = []) {
|
|
|
185
97
|
return null;
|
|
186
98
|
}
|
|
187
99
|
|
|
188
|
-
/**
|
|
189
|
-
* The full message for an invalid `surfaces`. Every reader prints this one.
|
|
190
|
-
*
|
|
191
|
-
* @param {string} id - persona id (agents.json key)
|
|
192
|
-
* @param {unknown} surfaces
|
|
193
|
-
* @param {unknown} [standards]
|
|
194
|
-
* @returns {string|null} null when valid
|
|
195
|
-
*/
|
|
196
100
|
export function surfacesError(id, surfaces, standards) {
|
|
197
101
|
const problem = surfacesProblem(surfaces, standards);
|
|
198
102
|
if (!problem) return null;
|
|
@@ -200,14 +104,7 @@ export function surfacesError(id, surfaces, standards) {
|
|
|
200
104
|
'Remova o campo para a persona ler só `standards`.';
|
|
201
105
|
}
|
|
202
106
|
|
|
203
|
-
/**
|
|
204
|
-
* Every standard id a persona can cite: the universal list plus each
|
|
205
|
-
* surface's. Reference checks read this, so a surface id that does not exist
|
|
206
|
-
* fails like any other.
|
|
207
|
-
*
|
|
208
|
-
* @param {object} persona - agents.json entry
|
|
209
|
-
* @returns {string[]} unique ids, universal first
|
|
210
|
-
*/
|
|
107
|
+
/** @returns {string[]} unique ids of `standards` plus every surface's, universal first */
|
|
211
108
|
export function personaStandardIds(persona) {
|
|
212
109
|
const ids = [...(Array.isArray(persona?.standards) ? persona.standards : [])];
|
|
213
110
|
const surfaces = persona?.surfaces;
|
|
@@ -219,48 +116,22 @@ export function personaStandardIds(persona) {
|
|
|
219
116
|
return [...new Set(ids)];
|
|
220
117
|
}
|
|
221
118
|
|
|
222
|
-
/** The first validity error of a persona entry, or null — what drift reports as `invalid`. */
|
|
223
119
|
function personaError(agent) {
|
|
224
120
|
return maxTurnsError(agent.id, agent.maxTurns)
|
|
225
121
|
?? surfacesError(agent.id, agent.surfaces, agent.standards)
|
|
226
122
|
?? effortError(agent.id, agent.effort);
|
|
227
123
|
}
|
|
228
124
|
|
|
229
|
-
|
|
230
|
-
* Tools every persona gets on top of its scope. A `tools:` line is a strict
|
|
231
|
-
* allowlist: without `Skill` the sub-agent cannot invoke a single skill, and
|
|
232
|
-
* without `ToolSearch` it cannot load an MCP tool the session defers.
|
|
233
|
-
*/
|
|
125
|
+
// `tools:` is a strict allowlist: without Skill no skill runs, without ToolSearch no deferred MCP tool loads.
|
|
234
126
|
export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
|
|
235
127
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
* server's tools after WHERE it is configured — `mcp__context7__*` from
|
|
239
|
-
* `.mcp.json`, `mcp__plugin_context7_context7__*` from the plugin — and a grant
|
|
240
|
-
* only matches its own spelling (measured 2026-09-24: `mcp__context7` gave a
|
|
241
|
-
* plugin-installed context7 nothing). So every spelling becomes a grant; one
|
|
242
|
-
* the project does not configure is inert.
|
|
243
|
-
*
|
|
244
|
-
* @param {string} id - registry key
|
|
245
|
-
* @param {object} entry - registry entry
|
|
246
|
-
* @returns {string[]}
|
|
247
|
-
*/
|
|
128
|
+
// A grant matches only its own spelling (`mcp__context7` vs `mcp__plugin_context7_context7`),
|
|
129
|
+
// so every spelling becomes a grant; one the project does not configure is inert.
|
|
248
130
|
export function mcpServerNames(id, entry) {
|
|
249
|
-
// A malformed list falls back to the id instead of breaking the render:
|
|
250
|
-
// installAgents and checkAgentsDrift both go through here (fail-open).
|
|
251
131
|
return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
|
|
252
132
|
}
|
|
253
133
|
|
|
254
|
-
|
|
255
|
-
* The ONE rule for a registry entry's `serverNames` (validate-framework Check 8
|
|
256
|
-
* reads it). Absent is valid (`[id]`). Present: non-empty, only the characters
|
|
257
|
-
* Claude Code keeps in a tool name, and it must include the id itself — that is
|
|
258
|
-
* the name the MCP installer writes into `.mcp.json`.
|
|
259
|
-
*
|
|
260
|
-
* @param {string} id - registry key
|
|
261
|
-
* @param {unknown} value - the raw `serverNames`
|
|
262
|
-
* @returns {string|null} what was expected, or null when valid
|
|
263
|
-
*/
|
|
134
|
+
// Must include the id itself: that is the name the MCP installer writes into `.mcp.json`.
|
|
264
135
|
export function serverNamesProblem(id, value) {
|
|
265
136
|
if (value === undefined) return null;
|
|
266
137
|
if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
|
|
@@ -269,15 +140,7 @@ export function serverNamesProblem(id, value) {
|
|
|
269
140
|
return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
|
|
270
141
|
}
|
|
271
142
|
|
|
272
|
-
/**
|
|
273
|
-
* Load the MCP servers known to the framework registry
|
|
274
|
-
* (framework/mcp/registry.json) with the server names each is granted under.
|
|
275
|
-
* Only registry-known ids in agents.json `mcp` become tool grants (fail-open
|
|
276
|
-
* on any error).
|
|
277
|
-
*
|
|
278
|
-
* @param {string} frameworkDir
|
|
279
|
-
* @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
|
|
280
|
-
*/
|
|
143
|
+
/** @returns {Map<string, string[]>|null} id → server names; null = no registry (grant nothing) */
|
|
281
144
|
export function loadKnownMcpIds(frameworkDir) {
|
|
282
145
|
try {
|
|
283
146
|
const registryPath = join(frameworkDir, 'mcp', 'registry.json');
|
|
@@ -289,21 +152,8 @@ export function loadKnownMcpIds(frameworkDir) {
|
|
|
289
152
|
}
|
|
290
153
|
}
|
|
291
154
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
* with NO known stack tags (greenfield / install before config.json exists)
|
|
295
|
-
* EVERY persona is generated — a project that dispatches `morph-{id}` must
|
|
296
|
-
* find the subagent registered, and an install that only lands
|
|
297
|
-
* morph-evaluator forces every domain dispatch into the general-purpose
|
|
298
|
-
* fallback. Stack filtering only applies when the project's stack is KNOWN
|
|
299
|
-
* (tags non-empty): the fallback stays reserved for stacks the project
|
|
300
|
-
* genuinely does not have. Note this deliberately differs from rules/skills
|
|
301
|
-
* filtering (stack-filter.js), which stays minimal on greenfield.
|
|
302
|
-
*
|
|
303
|
-
* @param {object} agent - agents.json entry (with `stack`)
|
|
304
|
-
* @param {string[]} projectTags
|
|
305
|
-
* @returns {boolean}
|
|
306
|
-
*/
|
|
155
|
+
// With no known stack tags every persona is generated: a dispatch of `morph-{id}` must find
|
|
156
|
+
// its subagent. Deliberately differs from rules/skills filtering.
|
|
307
157
|
export function agentEligible(agent, projectTags = []) {
|
|
308
158
|
if (projectTags.length === 0) return true;
|
|
309
159
|
return shouldInstall(agent.stack ?? ['*'], projectTags);
|
|
@@ -314,17 +164,6 @@ const MANAGED_HEADER =
|
|
|
314
164
|
' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
|
|
315
165
|
' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
|
|
316
166
|
|
|
317
|
-
/**
|
|
318
|
-
* Install morph-spec domain personas as native Claude Code subagents.
|
|
319
|
-
*
|
|
320
|
-
* @param {string} projectDir - Target project directory
|
|
321
|
-
* @param {string} frameworkDir - Path to morph-spec framework/ directory
|
|
322
|
-
* @param {object} options
|
|
323
|
-
* @param {string[]} [options.projectTags=[]] - Stack tags from config (e.g. ['dotnet', 'nextjs'])
|
|
324
|
-
* @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
|
|
325
|
-
* Coletor de efeitos; o default executa e registra numa lista descartada.
|
|
326
|
-
* @returns {Promise<{ installed: number }>}
|
|
327
|
-
*/
|
|
328
167
|
export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
|
|
329
168
|
const { projectTags = [], fx = realEffects() } = options;
|
|
330
169
|
|
|
@@ -336,8 +175,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
336
175
|
.filter(([key]) => !key.startsWith('_comment'))
|
|
337
176
|
.map(([id, data]) => ({ id, ...data }));
|
|
338
177
|
|
|
339
|
-
// Validate
|
|
340
|
-
// would leave some personas regenerated and others stale.
|
|
178
|
+
// Validate every persona before the first write: refusing halfway leaves a mix of stale and fresh.
|
|
341
179
|
for (const agent of agents) {
|
|
342
180
|
const error = personaError(agent);
|
|
343
181
|
if (error) throw new Error(error);
|
|
@@ -348,10 +186,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
348
186
|
|
|
349
187
|
const knownMcpIds = loadKnownMcpIds(frameworkDir);
|
|
350
188
|
|
|
351
|
-
// Filter by stack — install-all when the stack is unknown (empty tags).
|
|
352
189
|
const eligible = agents.filter(a => agentEligible(a, projectTags));
|
|
353
190
|
|
|
354
|
-
// Build set of all possible filenames for orphan cleanup
|
|
355
191
|
const allPossible = new Set(agents.map(a => `morph-${a.id}.md`));
|
|
356
192
|
|
|
357
193
|
let installed = 0;
|
|
@@ -373,12 +209,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
373
209
|
installed++;
|
|
374
210
|
}
|
|
375
211
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// Calado em `dryRun` pelo mesmo motivo do laço gêmeo em skills-installer.js
|
|
379
|
-
// (D6): numa execução real do `update` este diretório acabou de ser apagado
|
|
380
|
-
// inteiro por `cleanFrameworkDirs()`, então o laço nunca remove nada. Num
|
|
381
|
-
// dry ingênuo ele veria o diretório antigo e preveria remoções inexistentes.
|
|
212
|
+
// Silent in dryRun: the real update's cleanFrameworkDirs() already emptied the dir.
|
|
382
213
|
if (!fx.dryRun) {
|
|
383
214
|
for (const file of readdirSync(targetDir)) {
|
|
384
215
|
if (allPossible.has(file) && !installedFilenames.has(file)) {
|
|
@@ -390,20 +221,10 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
390
221
|
return { installed, agents: installedAgents };
|
|
391
222
|
}
|
|
392
223
|
|
|
393
|
-
|
|
394
|
-
* Render the full .md content for a persona — deterministic, so the same
|
|
395
|
-
* function serves the installer AND the doctor drift check (a hand-edited or
|
|
396
|
-
* stale file differs byte-for-byte from this output).
|
|
397
|
-
*
|
|
398
|
-
* @param {object} agent - agents.json entry with `id` merged in
|
|
399
|
-
* @param {Map<string, string[]>|null} [knownMcpIds] - registry-known MCP ids and
|
|
400
|
-
* their server names; agents.json `mcp` entries outside it are ignored (null = ignore all)
|
|
401
|
-
* @returns {string} complete file content
|
|
402
|
-
*/
|
|
224
|
+
// Deterministic: the doctor drift check compares files byte-for-byte against this output.
|
|
403
225
|
export function renderAgentMd(agent, knownMcpIds = null) {
|
|
404
226
|
const description = buildDescription(agent);
|
|
405
|
-
//
|
|
406
|
-
// must be the same number — the sub-agent never sees its own frontmatter.
|
|
227
|
+
// One value for the frontmatter and the body: the sub-agent never sees its own frontmatter.
|
|
407
228
|
const maxTurns = resolveMaxTurns(agent);
|
|
408
229
|
const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
|
|
409
230
|
if (surfaces) throw new Error(surfaces);
|
|
@@ -417,9 +238,6 @@ export function renderAgentMd(agent, knownMcpIds = null) {
|
|
|
417
238
|
function buildToolsLine(agent, knownMcpIds) {
|
|
418
239
|
const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
|
|
419
240
|
const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
|
|
420
|
-
// Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
|
|
421
|
-
// grants, one per server name (`mcp__{name}`). Only registry-known ids are
|
|
422
|
-
// emitted; in projects without that server configured the grant is inert.
|
|
423
241
|
const mcpGrants = (agent.mcp ?? [])
|
|
424
242
|
.filter(id => knownMcpIds?.has(id))
|
|
425
243
|
.flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
|
|
@@ -428,16 +246,11 @@ function buildToolsLine(agent, knownMcpIds) {
|
|
|
428
246
|
|
|
429
247
|
function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
430
248
|
const lines = [
|
|
431
|
-
// Canonical 1:1 naming: frontmatter name === filename === subagent_type.
|
|
432
249
|
`name: morph-${agent.id}`,
|
|
433
250
|
`description: ${description}`,
|
|
434
|
-
// The evaluator is sonnet here; the dispatch routes it to opus when the risk
|
|
435
|
-
// detector flags the diff. A persona without the field inherits the session model.
|
|
436
251
|
`model: ${agent.model ?? 'inherit'}`,
|
|
437
252
|
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
438
|
-
//
|
|
439
|
-
// line keeps its position so an agents.json without the field renders
|
|
440
|
-
// exactly what the hardcoded `maxTurns: 20` rendered.
|
|
253
|
+
// Position fixed so an agents.json without the field renders byte-for-byte as before.
|
|
441
254
|
`maxTurns: ${maxTurns}`,
|
|
442
255
|
];
|
|
443
256
|
if (agent.effort) lines.push(`effort: ${agent.effort}`);
|
|
@@ -453,6 +266,57 @@ function buildDescription(agent) {
|
|
|
453
266
|
return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
|
|
454
267
|
}
|
|
455
268
|
|
|
269
|
+
const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
|
|
270
|
+
'(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
|
|
271
|
+
'nunca chame tool destrutiva sem confirmação humana explícita.';
|
|
272
|
+
|
|
273
|
+
const STANDARDS_HINT = 'Digest de cada um: `morph-spec standards --find <id>`; só a seção que importa: ' +
|
|
274
|
+
'`morph-spec standards --show <id>#<âncora>` (`--show <id>` lista as âncoras).';
|
|
275
|
+
|
|
276
|
+
export const DISPATCH_CONTRACT = [
|
|
277
|
+
'## Contrato de dispatch',
|
|
278
|
+
'',
|
|
279
|
+
'O dispatch traz só o que muda por fatia: id e título, `cobre` com o texto de cada cenário, `arquivos`, `standards`, ' +
|
|
280
|
+
'o caminho do relatório e, se houver, o worktree. O resto é este contrato.',
|
|
281
|
+
'',
|
|
282
|
+
'1. RELATÓRIO: GRAVE o arquivo de relatório que o dispatch nomeia (`.morph/features/{feature}/3-implement/reports/F{n}.md`) ' +
|
|
283
|
+
'ANTES de encerrar — é o retorno autoritativo, não a sua mensagem final. `SendMessage` não é substituto do relatório e não deve ser chamado.',
|
|
284
|
+
'2. ESTADO: NÃO toque em `plan.md`, `checks.json`, `feature.json`, `state.json` nem no board de sessão — todo estado é gravado exclusivamente pelo orquestrador.',
|
|
285
|
+
'3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
|
|
286
|
+
'um por pós-condição, um de regressão por bug, variantes numa tabela (`[Theory]`/`it.each`). NÃO rode teste a cada marco nem faça ciclo red→green: ' +
|
|
287
|
+
'escreva código e testes, compile, e rode SÓ o filtro dos seus testes UMA vez, em primeiro plano, antes do relatório; se falhar, corrija e rode só os que falharam. ' +
|
|
288
|
+
'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
|
|
289
|
+
'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
|
|
290
|
+
'4. ESCOPO: uma fatia, ~4 arquivos (nunca mais de 8). Escopo maior falha e volta — fatie antes, não depois.',
|
|
291
|
+
'5. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `wip(F{n})` a cada marco (um arquivo ou uma fatia coerente escrita), não só ao fim — ' +
|
|
292
|
+
'e só os seus arquivos (`git add <paths>`, nunca `-A` nem diretório). Sem commit, a retomada recomeça do zero.',
|
|
293
|
+
'6. PROCESSOS: NUNCA `taskkill /IM node.exe` (derruba os MCPs de todas as sessões). Encerre dev server pelo PID que você mesmo subiu. ' +
|
|
294
|
+
'Build e suíte pesados rodam por `morph-spec heavy run -- <comando>`, em primeiro plano. Exit 3 é fila cheia depois da espera da própria fila: ' +
|
|
295
|
+
'não repita nem espere em laço — diga no relatório o que não rodou.',
|
|
296
|
+
'7. REUSO: antes de criar tipo, componente, hook ou serviço NOVO, verifique se já existe: `morph-spec graph explain <nome>` e ' +
|
|
297
|
+
'`graph affected <conceito> --uses-only`; sem grafo (`no-graph`), caia para `grep -rn "<nome>"` e siga — a ausência de grafo nunca bloqueia. ' +
|
|
298
|
+
'Se criar mesmo assim, diga por que em uma linha no relatório.',
|
|
299
|
+
'8. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver um teste falhar (`sed`, `mv`, `git show HEAD:… >`, `git stash`, ' +
|
|
300
|
+
'`git checkout --`). A prova de que o teste morde é do verificador (Gate 3).',
|
|
301
|
+
'9. ESCRITA: código e teste do projeto só por Write/Edit, nunca pelo Bash — heredoc, redirecionamento, `sed -i` ou script python/node que grava (`cat > X.cs <<EOF`, `sed -i … X.cs`): ' +
|
|
302
|
+
'os guards de escrita só enxergam Write/Edit.',
|
|
303
|
+
'',
|
|
304
|
+
'Formato do relatório (o `report-check` lê estas marcas):',
|
|
305
|
+
'',
|
|
306
|
+
'```',
|
|
307
|
+
'# F1 — <título>',
|
|
308
|
+
'O que foi feito, em 2-3 frases: o comportamento entregue e onde ele mora, sem repetir a lista de arquivos.',
|
|
309
|
+
'**Arquivos:** `src/a.cs`, `src/a.test.cs`',
|
|
310
|
+
'**Checks:**',
|
|
311
|
+
'- C1 — provado: `a.test.cs` › nome do teste (ou `arquivo:linha`)',
|
|
312
|
+
'- C2 — não provado: por quê',
|
|
313
|
+
'```',
|
|
314
|
+
'',
|
|
315
|
+
'`**Arquivos:**` lista TODO arquivo criado ou modificado, inclusive artefato auxiliar fora do plano (factory, seed, config gerada): ' +
|
|
316
|
+
'arquivo em produção sem dono é falha de protocolo que o Gate 3 aponta. Todo cenário que a fatia `cobre` aparece em `**Checks:**`. ' +
|
|
317
|
+
'Problemas e como foram resolvidos vão em prosa depois.',
|
|
318
|
+
];
|
|
319
|
+
|
|
456
320
|
function buildBody(agent, maxTurns) {
|
|
457
321
|
const lines = [
|
|
458
322
|
`# ${agent.title ?? agent.id}`,
|
|
@@ -461,12 +325,20 @@ function buildBody(agent, maxTurns) {
|
|
|
461
325
|
'',
|
|
462
326
|
];
|
|
463
327
|
|
|
464
|
-
|
|
328
|
+
// The shape without effort or surfaces renders byte-for-byte as the old fixture.
|
|
329
|
+
const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
|
|
330
|
+
|
|
331
|
+
if (legacyShape && agent.domains?.length) {
|
|
465
332
|
lines.push('## Domains', ...agent.domains.map(d => `- ${d}`), '');
|
|
466
333
|
}
|
|
467
334
|
|
|
335
|
+
if (!legacyShape && agent.mcp?.includes('neon')) {
|
|
336
|
+
lines.push(NEON_CAUTION, '');
|
|
337
|
+
}
|
|
338
|
+
|
|
468
339
|
if (agent.standards?.length) {
|
|
469
340
|
lines.push('## Standards to consult on demand');
|
|
341
|
+
if (!legacyShape) lines.push(STANDARDS_HINT);
|
|
470
342
|
for (const s of agent.standards) lines.push(`- ${s}`);
|
|
471
343
|
lines.push('');
|
|
472
344
|
}
|
|
@@ -485,7 +357,9 @@ function buildBody(agent, maxTurns) {
|
|
|
485
357
|
}
|
|
486
358
|
}
|
|
487
359
|
|
|
488
|
-
if (isImplementer(agent)) {
|
|
360
|
+
if (agent.effort && isImplementer(agent)) {
|
|
361
|
+
lines.push(...DISPATCH_CONTRACT, '');
|
|
362
|
+
} else if (isImplementer(agent)) {
|
|
489
363
|
lines.push(
|
|
490
364
|
'## How you operate',
|
|
491
365
|
'',
|
|
@@ -493,33 +367,27 @@ function buildBody(agent, maxTurns) {
|
|
|
493
367
|
'',
|
|
494
368
|
);
|
|
495
369
|
} else {
|
|
496
|
-
// A read-only persona has no task to do nor doneCriteria to report.
|
|
497
370
|
lines.push(
|
|
498
371
|
'## Como você opera',
|
|
499
372
|
'',
|
|
500
|
-
'Você é read-only: nunca escreva, edite nem reverta código, testes,
|
|
373
|
+
'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
|
|
501
374
|
'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
|
|
502
375
|
'',
|
|
503
376
|
);
|
|
504
377
|
}
|
|
505
378
|
|
|
506
379
|
lines.push(
|
|
507
|
-
// `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps
|
|
508
|
-
//
|
|
380
|
+
// `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps a partial
|
|
381
|
+
// review from carrying a Verdict the gate would read as final.
|
|
509
382
|
'## Turn budget',
|
|
510
383
|
'',
|
|
511
|
-
// Pasted into EVERY dispatch of every persona — kept to the minimum that
|
|
512
|
-
// still carries the contract (test/framework/maf-expert-persona.test.js
|
|
513
|
-
// pins the rendered size).
|
|
514
384
|
`You have ${maxTurns} turns (\`maxTurns\`); at the ceiling you are cut off mid-work, without warning. ` +
|
|
515
385
|
'One response is one turn, however many parallel tool calls it makes. ' +
|
|
516
386
|
`By turn ${checkpointTurn(maxTurns)}, if your dispatch names a report file, save a partial there — ` +
|
|
517
387
|
'`PARCIAL` on line 1, done, remaining, files, no verdict (final report only) — then continue or stop, your call; the final report replaces it.',
|
|
518
388
|
);
|
|
519
389
|
|
|
520
|
-
// The
|
|
521
|
-
// body is the only text every implementer dispatch is guaranteed to carry.
|
|
522
|
-
// Gated on `effort` so a persona without it renders byte-for-byte as before.
|
|
390
|
+
// The only text every implementer dispatch is guaranteed to carry.
|
|
523
391
|
if (agent.effort && isImplementer(agent)) {
|
|
524
392
|
lines.push(
|
|
525
393
|
'',
|
|
@@ -529,37 +397,22 @@ function buildBody(agent, maxTurns) {
|
|
|
529
397
|
'Use Read/Grep/Glob, never grep/sed/cat/find via Bash. Check a library API in the `context7` MCP before guessing ' +
|
|
530
398
|
'(load a deferred `mcp__` tool with ToolSearch). Write the tests the budget asks for, then run only your own ' +
|
|
531
399
|
'filter ONCE before the report — no red→green cycles, no `morph-spec verify` (the orchestrator proves it). ' +
|
|
532
|
-
'Never alter, revert or move production code to watch a test fail: mutation proof belongs to the Gate 3
|
|
400
|
+
'Never alter, revert or move production code to watch a test fail: mutation proof belongs to the Gate 3 verifier.',
|
|
533
401
|
);
|
|
534
402
|
}
|
|
535
403
|
|
|
536
404
|
return lines.join('\n');
|
|
537
405
|
}
|
|
538
406
|
|
|
539
|
-
/** The turn at which a sub-agent with `maxTurns` saves its partial report. */
|
|
540
407
|
function checkpointTurn(maxTurns) {
|
|
541
|
-
// Integer arithmetic: 0.7 × N in floating point is not
|
|
408
|
+
// Integer arithmetic: 0.7 × N in floating point is not exact.
|
|
542
409
|
return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
|
|
543
410
|
}
|
|
544
411
|
|
|
545
|
-
/**
|
|
546
|
-
* Compatibility shim for older callers that expected installDomainAgents.
|
|
547
|
-
* In v5 there is no separate "domain" install step — installAgents handles
|
|
548
|
-
* everything from agents.json.
|
|
549
|
-
*/
|
|
550
412
|
export async function installDomainAgents(projectDir, frameworkDir, options) {
|
|
551
|
-
// No-op: domain content is now defined inline in agents.json (persona field).
|
|
552
413
|
return { specialists: 0 };
|
|
553
414
|
}
|
|
554
415
|
|
|
555
|
-
/**
|
|
556
|
-
* Check which agents SHOULD be installed but are NOT.
|
|
557
|
-
*
|
|
558
|
-
* @param {string} projectDir
|
|
559
|
-
* @param {string} frameworkDir
|
|
560
|
-
* @param {string[]} projectTags
|
|
561
|
-
* @returns {{ missing: Array<{ name: string, stacks: string[] }>, installed: number, expected: number }}
|
|
562
|
-
*/
|
|
563
416
|
export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
564
417
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
565
418
|
const installedFiles = new Set();
|
|
@@ -594,26 +447,8 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
594
447
|
}
|
|
595
448
|
|
|
596
449
|
/**
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* - missing: eligible persona with no .md installed → run setup-infra --reinstall
|
|
601
|
-
* - stale: .md exists but differs from renderAgentMd() output (hand-edited
|
|
602
|
-
* or generated by an older CLI) → regenerate. `staleFields` names
|
|
603
|
-
* what differs per file: the frontmatter keys, and `body`.
|
|
604
|
-
* - invalid: persona whose agents.json entry cannot be rendered (an invalid
|
|
605
|
-
* `maxTurns` or `surfaces`) — reported with the reason instead of throwing, so
|
|
606
|
-
* the doctor prints the cause and not "failed to compute"
|
|
607
|
-
* - orphans: morph-*.md files with no matching agents.json entry
|
|
608
|
-
*
|
|
609
|
-
* @param {string} projectDir
|
|
610
|
-
* @param {string} frameworkDir - directory containing agents.json
|
|
611
|
-
* @param {string[]} projectTags
|
|
612
|
-
* @param {object} [options]
|
|
613
|
-
* @param {string} [options.mcpRegistryDir=frameworkDir] - where `mcp/registry.json`
|
|
614
|
-
* lives. It must be the directory the INSTALLER read: an installed project's
|
|
615
|
-
* `.morph/framework/` carries agents.json but no `mcp/`, so rendering against
|
|
616
|
-
* it drops every `mcp__*` grant and flags a pristine install as stale.
|
|
450
|
+
* `mcpRegistryDir` must be where the INSTALLER read the registry: the installed
|
|
451
|
+
* `.morph/framework/` has no `mcp/`, and rendering against it flags a clean install as stale.
|
|
617
452
|
* @returns {{ missing: string[], stale: string[], staleFields: Object<string, string[]>,
|
|
618
453
|
* invalid: Array<{ file: string, reason: string }>, orphans: string[], expected: number }}
|
|
619
454
|
*/
|
|
@@ -672,10 +507,6 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = [], opt
|
|
|
672
507
|
return { missing, stale, staleFields, invalid, orphans, expected };
|
|
673
508
|
}
|
|
674
509
|
|
|
675
|
-
/**
|
|
676
|
-
* Split a generated .md into its frontmatter map and the rest.
|
|
677
|
-
* @returns {{ fields: Map<string, string>, body: string }}
|
|
678
|
-
*/
|
|
679
510
|
function splitAgentMd(content) {
|
|
680
511
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
681
512
|
if (!match) return { fields: new Map(), body: content };
|
|
@@ -687,11 +518,6 @@ function splitAgentMd(content) {
|
|
|
687
518
|
return { fields, body: match[2] };
|
|
688
519
|
}
|
|
689
520
|
|
|
690
|
-
/**
|
|
691
|
-
* Name what differs between a .md on disk and its render: the frontmatter
|
|
692
|
-
* keys whose value (or presence) differs, in render order, then `body`.
|
|
693
|
-
* A file with no parseable frontmatter differs in everything → `frontmatter`.
|
|
694
|
-
*/
|
|
695
521
|
function diffAgentMd(onDisk, rendered) {
|
|
696
522
|
const disk = splitAgentMd(onDisk);
|
|
697
523
|
const want = splitAgentMd(rendered);
|
|
@@ -699,16 +525,10 @@ function diffAgentMd(onDisk, rendered) {
|
|
|
699
525
|
const keys = [...new Set([...want.fields.keys(), ...disk.fields.keys()])];
|
|
700
526
|
const fields = keys.filter(k => disk.fields.get(k) !== want.fields.get(k));
|
|
701
527
|
if (disk.body !== want.body) fields.push('body');
|
|
702
|
-
// Bytes differ but no key/value does (
|
|
703
|
-
// fences): still drift, and it lives in the frontmatter.
|
|
528
|
+
// Bytes differ but no key/value does (a stray blank line between the fences).
|
|
704
529
|
return fields.length > 0 ? fields : ['frontmatter'];
|
|
705
530
|
}
|
|
706
531
|
|
|
707
|
-
/**
|
|
708
|
-
* Compatibility export — kept so existing tests/CLI code that imports
|
|
709
|
-
* formatStandardRef or parseSkillFile / collectSkillFiles still work,
|
|
710
|
-
* but they are no-ops (will be removed in v5.1).
|
|
711
|
-
*/
|
|
712
532
|
export function formatStandardRef(standard) {
|
|
713
533
|
if (typeof standard === 'string') return `- ${standard}`;
|
|
714
534
|
const id = standard.id || String(standard);
|