@polymorphism-tech/morph-spec 7.9.4 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/README.md +31 -2
- package/bin/morph-spec.js +50 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +38 -1
- package/docs/QUICKSTART.md +60 -1
- package/framework/CLAUDE.md +14 -3
- package/framework/MORPH.md +41 -5
- package/framework/agents.json +22 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-hotfix.md +39 -0
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/hooks/README.md +25 -11
- package/framework/hooks/claude-code/core/gate-guard.js +4 -2
- package/framework/hooks/claude-code/core/loop-detect.js +2 -1
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
- package/framework/hooks/claude-code/core/validate-completion.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +160 -0
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/build-check.js +2 -2
- package/framework/hooks/shared/hook-response.js +17 -5
- package/framework/hooks/shared/payload-utils.js +42 -0
- package/framework/hooks/shared/state-reader.js +22 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/schemas/feature.schema.json +95 -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 +49 -27
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
- package/framework/skills/core/morph-eval/SKILL.md +6 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +134 -0
- package/framework/skills/core/morph-implement/SKILL.md +17 -22
- package/framework/skills/core/morph-plan/SKILL.md +5 -1
- package/framework/skills/core/morph-proposal/SKILL.md +24 -1
- package/framework/skills/core/morph-review/SKILL.md +13 -16
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +0 -62
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- 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/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- 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/README.md +56 -56
- 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/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- 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/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- 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/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +4 -2
- package/src/commands/project/init.js +46 -2
- package/src/commands/project/update.js +5 -3
- package/src/commands/project/worktree.js +1005 -46
- package/src/commands/state/advance.js +17 -2
- package/src/commands/state/approve.js +18 -1
- package/src/commands/state/create.js +158 -14
- package/src/commands/state/retype.js +126 -0
- package/src/commands/state/score.js +4 -0
- package/src/commands/tasks/dag.js +32 -0
- package/src/commands/validation/index.js +1 -0
- package/src/commands/validation/validate-feature.js +33 -5
- package/src/commands/validation/verify.js +190 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +35 -10
- package/src/core/templates/template-renderer.js +477 -477
- package/src/core/work-types.js +79 -0
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +11 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/scanner/state-scanner.js +42 -0
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/risk-detector.js +16 -0
- package/src/lib/tasks/dispatch-planner.js +122 -4
- package/src/lib/tasks/mermaid-renderer.js +132 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +27 -6
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/lib/validators/verify-runner.js +322 -0
- package/src/lib/work-type-detector.js +143 -0
- package/src/scripts/setup-infra.js +32 -5
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +10 -6
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +43 -0
- package/src/utils/hooks-installer.js +43 -34
- package/src/utils/skills-installer.js +174 -174
- package/framework/hooks/claude-code/core/subagent-report-guard.js +0 -95
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
- package/src/lib/tasks/wave-resolver.js +0 -353
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify Runner — deterministic build+tests+validators cadence.
|
|
3
|
+
*
|
|
4
|
+
* v8 adds a DETERMINISTIC verification node that runs BEFORE the LLM judge
|
|
5
|
+
* in the per-task loop: build + tests + validators (code, free) first; the
|
|
6
|
+
* LLM judge is only invoked once verify passes. This is a BINARY gate
|
|
7
|
+
* (pass/fail per node), NOT a second "bar 9" — the bar 9 remains morph-eval's.
|
|
8
|
+
*
|
|
9
|
+
* Pure orchestration — reuses (never reimplements):
|
|
10
|
+
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
11
|
+
* - runTestSuite() from src/lib/tasks/test-runner.js
|
|
12
|
+
* - runValidation() from ./validation-runner.js
|
|
13
|
+
* - loadRubric() + aggregateScores() from ./score-aggregator.js
|
|
14
|
+
*
|
|
15
|
+
* @module verify-runner
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
|
|
21
|
+
import { runBuildCheck, getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
|
|
22
|
+
import { runTestSuite, resolveTestCommand } from '../tasks/test-runner.js';
|
|
23
|
+
import { runValidation } from './validation-runner.js';
|
|
24
|
+
import { loadRubric, aggregateScores } from './score-aggregator.js';
|
|
25
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
26
|
+
|
|
27
|
+
/** Default build timeout — dotnet cold build is slow; 30s (the Stop-hook default) is too short. */
|
|
28
|
+
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
29
|
+
|
|
30
|
+
/** Number of trailing output lines kept for feedback/reporting. */
|
|
31
|
+
const OUTPUT_TAIL_LINES = 20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run the deterministic verify cadence for a feature (or a single task within it).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} projectPath - Project root directory
|
|
37
|
+
* @param {string} featureName - Feature name
|
|
38
|
+
* @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
|
|
39
|
+
* @param {Object} [options]
|
|
40
|
+
* @param {string} [options.rubric] - Force a rubric id (highest precedence)
|
|
41
|
+
* @param {number} [options.timeout] - Build timeout in ms (default 120000)
|
|
42
|
+
* @param {boolean} [options.skipTests] - Skip the tests node
|
|
43
|
+
* @param {boolean} [options.skipBuild] - Skip the build node
|
|
44
|
+
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
45
|
+
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
46
|
+
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
47
|
+
* @returns {Promise<Object>} Aggregated verify result — see module docs for shape
|
|
48
|
+
*/
|
|
49
|
+
export async function runVerify(projectPath, featureName, taskId, options = {}) {
|
|
50
|
+
const {
|
|
51
|
+
rubric: rubricOverride,
|
|
52
|
+
timeout = DEFAULT_BUILD_TIMEOUT,
|
|
53
|
+
skipTests = false,
|
|
54
|
+
skipBuild = false,
|
|
55
|
+
_runBuildCheck = runBuildCheck,
|
|
56
|
+
_runTestSuite = runTestSuite,
|
|
57
|
+
_runValidation = runValidation,
|
|
58
|
+
} = options;
|
|
59
|
+
|
|
60
|
+
const stack = readStack(projectPath);
|
|
61
|
+
|
|
62
|
+
const nodes = {};
|
|
63
|
+
const feedback = [];
|
|
64
|
+
|
|
65
|
+
// 1. build
|
|
66
|
+
nodes.build = await runBuildNode({ projectPath, stack, timeout, skipBuild, _runBuildCheck });
|
|
67
|
+
appendBuildFeedback(feedback, nodes.build);
|
|
68
|
+
|
|
69
|
+
// 2. tests — skipped entirely (no invocation) when build already failed.
|
|
70
|
+
nodes.tests = await runTestsNode({ projectPath, skipTests, buildNode: nodes.build, _runTestSuite });
|
|
71
|
+
appendTestsFeedback(feedback, nodes.tests);
|
|
72
|
+
|
|
73
|
+
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
74
|
+
const rubricId = resolveRubricId({ projectPath, featureName, taskId, rubricOverride });
|
|
75
|
+
nodes.validators = await runValidatorsNode({ projectPath, featureName, rubricId, _runValidation });
|
|
76
|
+
appendValidatorsFeedback(feedback, nodes.validators);
|
|
77
|
+
|
|
78
|
+
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
feature: featureName,
|
|
83
|
+
task: taskId || null,
|
|
84
|
+
scope: taskId ? 'task' : 'feature',
|
|
85
|
+
stack,
|
|
86
|
+
rubric: rubricId,
|
|
87
|
+
status,
|
|
88
|
+
nodes,
|
|
89
|
+
feedback,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Build node
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
async function runBuildNode({ projectPath, stack, timeout, skipBuild, _runBuildCheck }) {
|
|
98
|
+
if (skipBuild) {
|
|
99
|
+
return { status: 'skip', command: null, durationMs: 0, reason: 'requested', output: '' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
|
|
103
|
+
const command = getBuildCommand(stack, hasTsConfig);
|
|
104
|
+
if (!command) {
|
|
105
|
+
const reason = stack ? `no build command for stack "${stack}"` : 'no stack configured';
|
|
106
|
+
return { status: 'skip', command: null, durationMs: 0, reason, output: '' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
const result = await _runBuildCheck(projectPath, stack, timeout);
|
|
111
|
+
const durationMs = Date.now() - start;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
status: result.status,
|
|
115
|
+
command,
|
|
116
|
+
durationMs,
|
|
117
|
+
reason: result.status === 'skip' ? 'no build command for stack' : null,
|
|
118
|
+
output: result.output || '',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appendBuildFeedback(feedback, buildNode) {
|
|
123
|
+
if (buildNode.status !== 'fail') return;
|
|
124
|
+
for (const line of linesOf(buildNode.output)) {
|
|
125
|
+
feedback.push(`[build] ${line}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Tests node
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }) {
|
|
134
|
+
const command = resolveTestCommand(projectPath);
|
|
135
|
+
|
|
136
|
+
if (skipTests) {
|
|
137
|
+
return { status: 'skip', command, durationMs: 0, reason: 'requested', output: '' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (buildNode.status === 'fail') {
|
|
141
|
+
return {
|
|
142
|
+
status: 'skip',
|
|
143
|
+
command,
|
|
144
|
+
durationMs: 0,
|
|
145
|
+
reason: 'build failed — test run would only add noise',
|
|
146
|
+
output: '',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const start = Date.now();
|
|
151
|
+
const result = await _runTestSuite(projectPath);
|
|
152
|
+
const durationMs = Date.now() - start;
|
|
153
|
+
|
|
154
|
+
if (result.skipped) {
|
|
155
|
+
return {
|
|
156
|
+
status: 'skip',
|
|
157
|
+
command,
|
|
158
|
+
durationMs,
|
|
159
|
+
reason: result.reason || 'no test command detected',
|
|
160
|
+
output: '',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
status: result.passed ? 'pass' : 'fail',
|
|
166
|
+
command,
|
|
167
|
+
durationMs,
|
|
168
|
+
reason: null,
|
|
169
|
+
output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendTestsFeedback(feedback, testsNode) {
|
|
174
|
+
if (testsNode.status !== 'fail') return;
|
|
175
|
+
for (const line of linesOf(testsNode.output)) {
|
|
176
|
+
feedback.push(`[tests] ${line}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Validators node
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
async function runValidatorsNode({ projectPath, featureName, rubricId, _runValidation }) {
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
|
|
187
|
+
// runValidation() prints chalk-formatted progress to stdout. verify emits
|
|
188
|
+
// JSON — silence console during the call and ALWAYS restore it, even if
|
|
189
|
+
// runValidation throws.
|
|
190
|
+
const originalLog = console.log;
|
|
191
|
+
const originalWarn = console.warn;
|
|
192
|
+
let validationResult;
|
|
193
|
+
try {
|
|
194
|
+
console.log = () => {};
|
|
195
|
+
console.warn = () => {};
|
|
196
|
+
validationResult = await _runValidation(projectPath, featureName, { mode: 'full' });
|
|
197
|
+
} finally {
|
|
198
|
+
console.log = originalLog;
|
|
199
|
+
console.warn = originalWarn;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const durationMs = Date.now() - start;
|
|
203
|
+
|
|
204
|
+
if (validationResult && validationResult._skipReason) {
|
|
205
|
+
return {
|
|
206
|
+
status: 'skip',
|
|
207
|
+
score: null,
|
|
208
|
+
action: null,
|
|
209
|
+
ran: [],
|
|
210
|
+
reason: validationResult._skipReason,
|
|
211
|
+
priorityFixes: [],
|
|
212
|
+
durationMs,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rubric = loadRubric(projectPath, rubricId);
|
|
217
|
+
const scoreResult = aggregateScores(validationResult, rubric);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
status: validationResult.passed === false ? 'fail' : 'pass',
|
|
221
|
+
score: scoreResult.score,
|
|
222
|
+
action: scoreResult.action,
|
|
223
|
+
ran: Object.keys(validationResult.results || {}),
|
|
224
|
+
reason: null,
|
|
225
|
+
priorityFixes: scoreResult.priorityFixes || [],
|
|
226
|
+
durationMs,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
231
|
+
for (const fix of validatorsNode.priorityFixes || []) {
|
|
232
|
+
const topIssue = fix.issues && fix.issues[0];
|
|
233
|
+
if (!topIssue) continue;
|
|
234
|
+
const location = topIssue.file
|
|
235
|
+
? ` (${topIssue.file}${topIssue.line ? ':' + topIssue.line : ''})`
|
|
236
|
+
: '';
|
|
237
|
+
feedback.push(`[validators/${fix.category} ${fix.score}/10] ${topIssue.message}${location}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Rubric selection — flag > task group > config > default
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
function resolveRubricId({ projectPath, featureName, taskId, rubricOverride }) {
|
|
246
|
+
if (rubricOverride) return rubricOverride;
|
|
247
|
+
|
|
248
|
+
if (taskId) {
|
|
249
|
+
const group = readTaskGroup(projectPath, featureName, taskId);
|
|
250
|
+
const mapped = mapGroupToRubric(group);
|
|
251
|
+
if (mapped) return mapped;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const configRubric = readConfigRubric(projectPath);
|
|
255
|
+
if (configRubric) return configRubric;
|
|
256
|
+
|
|
257
|
+
return 'default';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Map a task's `group` field to a rubric id.
|
|
262
|
+
* backend|infra → 'backend', frontend → 'frontend', tests|docs|absent → null
|
|
263
|
+
* (falls through to config/default).
|
|
264
|
+
*/
|
|
265
|
+
function mapGroupToRubric(group) {
|
|
266
|
+
if (!group) return null;
|
|
267
|
+
const g = String(group).toLowerCase();
|
|
268
|
+
if (g === 'backend' || g === 'infra') return 'backend';
|
|
269
|
+
if (g === 'frontend') return 'frontend';
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readTaskGroup(projectPath, featureName, taskId) {
|
|
274
|
+
try {
|
|
275
|
+
const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
|
|
276
|
+
if (!existsSync(tasksPath)) return null;
|
|
277
|
+
const tasksJson = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
278
|
+
const task = (tasksJson.tasks || []).find((t) => t.id === taskId);
|
|
279
|
+
return task?.group ?? null;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readConfigRubric(projectPath) {
|
|
286
|
+
try {
|
|
287
|
+
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
288
|
+
if (!existsSync(configPath)) return null;
|
|
289
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
290
|
+
return config?.validation?.rubric ?? null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Shared helpers
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
/** Read the project stack directly from config.json — never the stack-profile
|
|
301
|
+
* default (which falls back to 'unknown'→dotnet and would wrongly fire
|
|
302
|
+
* `dotnet build` on a greenfield JS project). */
|
|
303
|
+
function readStack(projectPath) {
|
|
304
|
+
try {
|
|
305
|
+
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
306
|
+
if (!existsSync(configPath)) return null;
|
|
307
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
308
|
+
return config?.project?.stack ?? null;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function linesOf(text) {
|
|
315
|
+
const trimmed = (text || '').trim();
|
|
316
|
+
return trimmed ? trimmed.split('\n') : [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function tailLines(text, n) {
|
|
320
|
+
const lines = linesOf(text);
|
|
321
|
+
return lines.slice(-n).join('\n');
|
|
322
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Type Detector — deterministic keyword classifier for `text`/`declaredScope`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors risk-detector.js's style: pure, deterministic, no I/O. This module
|
|
5
|
+
* is the TESTABLE MIRROR of the natural-language classification rules that
|
|
6
|
+
* live in MORPH.md — behavior described here must stay in sync with those
|
|
7
|
+
* rules, but this file is what `create`/`retype` call at runtime.
|
|
8
|
+
*
|
|
9
|
+
* Precedence: hotfix keywords win outright (a production incident never
|
|
10
|
+
* ties down against a bug or chore signal). Otherwise bug vs. chore is
|
|
11
|
+
* decided by keyword-match count, nudged by two weak signals:
|
|
12
|
+
* - estimatedFiles <= 2 -> +1 toward bug (small, surgical diffs)
|
|
13
|
+
* - declaredScope <= 6 words -> +1 toward chore (a narrowly-scoped ask)
|
|
14
|
+
* A genuine tie between bug and chore is surfaced to the caller (LLM/HCOP
|
|
15
|
+
* router) rather than guessed at. Zero keyword matches -> no opinion; the
|
|
16
|
+
* caller defaults to `feature`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Plural/inflected forms are listed EXPLICITLY: matching is whole-word, so
|
|
20
|
+
// "incidentes" is not "incidente". Recall lives here, in a list that is visible
|
|
21
|
+
// and tested, rather than in a matcher that would fire on fragments.
|
|
22
|
+
const HOTFIX_KEYWORDS = [
|
|
23
|
+
'hotfix', 'produção quebrada', 'prod down', 'produção caiu', 'incident',
|
|
24
|
+
'incidents', 'incidente', 'incidentes', 'urgente', 'urgência', 'p0', 'outage',
|
|
25
|
+
'caiu em produção',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const BUG_KEYWORDS = [
|
|
29
|
+
'bug', 'bugs', 'fix', 'corrigir', 'corrige', 'broken', 'quebrado', 'quebrada',
|
|
30
|
+
'regressão', 'regressao', 'exception', 'exceptions', 'stack trace',
|
|
31
|
+
'não funciona', 'nao funciona', 'erro', 'erros',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const CHORE_KEYWORDS = [
|
|
35
|
+
'chore', 'typo', 'typos', 'rename', 'renomear', 'bump', 'dependência',
|
|
36
|
+
'dependencia', 'dependências', 'dependencias', 'config', 'configs', 'cleanup',
|
|
37
|
+
'limpeza', 'docs', 'documentação', 'lint', 'formatação',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @returns {string[]} the subset of `keywords` present in `haystack` as WHOLE
|
|
42
|
+
* words (or whole phrases), never as a fragment of a larger word.
|
|
43
|
+
*
|
|
44
|
+
* Substring matching routed real requests to the wrong pipeline: `p0` is two
|
|
45
|
+
* characters with outright hotfix precedence, so "endpoint group0" matched it
|
|
46
|
+
* and skipped Gates 1 and 2 by policy — no human, and not even the LLM router,
|
|
47
|
+
* since a substring hit reports `confidence: 'high'`. Same shape for `fix`
|
|
48
|
+
* inside "prefix"/"fixtures" and `bug` inside "debug".
|
|
49
|
+
*
|
|
50
|
+
* The failure directions are asymmetric, which is what settles the trade-off: a
|
|
51
|
+
* false POSITIVE silently skips approval gates, while a false negative usually
|
|
52
|
+
* only lowers confidence — and low confidence defers to the LLM router (exit 2).
|
|
53
|
+
* Matching strictly is therefore the safe error, and provably so for the vector
|
|
54
|
+
* that motivated it: whole-word matches are a strict SUBSET of substring matches
|
|
55
|
+
* per keyword, so narrowing can never invent a hotfix hit that substring didn't
|
|
56
|
+
* already have. No misroute INTO the only gate-skipping pipeline can be created
|
|
57
|
+
* here, only removed.
|
|
58
|
+
*
|
|
59
|
+
* Being precise about the limit of that argument, since overclaiming in a
|
|
60
|
+
* comment is what this file is fixing: the asymmetry holds per KEYWORD, not per
|
|
61
|
+
* CLASSIFICATION. Dropping a match can still shift the bug-vs-chore balance —
|
|
62
|
+
* losing a bug hit may promote a chore from `tie` to `high`, and chore also
|
|
63
|
+
* skips Gates 1 and 2. Whole-word matching DID cost recall that substring had:
|
|
64
|
+
* "bugs" contains "bug" but is not the word "bug". The answer is to list the
|
|
65
|
+
* forms (below), not to match loosely — recall belongs in the vocabulary, where
|
|
66
|
+
* it is explicit and testable, not in a matcher that fires on fragments.
|
|
67
|
+
*
|
|
68
|
+
* `\b` is not usable here: JS word boundaries are ASCII-only, so `\bnão\b`
|
|
69
|
+
* would find a boundary between `n` and `ã` and misbehave on half of these
|
|
70
|
+
* pt-BR keywords. Unicode letter/number lookarounds are the portable form.
|
|
71
|
+
*/
|
|
72
|
+
function matchKeywords(haystack, keywords) {
|
|
73
|
+
return keywords.filter((kw) => {
|
|
74
|
+
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
return new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, 'u').test(haystack);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wordCount(str) {
|
|
80
|
+
const trimmed = str.trim();
|
|
81
|
+
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} input
|
|
86
|
+
* @param {string} [input.text]
|
|
87
|
+
* @param {string} [input.declaredScope]
|
|
88
|
+
* @param {number} [input.estimatedFiles]
|
|
89
|
+
* @returns {{
|
|
90
|
+
* workType: string|null,
|
|
91
|
+
* confidence: 'high'|'tie'|'none',
|
|
92
|
+
* matched: { hotfix: string[], bug: string[], chore: string[], feature: string[] },
|
|
93
|
+
* candidates: string[],
|
|
94
|
+
* }}
|
|
95
|
+
*/
|
|
96
|
+
export function detectWorkType({ text = '', declaredScope = '', estimatedFiles } = {}) {
|
|
97
|
+
const haystack = `${text} ${declaredScope}`.toLowerCase();
|
|
98
|
+
|
|
99
|
+
const hotfixMatched = matchKeywords(haystack, HOTFIX_KEYWORDS);
|
|
100
|
+
const bugMatched = matchKeywords(haystack, BUG_KEYWORDS);
|
|
101
|
+
const choreMatched = matchKeywords(haystack, CHORE_KEYWORDS);
|
|
102
|
+
|
|
103
|
+
const matched = {
|
|
104
|
+
hotfix: hotfixMatched,
|
|
105
|
+
bug: bugMatched,
|
|
106
|
+
chore: choreMatched,
|
|
107
|
+
feature: [], // no keywords of its own — the catch-all default
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Hard precedence: any hotfix signal wins outright, never ties down.
|
|
111
|
+
if (hotfixMatched.length > 0) {
|
|
112
|
+
return { workType: 'hotfix', confidence: 'high', matched, candidates: ['hotfix'] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (bugMatched.length === 0 && choreMatched.length === 0) {
|
|
116
|
+
return { workType: null, confidence: 'none', matched, candidates: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let bugScore = bugMatched.length;
|
|
120
|
+
let choreScore = choreMatched.length;
|
|
121
|
+
|
|
122
|
+
// The weak nudges act ONLY as tie-breakers — apply them only when the raw
|
|
123
|
+
// keyword counts are already equal. Otherwise a clean single-keyword match
|
|
124
|
+
// on one type would be dragged into a tie by the other type's weak signal
|
|
125
|
+
// (e.g. a lone "bug" keyword + a short declaredScope), routing an
|
|
126
|
+
// unambiguous request to the LLM and risking a gate-skipping misroute.
|
|
127
|
+
if (bugMatched.length === choreMatched.length) {
|
|
128
|
+
if (typeof estimatedFiles === 'number' && estimatedFiles <= 2) {
|
|
129
|
+
bugScore += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (wordCount(declaredScope) > 0 && wordCount(declaredScope) <= 6) {
|
|
133
|
+
choreScore += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (bugScore === choreScore) {
|
|
138
|
+
return { workType: null, confidence: 'tie', matched, candidates: ['bug', 'chore'] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const workType = bugScore > choreScore ? 'bug' : 'chore';
|
|
142
|
+
return { workType, confidence: 'high', matched, candidates: [workType] };
|
|
143
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* await setupInfra('/path/to/project');
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { join, dirname } from 'path';
|
|
14
|
+
import { join, dirname, resolve } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
17
|
import { readFileSync, readdirSync, copyFileSync, unlinkSync } from 'fs';
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
writeJson,
|
|
23
23
|
ensureDir,
|
|
24
24
|
writeFile,
|
|
25
|
-
updateGitignore
|
|
25
|
+
updateGitignore,
|
|
26
|
+
ensureGitattributes
|
|
26
27
|
} from '../utils/file-copier.js';
|
|
27
28
|
import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
|
|
28
29
|
import { injectMorphImport } from '../utils/claude-md-injector.js';
|
|
@@ -30,6 +31,7 @@ import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/versio
|
|
|
30
31
|
import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
|
|
31
32
|
import { installSkills } from '../utils/skills-installer.js';
|
|
32
33
|
import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
|
|
34
|
+
import { resolvePrimaryRoot } from '../commands/project/worktree.js';
|
|
33
35
|
|
|
34
36
|
const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
|
35
37
|
|
|
@@ -159,14 +161,38 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = []) {
|
|
|
159
161
|
* @param {object} options
|
|
160
162
|
* @param {Function} options._exec - exec function (for testing)
|
|
161
163
|
* @param {boolean} options.reinstall - If true, only re-install stack-aware assets (rules, skills, agents)
|
|
164
|
+
* @param {boolean} options.force - Bypass the secondary-worktree guard (see below). Callers
|
|
165
|
+
* (e.g. `initCommand`) that already performed their own equivalent check pass this so the
|
|
166
|
+
* check isn't redundantly re-applied here.
|
|
162
167
|
*/
|
|
163
|
-
export async function setupInfra(targetPath, { _exec = execSync, reinstall = false } = {}) {
|
|
168
|
+
export async function setupInfra(targetPath, { _exec = execSync, reinstall = false, force = false } = {}) {
|
|
164
169
|
const quiet = process.env.MORPH_QUIET === '1';
|
|
165
170
|
|
|
166
171
|
function log(msg) {
|
|
167
172
|
if (!quiet) process.stdout.write(msg + '\n');
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
// Guard: refuse a FULL install from inside a secondary (linked) worktree.
|
|
176
|
+
// `.claude/` and `.morph/framework/` are junctions back to the primary root
|
|
177
|
+
// there, but `.morph/config/`, `.morph/context/`, `.morph/checkpoints/`,
|
|
178
|
+
// `.morph/memory/`, `.morph/archive/` and `.morph/state.json` are NOT — a
|
|
179
|
+
// full install would scaffold a second, disconnected config/state tree
|
|
180
|
+
// inside the worktree. `reinstall` mode is exempt: it only touches
|
|
181
|
+
// .claude/* stack-aware assets, which are the junctioned (safe) subset.
|
|
182
|
+
// `resolvePrimaryRoot` fails open to `targetPath` itself outside a git repo
|
|
183
|
+
// (or on any git error), so non-git projects and the primary root are
|
|
184
|
+
// never blocked.
|
|
185
|
+
if (!reinstall && !force) {
|
|
186
|
+
const target = resolve(targetPath).replace(/\\/g, '/');
|
|
187
|
+
const root = resolvePrimaryRoot(targetPath);
|
|
188
|
+
if (root !== target) {
|
|
189
|
+
log(`Você está num worktree secundário (${target}).`);
|
|
190
|
+
log(`O morph-spec já está instalado na raiz primária (${root}).`);
|
|
191
|
+
log('Para reparar infra do worktree use `morph-spec worktree link --all`, não `init`/`setup-infra`.');
|
|
192
|
+
return { blocked: true, root, target };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
// --- Reinstall mode: only re-install stack-aware assets ---
|
|
171
197
|
if (reinstall) {
|
|
172
198
|
log('setup-infra (reinstall): re-installing stack-aware assets...');
|
|
@@ -354,9 +380,10 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
|
|
|
354
380
|
const cliVersion = getInstalledCLIVersion();
|
|
355
381
|
await saveProjectMorphVersion(targetPath, cliVersion);
|
|
356
382
|
|
|
357
|
-
// --- 18. updateGitignore ---
|
|
358
|
-
log('Step 18: Updating .gitignore...');
|
|
383
|
+
// --- 18. updateGitignore + .gitattributes ---
|
|
384
|
+
log('Step 18: Updating .gitignore + .gitattributes...');
|
|
359
385
|
await updateGitignore(targetPath);
|
|
386
|
+
await ensureGitattributes(targetPath);
|
|
360
387
|
|
|
361
388
|
// --- 19. Initialize state.json (only if not exists) ---
|
|
362
389
|
log('Step 19: Initializing state.json...');
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
-
*
|
|
4
|
-
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
-
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
-
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
-
*/
|
|
8
|
-
export function agentIdToSubagentType(id, tier) {
|
|
9
|
-
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
-
return `${prefix}${id}`;
|
|
11
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
+
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
+
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
+
*/
|
|
8
|
+
export function agentIdToSubagentType(id, tier) {
|
|
9
|
+
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
+
return `${prefix}${id}`;
|
|
11
|
+
}
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
* Canonical naming: subagent_type = `morph-{id}` — the frontmatter `name`
|
|
14
14
|
* matches the filename 1:1, and the skills dispatch that exact type.
|
|
15
15
|
*
|
|
16
|
-
* In v5 the harness has
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* In v5 the harness has 8 personas (no tiers) across two axes — domain
|
|
17
|
+
* (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer,
|
|
18
|
+
* infra-engineer, evaluator) and stage (scout, read-only diagnosis before
|
|
19
|
+
* implementation). Each persona is a subagent the Specialist (Claude in
|
|
20
|
+
* implementer role) can dispatch. The 3 roles (Orchestrator/Specialist/
|
|
21
|
+
* Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
|
|
@@ -163,8 +165,10 @@ function buildFrontmatter(agent, description, knownMcpIds) {
|
|
|
163
165
|
// Canonical 1:1 naming: frontmatter name === filename === subagent_type.
|
|
164
166
|
`name: morph-${agent.id}`,
|
|
165
167
|
`description: ${description}`,
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
+
// model is dimensioned per stage/criticality in agents.json (v5.2.0):
|
|
169
|
+
// evaluator (quality multiplier) → opus, domain implementers → sonnet,
|
|
170
|
+
// scout (read-only, cheap) → haiku. Falls back to `inherit` only for
|
|
171
|
+
// personas that omit the field.
|
|
168
172
|
`model: ${agent.model ?? 'inherit'}`,
|
|
169
173
|
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
170
174
|
`maxTurns: 20`,
|