@polymorphism-tech/morph-spec 8.46.0 → 9.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 (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -3,10 +3,9 @@
3
3
  *
4
4
  * The sibling of build-check.js, one layer out: where `runBuildCheck()` proves
5
5
  * the code COMPILES, `runE2eCheck()` proves the application RUNS. That gap is
6
- * not academic — `framework/evals/rubrics/architecture.md` calls out the
7
- * canonical case in its own words: a service injected into a Minimal API
8
- * endpoint without a matching `AddScoped` "derruba a app inteira no startup",
9
- * and it is invisible to `dotnet build`. Only a real bring-up catches it.
6
+ * not academic: a service injected into a Minimal API endpoint without a
7
+ * matching `AddScoped` takes the whole app down at startup, and it is
8
+ * invisible to `dotnet build`. Only a real bring-up catches it.
10
9
  *
11
10
  * Two levels of proof, in increasing strength:
12
11
  * 1. SMOKE — the stack comes up and every service reports healthy.
@@ -23,10 +22,12 @@
23
22
 
24
23
  import { execFileSync } from 'child_process';
25
24
  import { createHash } from 'crypto';
26
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
27
26
  import { join } from 'path';
28
27
 
28
+ import { E2E_SPEC_LOCATIONS } from './frontend-locations.js';
29
29
  import { isSecondaryWorktree } from './git-paths.js';
30
+ import { resolveSpawn } from './spawn-command.js';
30
31
 
31
32
  /** Compose filenames Docker itself recognises, in the order it resolves them. */
32
33
  const COMPOSE_CANDIDATES = [
@@ -43,9 +44,6 @@ const PLAYWRIGHT_CONFIGS = [
43
44
  'playwright.config.mjs',
44
45
  ];
45
46
 
46
- /** Task groups that touch runtime and therefore deserve an e2e pass. */
47
- const RUNTIME_GROUPS = ['frontend', 'backend', 'infra'];
48
-
49
47
  /** How long to wait for every service to report healthy. */
50
48
  export const DEFAULT_HEALTH_TIMEOUT = 180_000;
51
49
 
@@ -72,9 +70,11 @@ const OUTPUT_TAIL_LINES = 20;
72
70
  * @returns {{ code: number, output: string }}
73
71
  */
74
72
  export function defaultExec(file, args, opts = {}) {
73
+ const spawn = resolveSpawn(file, args, { cwd: opts.cwd });
75
74
  try {
76
- const stdout = execFileSync(file, args, {
75
+ const stdout = execFileSync(spawn.file, spawn.args, {
77
76
  cwd: opts.cwd,
77
+ shell: spawn.shell,
78
78
  timeout: opts.timeout,
79
79
  encoding: 'utf-8',
80
80
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -126,8 +126,7 @@ export function resolveSpecCommand(cwd, config, frontendPath) {
126
126
  return { command, args, cwd };
127
127
  }
128
128
 
129
- const searchDirs = frontendPath ? [join(cwd, frontendPath), cwd] : [cwd];
130
- for (const dir of searchDirs) {
129
+ for (const dir of specSearchDirs(cwd, frontendPath)) {
131
130
  const hasConfig = PLAYWRIGHT_CONFIGS.some((name) => existsSync(join(dir, name)));
132
131
  // node_modules guard, same rationale as getBuildSteps(): a config file is
133
132
  // versioned but the toolchain is not, and `npx playwright` in a fresh
@@ -139,6 +138,49 @@ export function resolveSpecCommand(cwd, config, frontendPath) {
139
138
  return null;
140
139
  }
141
140
 
141
+ function specSearchDirs(cwd, frontendPath) {
142
+ return frontendPath ? [join(cwd, frontendPath), cwd] : E2E_SPEC_LOCATIONS.map((loc) => join(cwd, loc));
143
+ }
144
+
145
+ function hasPlaywrightConfig(dir) {
146
+ return PLAYWRIGHT_CONFIGS.some((name) => existsSync(join(dir, name)));
147
+ }
148
+
149
+ // Configs `resolveSpecCommand` did not run (searched dir without node_modules,
150
+ // or a first-level dir outside the search): each is a smoke pass hiding specs.
151
+ export function findUnrunPlaywrightConfigs(cwd, frontendPath) {
152
+ const searched = frontendPath ? [frontendPath, '.'] : [...E2E_SPEC_LOCATIONS];
153
+ const norm = (rel) => rel.replace(/\\/g, '/').replace(/^\.\/|\/$/g, '') || '.';
154
+ const searchedSet = new Set(searched.map(norm));
155
+ let firstLevel = [];
156
+ try {
157
+ firstLevel = readdirSync(cwd, { withFileTypes: true })
158
+ .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
159
+ .map((e) => e.name);
160
+ } catch { /* unreadable root: nothing to report */ }
161
+
162
+ const found = [];
163
+ const seen = new Set();
164
+ for (const rel of [...searched.map(norm), ...firstLevel]) {
165
+ if (seen.has(rel)) continue;
166
+ seen.add(rel);
167
+ const dir = join(cwd, rel);
168
+ if (!hasPlaywrightConfig(dir)) continue;
169
+ if (!searchedSet.has(rel)) found.push({ dir: rel, why: 'not-searched' });
170
+ else if (!existsSync(join(dir, 'node_modules'))) found.push({ dir: rel, why: 'no-node-modules' });
171
+ }
172
+ return found;
173
+ }
174
+
175
+ function unrunConfigWarning({ dir, why }) {
176
+ const where = dir === '.' ? 'na raiz' : `em ${dir}/`;
177
+ const cause = why === 'no-node-modules'
178
+ ? 'sem node_modules ao lado'
179
+ : 'fora de project.frontendPath e dos locais conhecidos';
180
+ const fix = dir === '.' ? 'instale as dependências' : `declare project.frontendPath: "${dir}" e instale as dependências`;
181
+ return `playwright.config.* ${where} não rodou (${cause}) — o nó e2e ficou só no smoke; ${fix}`;
182
+ }
183
+
142
184
  /**
143
185
  * Compose project name for a feature — the isolation key that lets two
144
186
  * worktrees run their stacks side by side without fighting over container
@@ -487,7 +529,7 @@ export function isDockerAvailable(cwd, exec = defaultExec) {
487
529
  // ============================================================================
488
530
 
489
531
  /**
490
- * Decide whether this scope deserves an e2e run, and if not, why.
532
+ * Decide whether this project deserves an e2e run, and if not, why.
491
533
  *
492
534
  * The load-bearing rule is the third one: a project that declares Docker in
493
535
  * `project.integrations` but has no compose file is NOT dispensed — it is a
@@ -495,13 +537,10 @@ export function isDockerAvailable(cwd, exec = defaultExec) {
495
537
  * would end up decorative (every project without a compose file collecting a
496
538
  * green Gate 3 forever).
497
539
  *
498
- * @param {{ taskGroup?: string|null, scope?: string, composeFile: string|null,
499
- * integrations?: string[], skipRequested?: boolean }} params
540
+ * @param {{ composeFile: string|null, integrations?: string[], skipRequested?: boolean }} params
500
541
  * @returns {{ run: boolean, status?: 'skip'|'fail', reason?: string }}
501
542
  */
502
543
  export function decideApplicability({
503
- taskGroup,
504
- scope = 'feature',
505
544
  composeFile,
506
545
  integrations = [],
507
546
  skipRequested = false,
@@ -510,13 +549,6 @@ export function decideApplicability({
510
549
  return { run: false, status: 'skip', reason: 'requested' };
511
550
  }
512
551
 
513
- // Task scope only runs for groups that touch runtime; feature scope (Gate 3)
514
- // always runs. An absent group is treated as runtime-touching — the safe
515
- // direction, since a missing field must never buy a free pass.
516
- if (scope === 'task' && taskGroup && !RUNTIME_GROUPS.includes(String(taskGroup).toLowerCase())) {
517
- return { run: false, status: 'skip', reason: `task group "${taskGroup}" does not touch runtime` };
518
- }
519
-
520
552
  if (!composeFile) {
521
553
  if (integrations.map((i) => String(i).toLowerCase()).includes('docker')) {
522
554
  return {
@@ -837,8 +869,6 @@ export function prepareAuth(
837
869
  * @param {object} [options]
838
870
  * @param {object} [options.config] - .morph/config/config.json contents
839
871
  * @param {string} [options.featureName] - drives COMPOSE_PROJECT_NAME
840
- * @param {string|null} [options.taskGroup] - task's `group`, for applicability
841
- * @param {string} [options.scope] - 'feature' | 'task'
842
872
  * @param {string} [options.gitCommonDir] - where the freshness stamp lives
843
873
  * @param {object} [options.env] - extra env (port block) for compose
844
874
  * @param {boolean} [options.skipRequested]
@@ -853,8 +883,6 @@ export function runE2eCheck(cwd, options = {}) {
853
883
  const {
854
884
  config,
855
885
  featureName = 'default',
856
- taskGroup = null,
857
- scope = 'feature',
858
886
  gitCommonDir = null,
859
887
  env,
860
888
  skipRequested = false,
@@ -864,6 +892,7 @@ export function runE2eCheck(cwd, options = {}) {
864
892
  healthTimeout = DEFAULT_HEALTH_TIMEOUT,
865
893
  specTimeout = DEFAULT_SPEC_TIMEOUT,
866
894
  beforeHeavy = null,
895
+ countSpecs = null,
867
896
  _exec: exec = defaultExec,
868
897
  } = options;
869
898
 
@@ -871,7 +900,7 @@ export function runE2eCheck(cwd, options = {}) {
871
900
  const composeFile = resolveComposeFile(cwd, config);
872
901
  const integrations = Array.isArray(config?.project?.integrations) ? config.project.integrations : [];
873
902
 
874
- const applicability = decideApplicability({ taskGroup, scope, composeFile, integrations, skipRequested });
903
+ const applicability = decideApplicability({ composeFile, integrations, skipRequested });
875
904
  if (!applicability.run) {
876
905
  return { status: applicability.status, command: null, output: '', reason: applicability.reason, warnings };
877
906
  }
@@ -937,7 +966,8 @@ export function runE2eCheck(cwd, options = {}) {
937
966
  if (auth.status === 'fail') {
938
967
  return { status: 'fail', command: null, output: '', reason: auth.reason, warnings };
939
968
  }
940
- return { ...runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv }, exec), warnings };
969
+ const specs = runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec);
970
+ return { ...specs, warnings: [...warnings, ...(specs.warnings || [])] };
941
971
  } finally {
942
972
  if (!keepUp) {
943
973
  tearDownStack(cwd, { composeFile, projectName, env: composeEnv }, exec);
@@ -953,8 +983,9 @@ export function runE2eCheck(cwd, options = {}) {
953
983
  * hardcoded localhost:3000 — ECONNREFUSED that looks exactly like a broken app.
954
984
  * `PLAYWRIGHT_BASE_URL` travels in that env for the config to read.
955
985
  */
956
- function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv }, exec) {
957
- const spec = resolveSpecCommand(cwd, config, config?.project?.frontendPath);
986
+ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec) {
987
+ const frontendPath = config?.project?.frontendPath;
988
+ const spec = resolveSpecCommand(cwd, config, frontendPath);
958
989
  const notes = [up.reused ? up.reason : null, auth?.reason].filter(Boolean);
959
990
 
960
991
  if (!spec) {
@@ -963,11 +994,14 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
963
994
  command: `docker compose -f ${composeFile} up -d --wait`,
964
995
  output: '',
965
996
  reason: joinNotes('smoke only — stack healthy, no Playwright specs found', notes),
997
+ warnings: findUnrunPlaywrightConfigs(cwd, frontendPath).map(unrunConfigWarning),
966
998
  };
967
999
  }
968
1000
 
969
1001
  const commandLine = `${spec.command} ${spec.args.join(' ')}`;
970
1002
  const run = exec(spec.command, spec.args, { cwd: spec.cwd, timeout: specTimeout, env: composeEnv });
1003
+ // Read from the FULL output, before the tail cut; unparseable is null, never 0.
1004
+ const count = typeof countSpecs === 'function' ? (countSpecs(run.output) ?? null) : null;
971
1005
 
972
1006
  if (run.code !== 0) {
973
1007
  return {
@@ -975,6 +1009,7 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
975
1009
  command: commandLine,
976
1010
  output: tailLines(run.output, OUTPUT_TAIL_LINES),
977
1011
  reason: 'e2e specs failed against the running stack',
1012
+ count,
978
1013
  };
979
1014
  }
980
1015
 
@@ -983,6 +1018,7 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
983
1018
  command: commandLine,
984
1019
  output: '',
985
1020
  reason: notes.length ? notes.join('; ') : null,
1021
+ count,
986
1022
  };
987
1023
  }
988
1024
 
@@ -0,0 +1,72 @@
1
+ export const ERRATA_HEADING = '## Errata';
2
+
3
+ /** Strip BOM and CRLF — the one normalization every reader of a frozen markdown applies. */
4
+ export function normalizeMarkdown(text) {
5
+ return String(text ?? '').replace(/^/, '').replace(/\r\n?/g, '\n');
6
+ }
7
+
8
+ /**
9
+ * Lines with fenced blocks marked, so no heading is read inside ``` or ~~~.
10
+ *
11
+ * @returns {{ raw: string, n: number, fenced: boolean }[]}
12
+ */
13
+ export function markdownLines(text) {
14
+ const out = [];
15
+ let fence = null;
16
+ normalizeMarkdown(text).split('\n').forEach((raw, i) => {
17
+ const opener = /^\s*(`{3,}|~{3,})/.exec(raw);
18
+ let fenced = Boolean(fence);
19
+ if (fence) {
20
+ if (opener && opener[1][0] === fence[0] && opener[1].length >= fence.length) fence = null;
21
+ } else if (opener) {
22
+ fence = opener[1];
23
+ fenced = true;
24
+ }
25
+ out.push({ raw, n: i + 1, fenced });
26
+ });
27
+ return out;
28
+ }
29
+
30
+ const isErrataHeading = (line) => !line.fenced
31
+ && /^##\s+errata\s*$/i.test(line.raw);
32
+
33
+ /** Offset of the `## Errata` heading in the NORMALIZED text, or -1. */
34
+ export function errataOffset(text) {
35
+ let offset = 0;
36
+ for (const line of markdownLines(text)) {
37
+ if (isErrataHeading(line)) return offset;
38
+ offset += line.raw.length + 1;
39
+ }
40
+ return -1;
41
+ }
42
+
43
+ /** Everything before the errata block, normalized, without trailing blank. */
44
+ export function frozenBody(text) {
45
+ const norm = normalizeMarkdown(text);
46
+ const at = errataOffset(norm);
47
+ return (at === -1 ? norm : norm.slice(0, at)).trimEnd();
48
+ }
49
+
50
+ /**
51
+ * Is `proposed` the approved text plus an append-only errata? The body stays
52
+ * byte-identical, the result carries an errata block, and what the errata
53
+ * already said is never rewritten.
54
+ *
55
+ * @returns {{ok: true} | {ok: false, why: string}}
56
+ */
57
+ export function errataViolation(original, proposed) {
58
+ if (frozenBody(proposed) !== frozenBody(original)) {
59
+ return { ok: false, why: 'o corpo aprovado mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
60
+ }
61
+ const next = normalizeMarkdown(proposed);
62
+ const nextAt = errataOffset(next);
63
+ if (nextAt === -1) {
64
+ return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
65
+ }
66
+ const prev = normalizeMarkdown(original);
67
+ const prevAt = errataOffset(prev);
68
+ if (prevAt !== -1 && !next.slice(nextAt).startsWith(prev.slice(prevAt).trimEnd())) {
69
+ return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
70
+ }
71
+ return { ok: true };
72
+ }
@@ -100,7 +100,7 @@ export function featureNamedIn(text, names) {
100
100
  * know" as opposed to "the text names nothing in particular".
101
101
  *
102
102
  * This is what the single-feature fallback below was missing: a gate question
103
- * for a feature not yet registered (no `create`, no `proposal.md` written
103
+ * for a feature not yet registered (no `create`, no `plan.md` written
104
104
  * yet — a fresh hotfix, say) names NO known feature either, and used to fall
105
105
  * through to "the tree's one feature must be it". That is exactly how a Gate 3
106
106
  * for `backfill-bool-filter` landed on `export-hub`, the tree's only
@@ -0,0 +1,6 @@
1
+ // The single list of frontend package locations: `src/lib/detectors` re-exports it,
2
+ // because a hook cannot import `src/`.
3
+ export const PACKAGE_LOCATIONS = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
4
+
5
+ // Where `verify`/`e2e` look for a Playwright setup when `project.frontendPath` is absent.
6
+ export const E2E_SPEC_LOCATIONS = [...PACKAGE_LOCATIONS, 'web'];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The gate a gate question asks about, or null. Explicit gate names win over
3
+ * words that may also appear in a feature slug ("plan", "design", "review").
4
+ *
5
+ * @param {string} question
6
+ * @returns {'plan'|'uiux'|'review'|null}
7
+ */
8
+ export function gateFromQuestion(question) {
9
+ if (typeof question !== 'string') return null;
10
+ if (/gate\s+uiux/i.test(question)) return 'uiux';
11
+ if (/gate\s+3\b/i.test(question)) return 'review';
12
+ if (/gate\s+(?:[12]\b|d[oe]\s+plano|plan\b)/i.test(question)) return 'plan';
13
+ const lower = question.toLowerCase();
14
+ if (lower.includes('plano') || lower.includes('plan')) return 'plan';
15
+ if (lower.includes('design')) return 'uiux';
16
+ if (lower.includes('review') || lower.includes('ship')) return 'review';
17
+ return null;
18
+ }
@@ -8,13 +8,12 @@
8
8
  */
9
9
 
10
10
  /** Phase order */
11
- export const PHASE_ORDER = ["proposal","uiux","plan","implement","review","sync"];
11
+ export const PHASE_ORDER = ["plan","uiux","implement","review","sync"];
12
12
 
13
13
  /** Map phase → allowed output subdirectory */
14
14
  export const PHASE_DIRS = {
15
- proposal: '0-proposal',
16
- uiux: '1-design',
17
15
  plan: '2-plan',
16
+ uiux: '1-design',
18
17
  implement: '3-implement',
19
18
  review: '4-review',
20
19
  sync: '3-implement',
@@ -22,57 +21,41 @@ export const PHASE_DIRS = {
22
21
 
23
22
  /** Map output type (camelCase) → phase that produces it */
24
23
  export const OUTPUT_PHASE_MAP = {
25
- proposal: 'proposal',
26
- stackScan: 'proposal',
24
+ plan: 'plan',
25
+ checks: 'plan',
27
26
  uiDesignSystem: 'uiux',
28
27
  uiMockups: 'uiux',
29
28
  uiComponents: 'uiux',
30
29
  uiFlows: 'uiux',
31
- spec: 'plan',
32
- tasks: 'plan',
33
- decisions: 'plan',
34
- usecases: 'plan',
35
- recap: 'implement',
36
30
  evidencias: 'implement',
37
- reviewReport: 'review',
38
- evaluatorReport: 'review',
31
+ verifierReport: 'review',
32
+ reviewerReport: 'review',
33
+ securityReport: 'review',
39
34
  };
40
35
 
41
36
  /** Map filename → output type (camelCase) */
42
37
  export const FILENAME_TO_OUTPUT = {
43
- 'proposal.md': 'proposal',
44
- 'stack-scan.md': 'stackScan',
38
+ 'plan.md': 'plan',
39
+ 'checks.json': 'checks',
45
40
  'design-system.md': 'uiDesignSystem',
46
41
  'mockups.md': 'uiMockups',
47
42
  'components.md': 'uiComponents',
48
43
  'flows.md': 'uiFlows',
49
- 'spec.md': 'spec',
50
- 'tasks.json': 'tasks',
51
- 'decisions.md': 'decisions',
52
- 'recap.md': 'recap',
53
- 'review-report.md': 'reviewReport',
54
- 'evaluator-report.md': 'evaluatorReport',
44
+ 'verifier.md': 'verifierReport',
45
+ 'reviewer.md': 'reviewerReport',
46
+ 'security.md': 'securityReport',
55
47
  };
56
48
 
57
49
  /** Protected spec files and the approval gate that locks them */
58
50
  export const PROTECTED_SPEC_FILES = {
51
+ 'plan.md': 'plan',
52
+ 'checks.json': 'plan',
59
53
  'design-system.md': 'uiux',
60
54
  'mockups.md': 'uiux',
61
55
  'components.md': 'uiux',
62
56
  'flows.md': 'uiux',
63
- 'spec.md': 'plan',
64
- 'tasks.json': 'plan',
65
57
  };
66
58
 
67
- /**
68
- * For protected files that carry mutable progress fields: filename → the
69
- * top-level array-item fields that may change after the gate is approved.
70
- * When a protected file is listed here, the protect-spec-files hook does a
71
- * semantic diff (allowing only these fields to change) instead of a blanket block.
72
- */
73
- export const MUTABLE_SPEC_FIELDS = {
74
- 'tasks.json': ['status', 'outputs', 'notes'],
75
- };
76
59
 
77
60
  /**
78
61
  * Extract feature name from a .morph/features/{feature}/... path
@@ -88,7 +71,7 @@ export function extractFeatureName(filePath) {
88
71
  /**
89
72
  * Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
90
73
  * @param {string} filePath - File path to analyze
91
- * @returns {string|null} Phase directory (e.g., '0-proposal', '2-design') or null
74
+ * @returns {string|null} Phase directory (e.g., '2-plan', '1-design') or null
92
75
  */
93
76
  export function extractPhaseDir(filePath) {
94
77
  const normalized = filePath.replace(/\\/g, '/');
@@ -0,0 +1,118 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
5
+ export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
6
+
7
+ export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
8
+
9
+ export function checksPath(featureDir) {
10
+ return join(featureDir, '2-plan', 'checks.json');
11
+ }
12
+
13
+ export function sliceReportPath(featureDir, sliceId) {
14
+ return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
15
+ }
16
+
17
+ /** Status of a slice from its report text: no report = pendente, PARCIAL first line = parcial, else feito. */
18
+ export function sliceStatusFromReport(text) {
19
+ if (text == null) return 'pendente';
20
+ const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
21
+ return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
22
+ }
23
+
24
+ /**
25
+ * Progress of a feature's slices.
26
+ *
27
+ * @param {string|null} checksText - the raw extracted checks file of the plan
28
+ * @param {(sliceId: string) => string|null} readReport - report text, null when absent
29
+ * @returns {{ total: number, done: number, partial: number, pending: number,
30
+ * slices: { id: string, title: string, depends: string[], covers: string[], status: string }[] } | null} null when that file is unreadable
31
+ */
32
+ export function sliceProgressFrom(checksText, readReport) {
33
+ let slices;
34
+ try {
35
+ slices = JSON.parse(checksText ?? '').slices;
36
+ } catch {
37
+ return null;
38
+ }
39
+ if (!Array.isArray(slices)) return null;
40
+ const out = slices
41
+ .filter((s) => s && typeof s.id === 'string')
42
+ .map((s) => ({
43
+ id: s.id,
44
+ title: s.title ?? '',
45
+ depends: Array.isArray(s.depends) ? s.depends : [],
46
+ covers: Array.isArray(s.covers) ? s.covers : [],
47
+ status: sliceStatusFromReport(readReport(s.id)),
48
+ }));
49
+ const count = (status) => out.filter((s) => s.status === status).length;
50
+ return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
51
+ }
52
+
53
+ function readOrNull(path) {
54
+ try {
55
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Slice progress of the feature directory on disk. */
62
+ export function sliceProgress(featureDir) {
63
+ return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
64
+ }
65
+
66
+ /** Unfinished slices whose dependencies are all done: what the next dispatch can take. */
67
+ export function readySlices(progress) {
68
+ if (!progress) return [];
69
+ const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
70
+ return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
71
+ }
72
+
73
+ /**
74
+ * What a slice report must carry, besides prose and its covered scenarios.
75
+ * Loose on purpose: a false "complete" costs a read, a false "incomplete"
76
+ * re-dispatches finished work.
77
+ */
78
+ export const REPORT_MARKERS = [
79
+ { id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
80
+ ];
81
+
82
+ const MIN_PROSE_CHARS = 80;
83
+
84
+ const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
85
+
86
+ /**
87
+ * Inspect one report file: does it exist, which mandatory items it lacks,
88
+ * and whether it is partial.
89
+ *
90
+ * @param {string} path
91
+ * @param {{ covers?: string[] }} [slice] - every scenario the slice covers must be named
92
+ */
93
+ export function inspectReport(path, { covers = [] } = {}) {
94
+ const missingAll = ['what', 'files', ...covers];
95
+ if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
96
+ let body = '';
97
+ try {
98
+ body = readFileSync(path, 'utf8');
99
+ } catch {
100
+ return { exists: true, complete: false, missing: missingAll, unreadable: true };
101
+ }
102
+
103
+ const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
104
+ missing.push(...covers.filter((id) => !scenarioRe(id).test(body)));
105
+ const prose = body
106
+ .split('\n')
107
+ .filter((l) => !/^\s*(#|[-*]\s|\*\*)/.test(l))
108
+ .join(' ')
109
+ .replace(/\s+/g, ' ')
110
+ .trim();
111
+ if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
112
+
113
+ const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
114
+ const partial = PARTIAL_MARKER.test(firstLine);
115
+ if (partial) missing.push('partial');
116
+
117
+ return { exists: true, complete: missing.length === 0, missing, partial };
118
+ }
@@ -0,0 +1,23 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const NEEDS_CMD_SHIM = /^(npm|npx|pnpm|yarn)$/;
5
+
6
+ /**
7
+ * On win32 `npx`/`npm`/… are `.cmd` shims: ENOENT without a shell, EINVAL as `.cmd`
8
+ * with `shell: false`. Playwright runs from its own JS entry, so it needs no shell.
9
+ */
10
+ export function resolveSpawn(file, args = [], {
11
+ cwd = process.cwd(),
12
+ platform = process.platform,
13
+ exists = existsSync,
14
+ execPath = process.execPath,
15
+ } = {}) {
16
+ if (platform !== 'win32' || !NEEDS_CMD_SHIM.test(file)) return { file, args, shell: false };
17
+
18
+ if (file === 'npx' && args[0] === 'playwright') {
19
+ const cli = join(cwd, 'node_modules', '@playwright', 'test', 'cli.js');
20
+ if (exists(cli)) return { file: execPath, args: [cli, ...args.slice(1)], shell: false };
21
+ }
22
+ return { file: `${file}.cmd`, args, shell: true };
23
+ }