@polymorphism-tech/morph-spec 7.9.3 → 8.0.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.
Files changed (213) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +31 -2
  3. package/bin/morph-spec.js +50 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +38 -1
  7. package/docs/QUICKSTART.md +60 -1
  8. package/framework/CLAUDE.md +13 -2
  9. package/framework/MORPH.md +38 -2
  10. package/framework/agents.json +22 -3
  11. package/framework/commands/commit.md +166 -166
  12. package/framework/commands/morph-hotfix.md +39 -0
  13. package/framework/commands/morph-infra.md +215 -215
  14. package/framework/commands/morph-preflight.md +205 -205
  15. package/framework/commands/morph-troubleshoot.md +124 -124
  16. package/framework/hooks/README.md +25 -10
  17. package/framework/hooks/claude-code/core/gate-guard.js +4 -2
  18. package/framework/hooks/claude-code/core/loop-detect.js +2 -1
  19. package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
  20. package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
  21. package/framework/hooks/claude-code/core/validate-completion.js +1 -1
  22. package/framework/hooks/claude-code/core/worktree-context.js +160 -0
  23. package/framework/hooks/claude-code/statusline.py +679 -679
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/activity-logger.js +160 -160
  31. package/framework/hooks/shared/build-check.js +2 -2
  32. package/framework/hooks/shared/hook-response.js +17 -5
  33. package/framework/hooks/shared/payload-utils.js +42 -0
  34. package/framework/hooks/shared/state-reader.js +22 -2
  35. package/framework/hooks/shared/stdin-reader.js +26 -26
  36. package/framework/rubrics/backend.json +41 -41
  37. package/framework/rules/ai-agents-standards.md +45 -45
  38. package/framework/rules/dotnet-templates.md +86 -86
  39. package/framework/rules/frontend-standards.md +13 -13
  40. package/framework/rules/infrastructure-standards.md +27 -27
  41. package/framework/rules/neon-standards.md +25 -25
  42. package/framework/rules/nextjs-standards.md +20 -20
  43. package/framework/rules/nextjs-templates.md +40 -40
  44. package/framework/schemas/feature.schema.json +95 -1
  45. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  46. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  47. package/framework/skills/core/morph-apply/SKILL.md +39 -8
  48. package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
  49. package/framework/skills/core/morph-eval/SKILL.md +6 -1
  50. package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
  51. package/framework/skills/core/morph-implement/SKILL.md +8 -4
  52. package/framework/skills/core/morph-plan/SKILL.md +5 -1
  53. package/framework/skills/core/morph-proposal/SKILL.md +24 -1
  54. package/framework/skills/core/morph-review/SKILL.md +3 -2
  55. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  56. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  57. package/framework/standards/STANDARDS.json +7 -2
  58. package/framework/standards/ai-agents/agent-session.md +160 -160
  59. package/framework/standards/ai-agents/batching.md +193 -193
  60. package/framework/standards/ai-agents/context-providers.md +188 -188
  61. package/framework/standards/ai-agents/mcp-server.md +250 -250
  62. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  63. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  64. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  65. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  66. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  67. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  68. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  69. package/framework/standards/ai-agents/production.md +275 -275
  70. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  71. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  72. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  73. package/framework/standards/ai-agents/structured-output.md +249 -249
  74. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  75. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +45 -3
  76. package/framework/templates/.idea/morph-templates.xml +92 -0
  77. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  78. package/framework/templates/IDE-SNIPPETS.md +262 -262
  79. package/framework/templates/REGISTRY.json +0 -62
  80. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  81. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  82. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  83. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  84. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  85. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  86. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  87. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  88. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  89. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  91. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  92. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  93. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  94. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  95. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  96. package/framework/templates/code/typescript/contracts.ts +167 -167
  97. package/framework/templates/docs/clarifications.md +253 -253
  98. package/framework/templates/docs/schema-analysis.md +119 -119
  99. package/framework/templates/docs/ui-components.md +52 -52
  100. package/framework/templates/docs/ui-design-system.md +76 -76
  101. package/framework/templates/docs/ui-flows.md +167 -167
  102. package/framework/templates/docs/ui-mockups.md +98 -98
  103. package/framework/templates/feature/decisions.md +122 -122
  104. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  107. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  108. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  109. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  110. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  111. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  112. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  113. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  114. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  115. package/framework/templates/infrastructure/github/README.md +56 -56
  116. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  117. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  118. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  119. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  120. package/framework/templates/integrations/asaas-client.cs +387 -387
  121. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  122. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  123. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  124. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  125. package/framework/templates/project/validate.js +120 -120
  126. package/framework/templates/state.template.json +17 -17
  127. package/framework/templates/ui/design-system.css +226 -226
  128. package/package.json +1 -1
  129. package/src/commands/dashboard/dashboard.js +53 -53
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/mcp/mcp-setup.js +519 -519
  132. package/src/commands/project/checkpoint.js +209 -209
  133. package/src/commands/project/doctor.js +4 -2
  134. package/src/commands/project/init.js +46 -2
  135. package/src/commands/project/update.js +5 -3
  136. package/src/commands/project/worktree.js +1005 -46
  137. package/src/commands/state/advance.js +17 -2
  138. package/src/commands/state/approve.js +18 -1
  139. package/src/commands/state/create.js +158 -14
  140. package/src/commands/state/retype.js +126 -0
  141. package/src/commands/state/score.js +4 -0
  142. package/src/commands/tasks/dag.js +32 -0
  143. package/src/commands/validation/index.js +1 -0
  144. package/src/commands/validation/validate-feature.js +33 -5
  145. package/src/commands/validation/verify.js +190 -0
  146. package/src/core/state/phase-state-machine.js +197 -197
  147. package/src/core/state/state-manager.js +35 -10
  148. package/src/core/templates/template-renderer.js +477 -477
  149. package/src/core/work-types.js +79 -0
  150. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  151. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  152. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  153. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  154. package/src/dashboard/app/main.jsx +13 -13
  155. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  156. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  157. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  158. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  159. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  160. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  161. package/src/dashboard/app/pages/Standards.jsx +332 -332
  162. package/src/dashboard/app/styles/index.css +47 -47
  163. package/src/dashboard/index.html +12 -12
  164. package/src/dashboard/package.json +24 -24
  165. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  166. package/src/dashboard/scanner/phases-scanner.js +15 -15
  167. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  168. package/src/dashboard/scanner/rules-scanner.js +37 -37
  169. package/src/dashboard/scanner/skills-scanner.js +43 -43
  170. package/src/dashboard/scanner/squads-scanner.js +13 -13
  171. package/src/dashboard/scanner/standards-scanner.js +26 -26
  172. package/src/dashboard/scanner/state-scanner.js +42 -0
  173. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  174. package/src/dashboard/vite.config.js +24 -24
  175. package/src/lib/agents/metrics-aggregator.js +56 -56
  176. package/src/lib/agents/metrics-calculator.js +125 -125
  177. package/src/lib/agents/output-collector.js +73 -73
  178. package/src/lib/detectors/claude-config-detector.js +152 -152
  179. package/src/lib/detectors/design-system-detector.js +231 -231
  180. package/src/lib/detectors/index.js +217 -217
  181. package/src/lib/generators/architecture-scanner.js +639 -639
  182. package/src/lib/generators/design-system-generator.js +508 -508
  183. package/src/lib/generators/index.js +13 -13
  184. package/src/lib/generators/settings-generator.js +209 -209
  185. package/src/lib/hooks/hook-executor.js +167 -167
  186. package/src/lib/installers/mcp-installer.js +719 -719
  187. package/src/lib/installers/memory-installer.js +1 -1
  188. package/src/lib/risk-detector.js +16 -0
  189. package/src/lib/tasks/dispatch-planner.js +122 -4
  190. package/src/lib/tasks/mermaid-renderer.js +132 -0
  191. package/src/lib/tasks/task-classifier.js +30 -30
  192. package/src/lib/tasks/task-parser.js +209 -209
  193. package/src/lib/tasks/test-runner.js +27 -6
  194. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  195. package/src/lib/validators/checkpoint-persistence.js +39 -39
  196. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  197. package/src/lib/validators/css/css-validator.js +340 -340
  198. package/src/lib/validators/packages/package-validator.js +347 -347
  199. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  200. package/src/lib/validators/shared/issue-counter.js +14 -14
  201. package/src/lib/validators/validation-runner.js +349 -349
  202. package/src/lib/validators/verify-runner.js +322 -0
  203. package/src/lib/work-type-detector.js +143 -0
  204. package/src/scripts/setup-infra.js +32 -5
  205. package/src/utils/agent-id-mapper.js +11 -11
  206. package/src/utils/agents-installer.js +10 -6
  207. package/src/utils/claude-settings-manager.js +300 -300
  208. package/src/utils/file-copier.js +43 -0
  209. package/src/utils/hooks-installer.js +41 -11
  210. package/src/utils/skills-installer.js +174 -174
  211. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  212. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  213. 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 7 domain personas (no tiers) — each persona is a
17
- * subagent the Specialist (Claude in implementer role) can dispatch when
18
- * working in that domain. The 3 roles (Orchestrator/Specialist/Evaluator)
19
- * are assumed by Claude itself per MORPH.md, not as separate agents.
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
- // Domain implementers inherit the session model by default — a hardcoded
167
- // downgrade here would silently lower quality. Override via agents.json.
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`,