@polymorphism-tech/morph-spec 8.38.0 → 8.41.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 +20 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -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 +59 -25
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory Installer — populates Claude Code persistent memory during morph-init
|
|
2
|
+
* Memory Installer — populates Claude Code persistent memory during `morph-spec init`.
|
|
3
3
|
*
|
|
4
4
|
* Writes two memory files (project + reference) plus an entry in MEMORY.md
|
|
5
5
|
* so future Claude Code sessions enter pre-contextualized about the
|
|
@@ -67,7 +67,6 @@ metadata:
|
|
|
67
67
|
- When user mentions a feature, check \`.morph/features/\` first.
|
|
68
68
|
- When user types \`/morph-proposal\`, invoke \`Skill(morph-proposal)\` directly.
|
|
69
69
|
- In \`trust: "auto"\`, gates auto-pass; only AskUserQuestion on risk signals.
|
|
70
|
-
- Prefer morph-* skills over superpowers:* equivalents in this project.
|
|
71
70
|
`;
|
|
72
71
|
}
|
|
73
72
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the morph-spec projects on this machine are — the list the dashboard's
|
|
3
|
+
* portfolio reads. Nothing else records it: each project only knows its own
|
|
4
|
+
* `.morph/`.
|
|
5
|
+
*
|
|
6
|
+
* Three writers register a project as a side note of what they already do —
|
|
7
|
+
* `init`, `update` (never under `--dry-run`) and `dashboard` — and the
|
|
8
|
+
* dashboard adds and removes on request. Registering is fail-open: a registry
|
|
9
|
+
* that cannot be read or written never fails the command that tried.
|
|
10
|
+
*
|
|
11
|
+
* The file lives in the user's home (`~/.morph-spec/projects.json`, or
|
|
12
|
+
* `MORPH_PROJECTS_REGISTRY`), read through `os.homedir()` at call time so a
|
|
13
|
+
* suite that isolates the home also isolates this file. Paths under the
|
|
14
|
+
* system temp dir are never registered: the test suites run `init`/`update`
|
|
15
|
+
* there by the hundred, including through spawned CLIs that keep the real home.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
|
+
import { homedir, tmpdir } from 'os';
|
|
20
|
+
import { join, resolve, basename, dirname } from 'path';
|
|
21
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
22
|
+
import { canonicalPath, isInside } from '../tasks/test-runner.js';
|
|
23
|
+
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
24
|
+
|
|
25
|
+
export const REGISTRY_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
export function registryPath() {
|
|
28
|
+
return process.env.MORPH_PROJECTS_REGISTRY || join(homedir(), '.morph-spec', 'projects.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One spelling per directory: canonical, forward slashes (the registry is read back on any OS). */
|
|
32
|
+
export function projectKey(dir) {
|
|
33
|
+
return canonicalPath(resolve(dir)).replace(/\\/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
37
|
+
|
|
38
|
+
/** A folder is a morph-spec project when morph-spec installed its config there. */
|
|
39
|
+
export function isMorphProject(dir) {
|
|
40
|
+
return existsSync(join(dir, '.morph', 'config', 'config.json'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A worktree checkout is part of its repo's project, never a project of its own. */
|
|
44
|
+
function insideWorktreeContainer(dir) {
|
|
45
|
+
const parent = basename(dirname(dir));
|
|
46
|
+
return WORKTREE_DIR_NAMES.includes(parent);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function projectName(dir) {
|
|
50
|
+
try {
|
|
51
|
+
const config = JSON.parse(readFileSync(join(dir, '.morph', 'config', 'config.json'), 'utf8'));
|
|
52
|
+
const name = config?.project?.name;
|
|
53
|
+
// Some configs carry the project path as its name; the folder reads better.
|
|
54
|
+
return typeof name === 'string' && name.trim() && !/[\\/]/.test(name) ? name.trim() : basename(dir);
|
|
55
|
+
} catch {
|
|
56
|
+
return basename(dir);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The registered projects that still exist. A folder that was deleted or lost
|
|
62
|
+
* its `.morph/` drops out on read — the file is rewritten on the next write.
|
|
63
|
+
*
|
|
64
|
+
* @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
|
|
65
|
+
*/
|
|
66
|
+
export function readRegistry() {
|
|
67
|
+
let data = null;
|
|
68
|
+
try {
|
|
69
|
+
const path = registryPath();
|
|
70
|
+
if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch { /* unreadable registry reads as empty */ }
|
|
72
|
+
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
73
|
+
return {
|
|
74
|
+
version: REGISTRY_VERSION,
|
|
75
|
+
projects: projects.filter((p) => typeof p?.dir === 'string' && isMorphProject(p.dir)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function write(projects) {
|
|
80
|
+
atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
|
|
85
|
+
* Never throws.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} dir
|
|
88
|
+
* @param {'init'|'update'|'dashboard'|'manual'} source
|
|
89
|
+
* @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
|
|
90
|
+
* projects are never registered (the system temp dir)
|
|
91
|
+
* @returns {boolean} true when the project is in the registry afterwards
|
|
92
|
+
*/
|
|
93
|
+
export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
94
|
+
try {
|
|
95
|
+
const key = projectKey(dir);
|
|
96
|
+
if (isInside(canonicalPath(key), canonicalPath(tempRoot))) return false;
|
|
97
|
+
if (!isMorphProject(key) || insideWorktreeContainer(key)) return false;
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const { projects } = readRegistry();
|
|
100
|
+
const found = projects.find((p) => sameDir(p.dir, key));
|
|
101
|
+
if (found) {
|
|
102
|
+
found.lastSeenAt = now;
|
|
103
|
+
found.name = projectName(key);
|
|
104
|
+
} else {
|
|
105
|
+
projects.push({ dir: key, name: projectName(key), addedAt: now, lastSeenAt: now, source });
|
|
106
|
+
}
|
|
107
|
+
write(projects);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Take a project off the list. Its files are not touched. Never throws. */
|
|
115
|
+
export function unregisterProject(dir) {
|
|
116
|
+
try {
|
|
117
|
+
const key = projectKey(dir);
|
|
118
|
+
const { projects } = readRegistry();
|
|
119
|
+
const kept = projects.filter((p) => !sameDir(p.dir, key));
|
|
120
|
+
if (kept.length !== projects.length) write(kept);
|
|
121
|
+
return kept.length !== projects.length;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* morph-spec projects directly under each root that are not registered yet —
|
|
129
|
+
* the portfolio offers them, the person decides. One level deep: a repos
|
|
130
|
+
* folder is the expected root, and a deeper walk would enter node_modules and
|
|
131
|
+
* worktrees for nothing.
|
|
132
|
+
*
|
|
133
|
+
* @param {string[]} roots
|
|
134
|
+
* @returns {Array<{ dir: string, name: string }>}
|
|
135
|
+
*/
|
|
136
|
+
export function discoverProjects(roots) {
|
|
137
|
+
const registered = readRegistry().projects.map((p) => p.dir);
|
|
138
|
+
const out = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const root of roots) {
|
|
141
|
+
let entries = [];
|
|
142
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
143
|
+
for (const e of entries) {
|
|
144
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
145
|
+
const dir = join(root, e.name);
|
|
146
|
+
if (!isMorphProject(dir)) continue;
|
|
147
|
+
const key = projectKey(dir);
|
|
148
|
+
if (seen.has(key.toLowerCase()) || registered.some((r) => sameDir(r, key))) continue;
|
|
149
|
+
seen.add(key.toLowerCase());
|
|
150
|
+
out.push({ dir: key, name: projectName(key) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
154
|
+
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
|
|
28
28
|
* de tela incompleto pausa; no ship, item decidido sem a conferência do
|
|
29
29
|
* avaliador pausa. Mockup legado é isento nos dois, com a razão listada
|
|
30
|
+
* - oversizedTasks + gate: array — tasks acima de MAX_TASK_OUTPUTS outputs;
|
|
31
|
+
* pausa SÓ no plano (gate 'plan'): a task tem de caber num dispatch
|
|
30
32
|
* - evalScore: number — morph-eval score (pause if < 9)
|
|
31
33
|
* - taskCount: number — total tasks (pause if > 20)
|
|
32
34
|
*/
|
|
@@ -425,6 +427,14 @@ export function shouldPauseGate(config, signals) {
|
|
|
425
427
|
};
|
|
426
428
|
}
|
|
427
429
|
|
|
430
|
+
if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
|
|
431
|
+
const list = signals.oversizedTasks.map((t) => `${t.id} (${t.outputs})`).join(', ');
|
|
432
|
+
return {
|
|
433
|
+
pause: true,
|
|
434
|
+
reason: `task grande demais para um dispatch: ${list} outputs — fatie por pós-condição`,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
428
438
|
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
429
439
|
return {
|
|
430
440
|
pause: true,
|
|
@@ -498,6 +508,9 @@ export function getRiskSignals(signals) {
|
|
|
498
508
|
out.push(`prompt/modelo tocado (${touched.length}) com eval ${signals.evalsStatus ?? 'não declarado'}`);
|
|
499
509
|
}
|
|
500
510
|
}
|
|
511
|
+
if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
|
|
512
|
+
out.push(`tasks grandes demais: ${signals.oversizedTasks.map((t) => t.id).join(', ')}`);
|
|
513
|
+
}
|
|
501
514
|
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
502
515
|
out.push(`eval score ${signals.evalScore}/10`);
|
|
503
516
|
}
|
|
@@ -193,6 +193,27 @@ export function parseTasksJsonSync(filePath) {
|
|
|
193
193
|
return data;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Teto de `outputs` por task: a task inteira tem de caber num dispatch. Medido
|
|
198
|
+
* em campo (chat-demo-simulacao): tasks de 12 a 30 outputs levaram 6 de 10
|
|
199
|
+
* sub-agents ao teto de turnos, e cada retomada custou um contexto frio.
|
|
200
|
+
*/
|
|
201
|
+
export const MAX_TASK_OUTPUTS = 8;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A régua única de "task grande demais", lida pelo `dag` e pelo `gate-check`.
|
|
205
|
+
* Fora de `validateTasksJson` de propósito: `outputs` segue mutável depois do
|
|
206
|
+
* Gate 2, e o teto num write de status travaria feature em voo.
|
|
207
|
+
*
|
|
208
|
+
* @returns {{ id: string, outputs: number }[]}
|
|
209
|
+
*/
|
|
210
|
+
export function oversizedTasks(data) {
|
|
211
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
212
|
+
return tasks
|
|
213
|
+
.filter(t => Array.isArray(t?.outputs) && t.outputs.length > MAX_TASK_OUTPUTS)
|
|
214
|
+
.map(t => ({ id: t.id, outputs: t.outputs.length }));
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
/**
|
|
197
218
|
* @returns {object[]} tasks with status === 'pending'
|
|
198
219
|
*/
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* (a) linhas de comentário que citam processo (task, UC, ADR, Gate...), que o
|
|
6
6
|
* standard `architecture-code-comments` manda para o commit;
|
|
7
7
|
* (b) linhas de teste ÷ linhas de produção, contra o orçamento por task do
|
|
8
|
-
* `morph-plan` (um teste por pós-condição + um de regressão por bug)
|
|
8
|
+
* `morph-plan` (um teste por pós-condição + um de regressão por bug);
|
|
9
|
+
* (c) linhas de comentário ÷ linhas de código, porque o padrão do standard é
|
|
10
|
+
* nenhum comentário. Doc-comment (`///`, JSDoc) conta como qualquer outro.
|
|
9
11
|
*
|
|
10
12
|
* Sempre `warning`: é medida de volume, e o excesso pode estar justificado na
|
|
11
13
|
* task. Um aviso por medida, não por linha, porque cada aviso custa 1 ponto no
|
|
@@ -28,6 +30,12 @@ export const MAX_TEST_RATIO = 1.5;
|
|
|
28
30
|
/** Abaixo disto a razão não é julgada: a regressão de um bug de 2 linhas cabe aqui. */
|
|
29
31
|
export const MIN_TEST_LINES = 40;
|
|
30
32
|
|
|
33
|
+
/** Mais de uma linha de comentário a cada dez de código: os nomes não estão fazendo o trabalho. */
|
|
34
|
+
export const MAX_COMMENT_RATIO = 0.1;
|
|
35
|
+
|
|
36
|
+
/** Abaixo disto a densidade não é julgada: um comentário legítimo num arquivo pequeno já passaria da razão. */
|
|
37
|
+
export const MIN_CODE_LINES = 40;
|
|
38
|
+
|
|
31
39
|
/** Quantos locais citar no aviso de comentário antes de resumir com "(+N)". */
|
|
32
40
|
const MAX_CITED = 4;
|
|
33
41
|
|
|
@@ -42,10 +50,10 @@ const decimal = (n) => n.toFixed(1).replace('.', ',');
|
|
|
42
50
|
* @param {Array<{path: string, content: string, added: number[]|null}>} files -
|
|
43
51
|
* `added` são as linhas adicionadas; `null` é o arquivo inteiro (novo)
|
|
44
52
|
* @returns {{commentLines: number, processLines: Array<{file: string, line: number, text: string}>,
|
|
45
|
-
* testLines: number, productionLines: number}}
|
|
53
|
+
* testLines: number, productionLines: number, commentsByFile: Record<string, number>}}
|
|
46
54
|
*/
|
|
47
55
|
export function measureCodeBudget(files) {
|
|
48
|
-
const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0 };
|
|
56
|
+
const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0, commentsByFile: {} };
|
|
49
57
|
|
|
50
58
|
for (const { path, content, added } of files) {
|
|
51
59
|
if (!isProjectSource(path) || GENERATED.test(path)) continue;
|
|
@@ -60,6 +68,7 @@ export function measureCodeBudget(files) {
|
|
|
60
68
|
for (const [line, text] of commentLines(content, path)) {
|
|
61
69
|
if (!isAdded(line)) continue;
|
|
62
70
|
measured.commentLines++;
|
|
71
|
+
measured.commentsByFile[path] = (measured.commentsByFile[path] ?? 0) + 1;
|
|
63
72
|
if (citesProcess(text)) measured.processLines.push({ file: path, line, text });
|
|
64
73
|
}
|
|
65
74
|
}
|
|
@@ -73,7 +82,7 @@ export function measureCodeBudget(files) {
|
|
|
73
82
|
*/
|
|
74
83
|
export function budgetIssues(measured) {
|
|
75
84
|
const issues = [];
|
|
76
|
-
const { commentLines: comments, processLines, testLines, productionLines } = measured;
|
|
85
|
+
const { commentLines: comments, processLines, testLines, productionLines, commentsByFile = {} } = measured;
|
|
77
86
|
|
|
78
87
|
if (processLines.length > 0) {
|
|
79
88
|
const shown = processLines.slice(0, MAX_CITED).map((p) => `${p.file}:${p.line}`);
|
|
@@ -104,6 +113,25 @@ export function budgetIssues(measured) {
|
|
|
104
113
|
});
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
const codeLines = testLines + productionLines - comments;
|
|
117
|
+
if (codeLines >= MIN_CODE_LINES && comments > MAX_COMMENT_RATIO * codeLines) {
|
|
118
|
+
const ranked = Object.entries(commentsByFile).sort(([, a], [, b]) => b - a);
|
|
119
|
+
const shown = ranked.slice(0, MAX_CITED).map(([file, n]) => `${file} (${n})`);
|
|
120
|
+
const more = ranked.length - shown.length;
|
|
121
|
+
issues.push({
|
|
122
|
+
level: 'warning',
|
|
123
|
+
message:
|
|
124
|
+
`${comments} linhas de comentário para ${codeLines} de código adicionadas ` +
|
|
125
|
+
`(${decimal((comments / codeLines) * 10)} a cada 10, o teto é ${decimal(MAX_COMMENT_RATIO * 10)}): ` +
|
|
126
|
+
`${shown.join(', ')}${more > 0 ? ` (+${more})` : ''}`,
|
|
127
|
+
file: ranked[0]?.[0],
|
|
128
|
+
solution:
|
|
129
|
+
'O padrão é nenhum comentário: renomeie, extraia função ou constante, expresse no tipo ou ' +
|
|
130
|
+
'escreva um teste. Fica só o porquê que o código não mostra, sem doc-comment que repete a ' +
|
|
131
|
+
'assinatura (standard architecture-code-comments).',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
return issues;
|
|
108
136
|
}
|
|
109
137
|
|
|
@@ -191,16 +191,22 @@ export function portLayout(block, { stack = '', feature = '' } = {}) {
|
|
|
191
191
|
* feature, and exactly the `portLayout()` pairs — nothing else, so the file
|
|
192
192
|
* cannot drift from what compose receives.
|
|
193
193
|
*
|
|
194
|
-
*
|
|
194
|
+
* `morphFeature` is for a task tree born from `morph/<f>`: `MORPH_FEATURE`
|
|
195
|
+
* names the feature it works for, while the block and `COMPOSE_PROJECT_NAME`
|
|
196
|
+
* stay keyed by `feature` (`task/<n>`) — keying them by `<f>` would hand the
|
|
197
|
+
* sub-agent the feature's own ports and compose project.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} feature - the env-registry key: `<feature>` or `task/<name>`
|
|
195
200
|
* @param {{ base: number, range: [number, number] }} block
|
|
196
201
|
* @param {string} [stack] - config.json `project.stack` (e.g. "nextjs+dotnet")
|
|
202
|
+
* @param {{ morphFeature?: string|null }} [opts]
|
|
197
203
|
* @returns {string}
|
|
198
204
|
*/
|
|
199
|
-
export function buildEnvFileContent(feature, block, stack = '') {
|
|
205
|
+
export function buildEnvFileContent(feature, block, stack = '', { morphFeature = null } = {}) {
|
|
200
206
|
const lines = [
|
|
201
207
|
`# Managed by morph-spec — ambiente dedicado desta worktree (feature: ${feature}).`,
|
|
202
208
|
'# Derivado, git-invisível, regenerado por `worktree setup|link`; nunca editar nem commitar.',
|
|
203
|
-
`MORPH_FEATURE=${feature}`,
|
|
209
|
+
`MORPH_FEATURE=${morphFeature || feature}`,
|
|
204
210
|
...Object.entries(portLayout(block, { stack, feature })).map(([k, v]) => `${k}=${v}`),
|
|
205
211
|
];
|
|
206
212
|
return `${lines.join('\n')}\n`;
|
|
@@ -100,7 +100,7 @@ async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* Install a Claude Code plugin from GitHub.
|
|
103
|
-
* @param {string} pluginName - e.g. "
|
|
103
|
+
* @param {string} pluginName - e.g. "context7"
|
|
104
104
|
* @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
|
|
105
105
|
*/
|
|
106
106
|
export async function installPlugin(pluginName) {
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* setup-infra.js
|
|
3
3
|
*
|
|
4
4
|
* Headless infrastructure setup for MORPH-SPEC.
|
|
5
|
-
* Extracted from init.js (steps 1-10) so it can
|
|
6
|
-
*
|
|
7
|
-
* interactive prompts or stack detection.
|
|
5
|
+
* Extracted from init.js (steps 1-10) so it can run as
|
|
6
|
+
* `morph-spec setup-infra` without interactive prompts or stack detection.
|
|
8
7
|
*
|
|
9
8
|
* Usage:
|
|
10
9
|
* import { setupInfra } from './setup-infra.js';
|
|
@@ -38,7 +37,6 @@ import { realEffects } from '../lib/installers/effects.js';
|
|
|
38
37
|
const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
|
39
38
|
|
|
40
39
|
const REQUIRED_PLUGINS = [
|
|
41
|
-
'superpowers@claude-plugins-official',
|
|
42
40
|
'context7@claude-plugins-official',
|
|
43
41
|
];
|
|
44
42
|
|
|
@@ -102,6 +102,35 @@ export function resolveMaxTurns(agent) {
|
|
|
102
102
|
return agent.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Values Claude Code accepts in a subagent's `effort` frontmatter. */
|
|
106
|
+
export const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
107
|
+
|
|
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
|
+
*/
|
|
117
|
+
export function effortProblem(value) {
|
|
118
|
+
if (value === undefined) return null;
|
|
119
|
+
return VALID_EFFORTS.includes(value) ? null : `esperado um de ${VALID_EFFORTS.join('|')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
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
|
+
export function effortError(id, value) {
|
|
128
|
+
const problem = effortProblem(value);
|
|
129
|
+
if (!problem) return null;
|
|
130
|
+
return `agents.json: persona "${id}" tem effort inválido (${JSON.stringify(value)}) — ${problem}. ` +
|
|
131
|
+
'Remova o campo para herdar o effort da sessão.';
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
/** A surface id is the key the mockup checklist names (`Superfície: admin-app`). */
|
|
106
135
|
const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
107
136
|
|
|
@@ -192,23 +221,69 @@ export function personaStandardIds(persona) {
|
|
|
192
221
|
|
|
193
222
|
/** The first validity error of a persona entry, or null — what drift reports as `invalid`. */
|
|
194
223
|
function personaError(agent) {
|
|
195
|
-
return maxTurnsError(agent.id, agent.maxTurns)
|
|
224
|
+
return maxTurnsError(agent.id, agent.maxTurns)
|
|
225
|
+
?? surfacesError(agent.id, agent.surfaces, agent.standards)
|
|
226
|
+
?? effortError(agent.id, agent.effort);
|
|
227
|
+
}
|
|
228
|
+
|
|
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
|
+
*/
|
|
234
|
+
export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The server names an MCP registry entry is granted under. Claude Code names a
|
|
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
|
+
*/
|
|
248
|
+
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
|
+
return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
|
|
196
252
|
}
|
|
197
253
|
|
|
198
254
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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
|
+
*/
|
|
264
|
+
export function serverNamesProblem(id, value) {
|
|
265
|
+
if (value === undefined) return null;
|
|
266
|
+
if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
|
|
267
|
+
const bad = value.find(n => typeof n !== 'string' || !/^[A-Za-z0-9_-]+$/.test(n));
|
|
268
|
+
if (bad !== undefined) return `nome inválido ${JSON.stringify(bad)} (só [A-Za-z0-9_-])`;
|
|
269
|
+
return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
|
|
270
|
+
}
|
|
271
|
+
|
|
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).
|
|
202
277
|
*
|
|
203
278
|
* @param {string} frameworkDir
|
|
204
|
-
* @returns {
|
|
279
|
+
* @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
|
|
205
280
|
*/
|
|
206
281
|
export function loadKnownMcpIds(frameworkDir) {
|
|
207
282
|
try {
|
|
208
283
|
const registryPath = join(frameworkDir, 'mcp', 'registry.json');
|
|
209
284
|
if (!existsSync(registryPath)) return null;
|
|
210
285
|
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
211
|
-
return new
|
|
286
|
+
return new Map(Object.entries(registry.mcps ?? {}).map(([id, entry]) => [id, mcpServerNames(id, entry)]));
|
|
212
287
|
} catch {
|
|
213
288
|
return null;
|
|
214
289
|
}
|
|
@@ -321,8 +396,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
321
396
|
* stale file differs byte-for-byte from this output).
|
|
322
397
|
*
|
|
323
398
|
* @param {object} agent - agents.json entry with `id` merged in
|
|
324
|
-
* @param {
|
|
325
|
-
* `mcp` entries outside
|
|
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)
|
|
326
401
|
* @returns {string} complete file content
|
|
327
402
|
*/
|
|
328
403
|
export function renderAgentMd(agent, knownMcpIds = null) {
|
|
@@ -332,20 +407,23 @@ export function renderAgentMd(agent, knownMcpIds = null) {
|
|
|
332
407
|
const maxTurns = resolveMaxTurns(agent);
|
|
333
408
|
const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
|
|
334
409
|
if (surfaces) throw new Error(surfaces);
|
|
410
|
+
const effort = effortError(agent.id, agent.effort);
|
|
411
|
+
if (effort) throw new Error(effort);
|
|
335
412
|
const frontmatter = buildFrontmatter(agent, description, knownMcpIds, maxTurns);
|
|
336
413
|
const body = buildBody(agent, maxTurns);
|
|
337
414
|
return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
|
|
338
415
|
}
|
|
339
416
|
|
|
340
417
|
function buildToolsLine(agent, knownMcpIds) {
|
|
341
|
-
const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
|
|
418
|
+
const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
|
|
419
|
+
const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
|
|
342
420
|
// Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
|
|
343
|
-
// grants (`mcp__{
|
|
344
|
-
// without that server configured the grant is inert.
|
|
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.
|
|
345
423
|
const mcpGrants = (agent.mcp ?? [])
|
|
346
424
|
.filter(id => knownMcpIds?.has(id))
|
|
347
|
-
.
|
|
348
|
-
return [base, ...mcpGrants].join(', ');
|
|
425
|
+
.flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
|
|
426
|
+
return [...base, ...platform, ...mcpGrants].join(', ');
|
|
349
427
|
}
|
|
350
428
|
|
|
351
429
|
function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
@@ -364,6 +442,7 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
|
364
442
|
// exactly what the hardcoded `maxTurns: 20` rendered.
|
|
365
443
|
`maxTurns: ${maxTurns}`,
|
|
366
444
|
];
|
|
445
|
+
if (agent.effort) lines.push(`effort: ${agent.effort}`);
|
|
367
446
|
if (agent.color) lines.push(`color: ${agent.color}`);
|
|
368
447
|
lines.push(`memory: project`, '');
|
|
369
448
|
return lines.join('\n');
|
|
@@ -430,6 +509,22 @@ function buildBody(agent, maxTurns) {
|
|
|
430
509
|
'`PARCIAL` on line 1, done, remaining, files, no score (final report only) — then continue or stop, your call; the final report replaces it.',
|
|
431
510
|
);
|
|
432
511
|
|
|
512
|
+
// The dispatch template is paraphrased by the orchestrator in the field; this
|
|
513
|
+
// body is the only text every implementer dispatch is guaranteed to carry.
|
|
514
|
+
// Gated on `effort` so a persona without it renders byte-for-byte as before.
|
|
515
|
+
if (agent.effort && isImplementer(agent)) {
|
|
516
|
+
lines.push(
|
|
517
|
+
'',
|
|
518
|
+
'## Working fast',
|
|
519
|
+
'',
|
|
520
|
+
'Independent tool calls go in the SAME response: read every listed file at once, fire the Greps together. ' +
|
|
521
|
+
'Use Read/Grep/Glob, never grep/sed/cat/find via Bash. Check a library API in the `context7` MCP before guessing ' +
|
|
522
|
+
'(load a deferred `mcp__` tool with ToolSearch). Write the tests the budget asks for, then run only your own ' +
|
|
523
|
+
'filter ONCE before the report — no red→green cycles, no `morph-spec verify` (the orchestrator proves it). ' +
|
|
524
|
+
'Never alter, revert or move production code to watch a test fail: mutation proof belongs to the Gate 3 evaluator.',
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
433
528
|
return lines.join('\n');
|
|
434
529
|
}
|
|
435
530
|
|
package/src/utils/banner.js
CHANGED
|
@@ -117,7 +117,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
|
|
|
117
117
|
sep("Next · new project"),
|
|
118
118
|
` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
|
|
119
119
|
` ${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-
|
|
120
|
+
` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-proposal <feature>")}`,
|
|
121
121
|
"",
|
|
122
122
|
sep("Next · project already on morph-spec"),
|
|
123
123
|
` ${chalk.cyan("1.")} Run ${chalk.bold.white("morph-spec update")} ${DIM("— after a CLI upgrade or a project.stack change: full stack-aware resync")}`,
|
package/src/commands/index.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* All Commands - Category Exports
|
|
3
|
-
*
|
|
4
|
-
* Commands organized by functional category.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export * from './state/index.js';
|
|
8
|
-
export * from './validation/index.js';
|
|
9
|
-
export * from './templates/index.js';
|
|
10
|
-
export * from './project/index.js';
|
|
11
|
-
export * from './tasks/index.js';
|
|
12
|
-
export * from './agents/index.js';
|
|
13
|
-
export * from './utils/index.js';
|