@polymorphism-tech/morph-spec 7.9.4 → 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 (211) 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/ai-agents/agent-session.md +160 -160
  58. package/framework/standards/ai-agents/batching.md +193 -193
  59. package/framework/standards/ai-agents/context-providers.md +188 -188
  60. package/framework/standards/ai-agents/mcp-server.md +250 -250
  61. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  62. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  63. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  64. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  65. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  66. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  67. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  68. package/framework/standards/ai-agents/production.md +275 -275
  69. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  70. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  71. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  72. package/framework/standards/ai-agents/structured-output.md +249 -249
  73. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  74. package/framework/templates/.idea/morph-templates.xml +92 -0
  75. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  76. package/framework/templates/IDE-SNIPPETS.md +262 -262
  77. package/framework/templates/REGISTRY.json +0 -62
  78. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  79. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  80. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  81. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  82. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  83. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  84. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  85. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  87. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  88. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  89. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  90. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  92. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  93. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  94. package/framework/templates/code/typescript/contracts.ts +167 -167
  95. package/framework/templates/docs/clarifications.md +253 -253
  96. package/framework/templates/docs/schema-analysis.md +119 -119
  97. package/framework/templates/docs/ui-components.md +52 -52
  98. package/framework/templates/docs/ui-design-system.md +76 -76
  99. package/framework/templates/docs/ui-flows.md +167 -167
  100. package/framework/templates/docs/ui-mockups.md +98 -98
  101. package/framework/templates/feature/decisions.md +122 -122
  102. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  103. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  104. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  105. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  106. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  109. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  110. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  111. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  112. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  113. package/framework/templates/infrastructure/github/README.md +56 -56
  114. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  115. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  116. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  117. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  118. package/framework/templates/integrations/asaas-client.cs +387 -387
  119. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  120. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  121. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  122. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  123. package/framework/templates/project/validate.js +120 -120
  124. package/framework/templates/state.template.json +17 -17
  125. package/framework/templates/ui/design-system.css +226 -226
  126. package/package.json +1 -1
  127. package/src/commands/dashboard/dashboard.js +53 -53
  128. package/src/commands/index.js +13 -13
  129. package/src/commands/mcp/mcp-setup.js +519 -519
  130. package/src/commands/project/checkpoint.js +209 -209
  131. package/src/commands/project/doctor.js +4 -2
  132. package/src/commands/project/init.js +46 -2
  133. package/src/commands/project/update.js +5 -3
  134. package/src/commands/project/worktree.js +1005 -46
  135. package/src/commands/state/advance.js +17 -2
  136. package/src/commands/state/approve.js +18 -1
  137. package/src/commands/state/create.js +158 -14
  138. package/src/commands/state/retype.js +126 -0
  139. package/src/commands/state/score.js +4 -0
  140. package/src/commands/tasks/dag.js +32 -0
  141. package/src/commands/validation/index.js +1 -0
  142. package/src/commands/validation/validate-feature.js +33 -5
  143. package/src/commands/validation/verify.js +190 -0
  144. package/src/core/state/phase-state-machine.js +197 -197
  145. package/src/core/state/state-manager.js +35 -10
  146. package/src/core/templates/template-renderer.js +477 -477
  147. package/src/core/work-types.js +79 -0
  148. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  149. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  150. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  151. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  152. package/src/dashboard/app/main.jsx +13 -13
  153. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  154. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  155. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  156. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  157. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  158. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  159. package/src/dashboard/app/pages/Standards.jsx +332 -332
  160. package/src/dashboard/app/styles/index.css +47 -47
  161. package/src/dashboard/index.html +12 -12
  162. package/src/dashboard/package.json +24 -24
  163. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  164. package/src/dashboard/scanner/phases-scanner.js +15 -15
  165. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  166. package/src/dashboard/scanner/rules-scanner.js +37 -37
  167. package/src/dashboard/scanner/skills-scanner.js +43 -43
  168. package/src/dashboard/scanner/squads-scanner.js +13 -13
  169. package/src/dashboard/scanner/standards-scanner.js +26 -26
  170. package/src/dashboard/scanner/state-scanner.js +42 -0
  171. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  172. package/src/dashboard/vite.config.js +24 -24
  173. package/src/lib/agents/metrics-aggregator.js +56 -56
  174. package/src/lib/agents/metrics-calculator.js +125 -125
  175. package/src/lib/agents/output-collector.js +73 -73
  176. package/src/lib/detectors/claude-config-detector.js +152 -152
  177. package/src/lib/detectors/design-system-detector.js +231 -231
  178. package/src/lib/detectors/index.js +217 -217
  179. package/src/lib/generators/architecture-scanner.js +639 -639
  180. package/src/lib/generators/design-system-generator.js +508 -508
  181. package/src/lib/generators/index.js +13 -13
  182. package/src/lib/generators/settings-generator.js +209 -209
  183. package/src/lib/hooks/hook-executor.js +167 -167
  184. package/src/lib/installers/mcp-installer.js +719 -719
  185. package/src/lib/installers/memory-installer.js +1 -1
  186. package/src/lib/risk-detector.js +16 -0
  187. package/src/lib/tasks/dispatch-planner.js +122 -4
  188. package/src/lib/tasks/mermaid-renderer.js +132 -0
  189. package/src/lib/tasks/task-classifier.js +30 -30
  190. package/src/lib/tasks/task-parser.js +209 -209
  191. package/src/lib/tasks/test-runner.js +27 -6
  192. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  193. package/src/lib/validators/checkpoint-persistence.js +39 -39
  194. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  195. package/src/lib/validators/css/css-validator.js +340 -340
  196. package/src/lib/validators/packages/package-validator.js +347 -347
  197. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  198. package/src/lib/validators/shared/issue-counter.js +14 -14
  199. package/src/lib/validators/validation-runner.js +349 -349
  200. package/src/lib/validators/verify-runner.js +322 -0
  201. package/src/lib/work-type-detector.js +143 -0
  202. package/src/scripts/setup-infra.js +32 -5
  203. package/src/utils/agent-id-mapper.js +11 -11
  204. package/src/utils/agents-installer.js +10 -6
  205. package/src/utils/claude-settings-manager.js +300 -300
  206. package/src/utils/file-copier.js +43 -0
  207. package/src/utils/hooks-installer.js +41 -11
  208. package/src/utils/skills-installer.js +174 -174
  209. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  210. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  211. 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`,