@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
|
@@ -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, verifier, reviewer, security) 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-verifier 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 verifier 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,12 +266,6 @@ function buildDescription(agent) {
|
|
|
453
266
|
return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
|
|
454
267
|
}
|
|
455
268
|
|
|
456
|
-
/**
|
|
457
|
-
* The fixed half of every implementer dispatch. The orchestrator paraphrases
|
|
458
|
-
* a prompt template in the field; the generated body is the only text a
|
|
459
|
-
* dispatch is guaranteed to carry, so the contract lives here and the
|
|
460
|
-
* dispatch carries only what changes per slice.
|
|
461
|
-
*/
|
|
462
269
|
const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
|
|
463
270
|
'(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
|
|
464
271
|
'nunca chame tool destrutiva sem confirmação humana explícita.';
|
|
@@ -476,7 +283,7 @@ export const DISPATCH_CONTRACT = [
|
|
|
476
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.',
|
|
477
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.',
|
|
478
285
|
'3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
|
|
479
|
-
'um por
|
|
286
|
+
'um por Cn `prova: teste`, variantes em tabela (`[Theory]`/`it.each`); `leitura` sem teste. NÃO rode teste a cada marco nem faça ciclo red→green: ' +
|
|
480
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. ' +
|
|
481
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. ' +
|
|
482
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.',
|
|
@@ -518,7 +325,7 @@ function buildBody(agent, maxTurns) {
|
|
|
518
325
|
'',
|
|
519
326
|
];
|
|
520
327
|
|
|
521
|
-
// The
|
|
328
|
+
// The shape without effort or surfaces renders byte-for-byte as the old fixture.
|
|
522
329
|
const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
|
|
523
330
|
|
|
524
331
|
if (legacyShape && agent.domains?.length) {
|
|
@@ -560,7 +367,6 @@ function buildBody(agent, maxTurns) {
|
|
|
560
367
|
'',
|
|
561
368
|
);
|
|
562
369
|
} else {
|
|
563
|
-
// A read-only persona has no slice to implement nor scenario to report.
|
|
564
370
|
lines.push(
|
|
565
371
|
'## Como você opera',
|
|
566
372
|
'',
|
|
@@ -571,22 +377,17 @@ function buildBody(agent, maxTurns) {
|
|
|
571
377
|
}
|
|
572
378
|
|
|
573
379
|
lines.push(
|
|
574
|
-
// `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps
|
|
575
|
-
// review
|
|
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.
|
|
576
382
|
'## Turn budget',
|
|
577
383
|
'',
|
|
578
|
-
// Pasted into EVERY dispatch of every persona — kept to the minimum that
|
|
579
|
-
// still carries the contract (test/framework/maf-expert-persona.test.js
|
|
580
|
-
// pins the rendered size).
|
|
581
384
|
`You have ${maxTurns} turns (\`maxTurns\`); at the ceiling you are cut off mid-work, without warning. ` +
|
|
582
385
|
'One response is one turn, however many parallel tool calls it makes. ' +
|
|
583
386
|
`By turn ${checkpointTurn(maxTurns)}, if your dispatch names a report file, save a partial there — ` +
|
|
584
387
|
'`PARCIAL` on line 1, done, remaining, files, no verdict (final report only) — then continue or stop, your call; the final report replaces it.',
|
|
585
388
|
);
|
|
586
389
|
|
|
587
|
-
// The
|
|
588
|
-
// body is the only text every implementer dispatch is guaranteed to carry.
|
|
589
|
-
// 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.
|
|
590
391
|
if (agent.effort && isImplementer(agent)) {
|
|
591
392
|
lines.push(
|
|
592
393
|
'',
|
|
@@ -603,30 +404,15 @@ function buildBody(agent, maxTurns) {
|
|
|
603
404
|
return lines.join('\n');
|
|
604
405
|
}
|
|
605
406
|
|
|
606
|
-
/** The turn at which a sub-agent with `maxTurns` saves its partial report. */
|
|
607
407
|
function checkpointTurn(maxTurns) {
|
|
608
|
-
// Integer arithmetic: 0.7 × N in floating point is not
|
|
408
|
+
// Integer arithmetic: 0.7 × N in floating point is not exact.
|
|
609
409
|
return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
|
|
610
410
|
}
|
|
611
411
|
|
|
612
|
-
/**
|
|
613
|
-
* Compatibility shim for older callers that expected installDomainAgents.
|
|
614
|
-
* In v5 there is no separate "domain" install step — installAgents handles
|
|
615
|
-
* everything from agents.json.
|
|
616
|
-
*/
|
|
617
412
|
export async function installDomainAgents(projectDir, frameworkDir, options) {
|
|
618
|
-
// No-op: domain content is now defined inline in agents.json (persona field).
|
|
619
413
|
return { specialists: 0 };
|
|
620
414
|
}
|
|
621
415
|
|
|
622
|
-
/**
|
|
623
|
-
* Check which agents SHOULD be installed but are NOT.
|
|
624
|
-
*
|
|
625
|
-
* @param {string} projectDir
|
|
626
|
-
* @param {string} frameworkDir
|
|
627
|
-
* @param {string[]} projectTags
|
|
628
|
-
* @returns {{ missing: Array<{ name: string, stacks: string[] }>, installed: number, expected: number }}
|
|
629
|
-
*/
|
|
630
416
|
export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
631
417
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
632
418
|
const installedFiles = new Set();
|
|
@@ -661,26 +447,8 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
661
447
|
}
|
|
662
448
|
|
|
663
449
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
* - missing: eligible persona with no .md installed → run setup-infra --reinstall
|
|
668
|
-
* - stale: .md exists but differs from renderAgentMd() output (hand-edited
|
|
669
|
-
* or generated by an older CLI) → regenerate. `staleFields` names
|
|
670
|
-
* what differs per file: the frontmatter keys, and `body`.
|
|
671
|
-
* - invalid: persona whose agents.json entry cannot be rendered (an invalid
|
|
672
|
-
* `maxTurns` or `surfaces`) — reported with the reason instead of throwing, so
|
|
673
|
-
* the doctor prints the cause and not "failed to compute"
|
|
674
|
-
* - orphans: morph-*.md files with no matching agents.json entry
|
|
675
|
-
*
|
|
676
|
-
* @param {string} projectDir
|
|
677
|
-
* @param {string} frameworkDir - directory containing agents.json
|
|
678
|
-
* @param {string[]} projectTags
|
|
679
|
-
* @param {object} [options]
|
|
680
|
-
* @param {string} [options.mcpRegistryDir=frameworkDir] - where `mcp/registry.json`
|
|
681
|
-
* lives. It must be the directory the INSTALLER read: an installed project's
|
|
682
|
-
* `.morph/framework/` carries agents.json but no `mcp/`, so rendering against
|
|
683
|
-
* 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.
|
|
684
452
|
* @returns {{ missing: string[], stale: string[], staleFields: Object<string, string[]>,
|
|
685
453
|
* invalid: Array<{ file: string, reason: string }>, orphans: string[], expected: number }}
|
|
686
454
|
*/
|
|
@@ -739,10 +507,6 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = [], opt
|
|
|
739
507
|
return { missing, stale, staleFields, invalid, orphans, expected };
|
|
740
508
|
}
|
|
741
509
|
|
|
742
|
-
/**
|
|
743
|
-
* Split a generated .md into its frontmatter map and the rest.
|
|
744
|
-
* @returns {{ fields: Map<string, string>, body: string }}
|
|
745
|
-
*/
|
|
746
510
|
function splitAgentMd(content) {
|
|
747
511
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
748
512
|
if (!match) return { fields: new Map(), body: content };
|
|
@@ -754,11 +518,6 @@ function splitAgentMd(content) {
|
|
|
754
518
|
return { fields, body: match[2] };
|
|
755
519
|
}
|
|
756
520
|
|
|
757
|
-
/**
|
|
758
|
-
* Name what differs between a .md on disk and its render: the frontmatter
|
|
759
|
-
* keys whose value (or presence) differs, in render order, then `body`.
|
|
760
|
-
* A file with no parseable frontmatter differs in everything → `frontmatter`.
|
|
761
|
-
*/
|
|
762
521
|
function diffAgentMd(onDisk, rendered) {
|
|
763
522
|
const disk = splitAgentMd(onDisk);
|
|
764
523
|
const want = splitAgentMd(rendered);
|
|
@@ -766,16 +525,10 @@ function diffAgentMd(onDisk, rendered) {
|
|
|
766
525
|
const keys = [...new Set([...want.fields.keys(), ...disk.fields.keys()])];
|
|
767
526
|
const fields = keys.filter(k => disk.fields.get(k) !== want.fields.get(k));
|
|
768
527
|
if (disk.body !== want.body) fields.push('body');
|
|
769
|
-
// Bytes differ but no key/value does (
|
|
770
|
-
// fences): still drift, and it lives in the frontmatter.
|
|
528
|
+
// Bytes differ but no key/value does (a stray blank line between the fences).
|
|
771
529
|
return fields.length > 0 ? fields : ['frontmatter'];
|
|
772
530
|
}
|
|
773
531
|
|
|
774
|
-
/**
|
|
775
|
-
* Compatibility export — kept so existing tests/CLI code that imports
|
|
776
|
-
* formatStandardRef or parseSkillFile / collectSkillFiles still work,
|
|
777
|
-
* but they are no-ops (will be removed in v5.1).
|
|
778
|
-
*/
|
|
779
532
|
export function formatStandardRef(standard) {
|
|
780
533
|
if (typeof standard === 'string') return `- ${standard}`;
|
|
781
534
|
const id = standard.id || String(standard);
|
package/src/utils/banner.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/utils/banner.js
|
|
2
1
|
import chalk from "chalk";
|
|
3
2
|
import { type as osType, release as osRelease, homedir } from "os";
|
|
4
3
|
import { join } from "path";
|
|
@@ -7,7 +6,6 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
|
7
6
|
const BRAND = chalk.hex("#7C3AED");
|
|
8
7
|
const DIM = chalk.dim;
|
|
9
8
|
|
|
10
|
-
/** Purple gradient, one tone per logo line (top → bottom). */
|
|
11
9
|
const GRADIENT = [
|
|
12
10
|
"#6D28D9",
|
|
13
11
|
"#7C3AED",
|
|
@@ -26,16 +24,8 @@ const LOGO_LINES = [
|
|
|
26
24
|
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝",
|
|
27
25
|
];
|
|
28
26
|
|
|
29
|
-
const LOGO_WIDTH = LOGO_LINES[0].length;
|
|
27
|
+
const LOGO_WIDTH = LOGO_LINES[0].length;
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
* The morph-spec logo: gradient "MORPH" block art with a S P E C + version
|
|
33
|
-
* rule underneath. Single source for the CLI help banner and the install
|
|
34
|
-
* banner — one drawing, everywhere.
|
|
35
|
-
*
|
|
36
|
-
* @param {string} version - e.g. "7.7.2"
|
|
37
|
-
* @returns {string}
|
|
38
|
-
*/
|
|
39
29
|
export function renderLogo(version) {
|
|
40
30
|
const art = LOGO_LINES.map(
|
|
41
31
|
(line, i) => " " + chalk.hex(GRADIENT[i])(line),
|
|
@@ -56,19 +46,8 @@ export function renderLogo(version) {
|
|
|
56
46
|
return `\n${art}\n${rule}\n`;
|
|
57
47
|
}
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* after an install/upgrade. npm ≥7 swallows lifecycle-script output, so the
|
|
62
|
-
* postinstall banner is invisible on a normal `npm i -g` — this is the
|
|
63
|
-
* fallback that guarantees the next-steps guidance is actually seen.
|
|
64
|
-
*
|
|
65
|
-
* Marker: ~/.claude/morph-spec-banner-version (last version shown).
|
|
66
|
-
* Skips silently when stdout is not a TTY (piped/parsed output) or when
|
|
67
|
-
* MORPH_QUIET=1. Fail-open: any fs error just skips the banner.
|
|
68
|
-
*
|
|
69
|
-
* @param {{version: string, nodeVersion: string, markerDir?: string}} opts
|
|
70
|
-
* @returns {boolean} true if the banner was printed
|
|
71
|
-
*/
|
|
49
|
+
// npm >=7 swallows postinstall output, so the first CLI run after an upgrade
|
|
50
|
+
// shows the banner instead. Skips without a TTY or with MORPH_QUIET=1.
|
|
72
51
|
export function maybePrintFirstRunBanner({ version, nodeVersion, markerDir }) {
|
|
73
52
|
try {
|
|
74
53
|
if (!process.stdout.isTTY) return false;
|
|
@@ -117,7 +96,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
|
|
|
117
96
|
sep("Next · new project"),
|
|
118
97
|
` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
|
|
119
98
|
` ${chalk.cyan("2.")} Run ${chalk.bold.white("morph-spec init")}`,
|
|
120
|
-
` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-
|
|
99
|
+
` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-plan <feature>")}`,
|
|
121
100
|
"",
|
|
122
101
|
sep("Next · project already on morph-spec"),
|
|
123
102
|
` ${chalk.cyan("1.")} Run ${chalk.bold.white("morph-spec update")} ${DIM("— after a CLI upgrade or a project.stack change: full stack-aware resync")}`,
|