@polymorphism-tech/morph-spec 8.32.0 → 8.34.2

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 (176) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +26 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +71 -68
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +218 -39
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +37 -4
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -8,32 +8,46 @@
8
8
  * .sln) → "dotnet test <proj> --nologo -v q" for each match
9
9
  * 4. package.json present AND has scripts.test → "npm test"
10
10
  * 5. null (no test suite detected)
11
+ *
12
+ * Step 3 deliberately does NOT see linked git worktrees. A worktree is another
13
+ * feature's checkout of the SAME repo: its `tests/X.Tests.csproj` is a copy of
14
+ * the one already matched at the root, so including it both doubles the wall
15
+ * clock and reports on code that is not the tree under verification. In the
16
+ * field this produced a red `tests` node — "fail" at exactly the timeout, with
17
+ * the first run's own summary reading "Com falha: 0" — while nothing had
18
+ * failed. Two independent filters carry that rule (see collectDotnetTestProjects):
19
+ * the CONVENTION path (worktrees/, .worktrees/) and, for worktrees created
20
+ * anywhere else, the registry git itself keeps.
11
21
  */
12
22
 
13
- import { execSync } from 'child_process';
14
- import { existsSync, readFileSync, readdirSync } from 'fs';
15
- import { join } from 'path';
23
+ import { execSync, execFileSync } from 'child_process';
24
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
25
+ import { join, resolve, relative, isAbsolute } from 'path';
16
26
  import { globSync } from 'glob';
17
27
 
28
+ import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/worktree.js';
29
+
18
30
  /**
19
- * Resolve the test command for a project.
31
+ * Timeout budget granted to ONE test project. The chained `a && b` command is
32
+ * given this much per link: a fixed cap made every multi-project chain born
33
+ * condemned — it timed out for being long, not for being broken.
34
+ */
35
+ export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
36
+
37
+ /**
38
+ * Resolve the test command for a project, together with how many test projects
39
+ * it chains — the timeout budget is derived from that count.
20
40
  *
21
41
  * @param {string} [projectPath] - Project root directory (defaults to '.')
22
- * @returns {string | null} The test command to run, or null if none found
42
+ * @returns {{ command: string | null, projectCount: number, source: string }}
43
+ * `source` is one of 'config' | 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'.
23
44
  */
24
- export function resolveTestCommand(projectPath = '.') {
45
+ export function resolveTestPlan(projectPath = '.') {
25
46
  // 1. Read from .morph/config/config.json
26
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
27
- if (existsSync(configPath)) {
28
- try {
29
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
30
- const cmd = config?.project?.testCommand;
31
- if (cmd && typeof cmd === 'string') {
32
- return cmd;
33
- }
34
- } catch {
35
- // Malformed config — fall through to detection
36
- }
47
+ const config = readConfig(projectPath);
48
+ const configured = config?.project?.testCommand;
49
+ if (configured && typeof configured === 'string') {
50
+ return { command: configured, projectCount: 1, source: 'config' };
37
51
  }
38
52
 
39
53
  // 2. Detect .NET project (sln/csproj at root) → dotnet test
@@ -43,7 +57,7 @@ export function resolveTestCommand(projectPath = '.') {
43
57
  (entry) => entry.endsWith('.sln') || entry.endsWith('.csproj')
44
58
  );
45
59
  if (hasDotnetProject) {
46
- return 'dotnet test';
60
+ return { command: 'dotnet test', projectCount: 1, source: 'dotnet-root' };
47
61
  }
48
62
  } catch {
49
63
  // Unreadable project directory — fall through to next detection
@@ -53,19 +67,13 @@ export function resolveTestCommand(projectPath = '.') {
53
67
  // (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
54
68
  // have no root .sln. Run each matched project explicitly rather than a
55
69
  // bare `dotnet test` (which finds nothing to build at the root).
56
- try {
57
- const testProjects = globSync('**/*Tests.csproj', {
58
- cwd: projectPath,
59
- ignore: ['**/bin/**', '**/obj/**', '**/node_modules/**'],
60
- });
61
- if (testProjects.length > 0) {
62
- return testProjects
63
- .sort()
64
- .map((p) => `dotnet test "${p}" --nologo -v q`)
65
- .join(' && ');
66
- }
67
- } catch {
68
- // Glob failure — fall through to next detection
70
+ const testProjects = collectDotnetTestProjects(projectPath);
71
+ if (testProjects.length > 0) {
72
+ return {
73
+ command: testProjects.map((p) => `dotnet test "${p}" --nologo -v q`).join(' && '),
74
+ projectCount: testProjects.length,
75
+ source: 'dotnet-scan',
76
+ };
69
77
  }
70
78
 
71
79
  // 4. package.json present AND has scripts.test → npm test
@@ -74,7 +82,7 @@ export function resolveTestCommand(projectPath = '.') {
74
82
  try {
75
83
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
76
84
  if (pkg?.scripts?.test) {
77
- return 'npm test';
85
+ return { command: 'npm test', projectCount: 1, source: 'npm' };
78
86
  }
79
87
  } catch {
80
88
  // Malformed package.json — fall through to null
@@ -82,43 +90,214 @@ export function resolveTestCommand(projectPath = '.') {
82
90
  }
83
91
 
84
92
  // 5. Nothing found
85
- return null;
93
+ return { command: null, projectCount: 0, source: 'none' };
94
+ }
95
+
96
+ /**
97
+ * Resolve the test command for a project.
98
+ *
99
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
100
+ * @returns {string | null} The test command to run, or null if none found
101
+ */
102
+ export function resolveTestCommand(projectPath = '.') {
103
+ return resolveTestPlan(projectPath).command;
104
+ }
105
+
106
+ /**
107
+ * Find the *Tests.csproj files that belong to THIS tree — never to a linked
108
+ * worktree of the same repo.
109
+ *
110
+ * Two filters, because one is not enough:
111
+ * - the glob ignore covers worktrees created by convention (`worktrees/{f}`
112
+ * and the legacy `.worktrees/{f}`), which is where `morph-spec worktree
113
+ * setup` puts them;
114
+ * - `git worktree list --porcelain` covers every OTHER location, including a
115
+ * worktree someone created by hand outside the convention. Fail-open: no
116
+ * git, no answer, no filter — the convention filter still stands.
117
+ *
118
+ * @param {string} projectPath
119
+ * @returns {string[]} Project-relative csproj paths, sorted
120
+ */
121
+ function collectDotnetTestProjects(projectPath) {
122
+ let matches;
123
+ try {
124
+ matches = globSync('**/*Tests.csproj', {
125
+ cwd: projectPath,
126
+ ignore: [
127
+ '**/bin/**',
128
+ '**/obj/**',
129
+ '**/node_modules/**',
130
+ `${WORKTREES_DIR}/**`,
131
+ `${LEGACY_WORKTREES_DIR}/**`,
132
+ ],
133
+ });
134
+ } catch {
135
+ // Glob failure — caller falls through to the next detection step
136
+ return [];
137
+ }
138
+
139
+ const linked = listLinkedWorktreePaths(projectPath);
140
+ if (linked.length > 0) {
141
+ matches = matches.filter(
142
+ (p) => !linked.some((wt) => isInside(canonicalPath(resolve(projectPath, p)), wt))
143
+ );
144
+ }
145
+
146
+ return matches.sort();
147
+ }
148
+
149
+ /**
150
+ * One spelling per directory. Git prints the LONG form of a path; the tree
151
+ * under verification may be addressed by its 8.3 SHORT form (`MORPHT~1` —
152
+ * every GitHub Windows runner's temp dir, and any user path with a short
153
+ * name). `path.resolve` does not expand aliases, so `isInside(long, short)`
154
+ * says "not inside" for the same directory and a linked worktree leaks into
155
+ * the test command. Only `realpathSync.native` asks the OS. Fail-open to the
156
+ * plain resolve for a path that does not exist (yet).
157
+ */
158
+ function canonicalPath(p) {
159
+ try {
160
+ return realpathSync.native(p);
161
+ } catch {
162
+ return resolve(p);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Absolute paths of the git worktrees linked to this repo, excluding the tree
168
+ * being verified. Fail-open — a repo without git (or a git that errors) yields
169
+ * an empty list, and discovery proceeds on the convention filter alone.
170
+ *
171
+ * @param {string} projectPath
172
+ * @returns {string[]}
173
+ */
174
+ function listLinkedWorktreePaths(projectPath) {
175
+ let out;
176
+ try {
177
+ out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
178
+ cwd: projectPath,
179
+ stdio: 'pipe',
180
+ encoding: 'utf8',
181
+ windowsHide: true,
182
+ });
183
+ } catch {
184
+ return [];
185
+ }
186
+
187
+ const self = canonicalPath(projectPath);
188
+ return out
189
+ .split('\n')
190
+ .filter((line) => line.startsWith('worktree '))
191
+ .map((line) => canonicalPath(line.slice('worktree '.length).trim()))
192
+ .filter((p) => p && p !== self);
193
+ }
194
+
195
+ /** True when `child` is `parent` or lives under it. */
196
+ function isInside(child, parent) {
197
+ const rel = relative(parent, child);
198
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
199
+ }
200
+
201
+ function readConfig(projectPath) {
202
+ const configPath = join(projectPath, '.morph', 'config', 'config.json');
203
+ if (!existsSync(configPath)) return null;
204
+ try {
205
+ return JSON.parse(readFileSync(configPath, 'utf8'));
206
+ } catch {
207
+ // Malformed config — treated as absent by every caller
208
+ return null;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Resolve the timeout budget for the test node.
214
+ *
215
+ * Precedence: explicit override (`morph-spec verify --timeout`) >
216
+ * `config.project.testTimeoutMs` > default scaled by the number of chained
217
+ * test projects.
218
+ *
219
+ * @param {string} projectPath
220
+ * @param {number} projectCount - How many test projects the command chains
221
+ * @param {number} [override] - Explicit override in ms, if the caller has one
222
+ * @returns {number} Timeout in ms
223
+ */
224
+ export function resolveTestTimeout(projectPath, projectCount, override) {
225
+ if (Number.isFinite(override) && override > 0) return override;
226
+
227
+ const configured = readConfig(projectPath)?.project?.testTimeoutMs;
228
+ if (Number.isFinite(configured) && configured > 0) return configured;
229
+
230
+ return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, projectCount);
231
+ }
232
+
233
+ /**
234
+ * Count the test runs that reported a summary before the command was cut off.
235
+ * Heuristic by construction (the summary line is localized), and used only to
236
+ * name WHICH link of the chain was still running when the clock ran out.
237
+ */
238
+ function countCompletedRuns(output) {
239
+ const matches = (output || '').match(
240
+ /(?:^|\n)\s*(?:Aprovado|Passed|Failed|Reprovado|Com falha|Erro)!/gi
241
+ );
242
+ return matches ? matches.length : 0;
86
243
  }
87
244
 
88
245
  /**
89
246
  * Run the project's test suite.
90
247
  *
91
248
  * @param {string} [projectPath] - Project root directory (defaults to '.')
92
- * @param {object} [opts] - Optional overrides for testing
249
+ * @param {object} [opts]
250
+ * @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
93
251
  * @param {Function} [opts._execSync] - Override execSync for unit testing
94
- * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string }}
252
+ * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
95
253
  */
96
254
  export function runTestSuite(projectPath = '.', opts = {}) {
97
- const cmd = resolveTestCommand(projectPath);
255
+ const { command: cmd, projectCount } = resolveTestPlan(projectPath);
98
256
 
99
257
  if (!cmd) {
100
258
  return { skipped: true, reason: 'No testCommand configured and no known project file detected' };
101
259
  }
102
260
 
103
261
  const exec = opts._execSync ?? execSync;
262
+ const timeout = resolveTestTimeout(projectPath, projectCount, opts.timeout);
104
263
 
105
264
  try {
106
265
  const output = exec(cmd, {
107
266
  cwd: projectPath,
108
- timeout: 120_000,
267
+ timeout,
109
268
  encoding: 'utf8',
110
269
  stdio: 'pipe'
111
270
  });
112
- return { passed: true, output: output ?? '', exitCode: 0 };
271
+ return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
113
272
  } catch (err) {
114
273
  const output = (err.stdout || '') + (err.stderr || '');
274
+
275
+ // A timeout is NOT a red suite: the command was interrupted, so no verdict
276
+ // exists. Saying so by name is the whole point — the field diagnosis of
277
+ // this cost a reading of durationMs against a truncated log.
278
+ if (err.code === 'ETIMEDOUT' || err.killed === true) {
279
+ const total = Math.max(1, projectCount);
280
+ const at = Math.min(countCompletedRuns(output) + 1, total);
281
+ const where = total > 1 ? ` na execução ${at} de ${total}` : '';
282
+ return {
283
+ passed: false,
284
+ timedOut: true,
285
+ timeoutMs: timeout,
286
+ buildFailed: false,
287
+ message: `timeout após ${timeout} ms${where} — nenhum teste falhou, o comando foi interrompido`,
288
+ output,
289
+ exitCode: err.status ?? null,
290
+ };
291
+ }
292
+
115
293
  const BUILD_FAIL = /Build FAILED|error MSB\d+|error CS\d+|Could not find.*\.csproj/i;
116
294
  const buildFailed = BUILD_FAIL.test(output);
117
295
  return {
118
296
  passed: false,
119
297
  buildFailed,
120
298
  output,
121
- exitCode: err.status ?? 1
299
+ exitCode: err.status ?? 1,
300
+ timeoutMs: timeout,
122
301
  };
123
302
  }
124
303
  }
@@ -1,39 +1,39 @@
1
- /**
2
- * Checkpoint Validation Persistence
3
- *
4
- * Persists checkpoint validation results to disk for traceability.
5
- * Each checkpoint writes a JSON file to:
6
- * .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
7
- */
8
-
9
- import { mkdirSync, writeFileSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- /**
13
- * Persist a checkpoint validation result to disk.
14
- *
15
- * @param {string} projectPath - Project root
16
- * @param {string} featureName - Feature name
17
- * @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
18
- * @returns {string} Path to the saved JSON file
19
- */
20
- export function persistCheckpointValidation(projectPath, featureName, result) {
21
- const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
22
- mkdirSync(dir, { recursive: true });
23
-
24
- const filename = `checkpoint-${result.checkpointNum}.json`;
25
- const filePath = join(dir, filename);
26
-
27
- const output = {
28
- checkpointNum: result.checkpointNum,
29
- timestamp: result.timestamp || new Date().toISOString(),
30
- passed: result.passed,
31
- validators: result.validators || {},
32
- summary: result.summary || '',
33
- feature: result.feature || featureName,
34
- phase: result.phase || 'implement',
35
- };
36
-
37
- writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
38
- return filePath;
39
- }
1
+ /**
2
+ * Checkpoint Validation Persistence
3
+ *
4
+ * Persists checkpoint validation results to disk for traceability.
5
+ * Each checkpoint writes a JSON file to:
6
+ * .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
7
+ */
8
+
9
+ import { mkdirSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Persist a checkpoint validation result to disk.
14
+ *
15
+ * @param {string} projectPath - Project root
16
+ * @param {string} featureName - Feature name
17
+ * @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
18
+ * @returns {string} Path to the saved JSON file
19
+ */
20
+ export function persistCheckpointValidation(projectPath, featureName, result) {
21
+ const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
22
+ mkdirSync(dir, { recursive: true });
23
+
24
+ const filename = `checkpoint-${result.checkpointNum}.json`;
25
+ const filePath = join(dir, filename);
26
+
27
+ const output = {
28
+ checkpointNum: result.checkpointNum,
29
+ timestamp: result.timestamp || new Date().toISOString(),
30
+ passed: result.passed,
31
+ validators: result.validators || {},
32
+ summary: result.summary || '',
33
+ feature: result.feature || featureName,
34
+ phase: result.phase || 'implement',
35
+ };
36
+
37
+ writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
38
+ return filePath;
39
+ }