@enterpriseai/cli 3.19.0 → 3.19.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 (364) hide show
  1. package/package.json +3 -3
  2. package/resources/deploy/eai-app-template/producer-pin.json +5 -5
  3. package/resources/deploy/eai-app-template/tests/source-unknown-deployment-evidence.test.mjs +2141 -1
  4. package/resources/gofer/.gofer-version +4 -357
  5. package/resources/gofer/agents-skills/eai/SKILL.md +365 -0
  6. package/resources/gofer/agents-skills/eai-update/SKILL.md +56 -0
  7. package/resources/gofer/agents-skills/gofer-documentation/SKILL.md +93 -0
  8. package/resources/gofer/bash-scripts/create-new-feature.sh +3 -1
  9. package/resources/gofer/bash-scripts/sync-implementation-status.sh +4 -3
  10. package/resources/gofer/bash-scripts/update-agent-context.sh +7 -8
  11. package/resources/gofer/bash-scripts/validate-artifact.sh +10 -6
  12. package/resources/gofer/bash-scripts/verify-object-type-routing-workspace.sh +184 -0
  13. package/resources/gofer/claude-commands/eai-update.md +46 -0
  14. package/resources/gofer/claude-commands/eai.md +355 -0
  15. package/resources/gofer/claude-skills/eai/SKILL.md +365 -0
  16. package/resources/gofer/claude-skills/eai-update/SKILL.md +56 -0
  17. package/resources/gofer/claude-skills/gofer-documentation/SKILL.md +93 -0
  18. package/resources/gofer/commands/0_gofer_start.md +440 -99
  19. package/resources/gofer/commands/0a_problem_validation.md +187 -27
  20. package/resources/gofer/commands/10_gofer_cloud.md +183 -23
  21. package/resources/gofer/commands/1_gofer_research.md +239 -30
  22. package/resources/gofer/commands/2_gofer_specify.md +239 -35
  23. package/resources/gofer/commands/3_gofer_plan.md +259 -43
  24. package/resources/gofer/commands/4_gofer_tasks.md +401 -81
  25. package/resources/gofer/commands/5_gofer_implement.md +375 -84
  26. package/resources/gofer/commands/6_gofer_validate.md +284 -53
  27. package/resources/gofer/commands/7_gofer_save.md +187 -26
  28. package/resources/gofer/commands/7a_stakeholder_comms.md +183 -23
  29. package/resources/gofer/commands/8_gofer_branding.md +183 -23
  30. package/resources/gofer/commands/9_gofer_tests.md +212 -23
  31. package/resources/gofer/commands/gofer_bootstrap_workspace.md +121 -8
  32. package/resources/gofer/commands/gofer_check_workspace.md +123 -8
  33. package/resources/gofer/commands/gofer_constitution.md +183 -23
  34. package/resources/gofer/commands/gofer_diagnose.md +183 -23
  35. package/resources/gofer/commands/gofer_eai_first_run.md +254 -25
  36. package/resources/gofer/commands/gofer_hydrate.md +183 -23
  37. package/resources/gofer/commands/gofer_personality.md +117 -6
  38. package/resources/gofer/commands/gofer_plan.md +117 -6
  39. package/resources/gofer/commands/gofer_side.md +117 -6
  40. package/resources/gofer/commands/gofer_spec_summary.md +183 -23
  41. package/resources/gofer/commands/gofer_tdd.md +183 -23
  42. package/resources/gofer/commands/gofer_vocabulary.md +185 -25
  43. package/resources/gofer/commands/gofer_zoom_out.md +183 -23
  44. package/resources/gofer/config/typesafe-learning-review.json +16 -0
  45. package/resources/gofer/config/typesafe-semantic-review.json +9 -0
  46. package/resources/gofer/copilot-instructions/gofer-cost.instructions.md +15 -6
  47. package/resources/gofer/copilot-instructions/typescript.instructions.md +1 -1
  48. package/resources/gofer/copilot-prompts/eai-update.prompt.md +58 -0
  49. package/resources/gofer/copilot-prompts/eai.prompt.md +367 -0
  50. package/resources/gofer/gemini/commands/gofer/eai-update.md +22 -0
  51. package/resources/gofer/gemini/commands/gofer/eai-update.toml +2 -0
  52. package/resources/gofer/gemini/commands/gofer/eai.md +357 -0
  53. package/resources/gofer/gemini/commands/gofer/eai.toml +2 -0
  54. package/resources/gofer/gemini/commands/gofer/manifest.json +3 -27
  55. package/resources/gofer/gemini/extension.json +4 -4
  56. package/resources/gofer/github-agents/eai.agent.md +83 -0
  57. package/resources/gofer/github-agents/gofer-business.agent.md +73 -4
  58. package/resources/gofer/github-agents/gofer-implement.agent.md +71 -2
  59. package/resources/gofer/github-agents/gofer-plan.agent.md +72 -2
  60. package/resources/gofer/github-agents/gofer-research.agent.md +72 -2
  61. package/resources/gofer/github-agents/gofer-validate.agent.md +72 -2
  62. package/resources/gofer/github-skills/eai/SKILL.md +365 -0
  63. package/resources/gofer/github-skills/eai-update/SKILL.md +56 -0
  64. package/resources/gofer/github-skills/gofer-documentation/SKILL.md +93 -0
  65. package/resources/gofer/grok-skills/eai/SKILL.md +267 -14
  66. package/resources/gofer/grok-skills/eai-update/SKILL.md +56 -0
  67. package/resources/gofer/hook-scripts/user-prompt-submit.mjs +58 -13
  68. package/resources/gofer/instruction-templates/base/agents-base.md +8 -4
  69. package/resources/gofer/instruction-templates/gofer/gofer-claude.md +7 -5
  70. package/resources/gofer/instruction-templates/gofer/gofer-copilot.md +10 -8
  71. package/resources/gofer/memory/constitution.md +9 -7
  72. package/resources/gofer/memory/gofer-model-policy.yaml +23 -1
  73. package/resources/gofer/memory/lessons.md +0 -31
  74. package/resources/gofer/node-scripts/canonical-descriptions.mjs +1 -1
  75. package/resources/gofer/node-scripts/check-version-alignment.mjs +4 -7
  76. package/resources/gofer/node-scripts/delivery-lineage-corpus.mjs +156 -0
  77. package/resources/gofer/node-scripts/generate-commands.mjs +1073 -265
  78. package/resources/gofer/node-scripts/gofer-acceptance-check.mjs +143 -0
  79. package/resources/gofer/node-scripts/gofer-agent-catalog.mjs +271 -0
  80. package/resources/gofer/node-scripts/gofer-benchmark-executor.mjs +123 -0
  81. package/resources/gofer/node-scripts/gofer-benchmark-sign.mjs +62 -0
  82. package/resources/gofer/node-scripts/gofer-benchmark-signer.mjs +89 -0
  83. package/resources/gofer/node-scripts/gofer-benchmark.mjs +76 -0
  84. package/resources/gofer/node-scripts/gofer-blocker-control.mjs +274 -0
  85. package/resources/gofer/node-scripts/gofer-claude-adapter.mjs +154 -0
  86. package/resources/gofer/node-scripts/gofer-closed-loop-audit.mjs +19 -2
  87. package/resources/gofer/node-scripts/gofer-delivery-check.mjs +121 -0
  88. package/resources/gofer/node-scripts/gofer-encrypted-key.mjs +58 -0
  89. package/resources/gofer/node-scripts/gofer-execution-metrics.mjs +477 -0
  90. package/resources/gofer/node-scripts/gofer-execution-recovery.mjs +334 -0
  91. package/resources/gofer/node-scripts/gofer-grok-adapter.mjs +184 -0
  92. package/resources/gofer/node-scripts/gofer-heldout-corpus.mjs +73 -0
  93. package/resources/gofer/node-scripts/gofer-heldout-snapshot.mjs +286 -0
  94. package/resources/gofer/node-scripts/gofer-heldout-verifier.mjs +211 -0
  95. package/resources/gofer/node-scripts/gofer-host-capability.mjs +335 -0
  96. package/resources/gofer/node-scripts/gofer-independent-reviewer.mjs +130 -0
  97. package/resources/gofer/node-scripts/gofer-live-routing.mjs +109 -0
  98. package/resources/gofer/node-scripts/gofer-local-capability-issuer.mjs +71 -0
  99. package/resources/gofer/node-scripts/gofer-local-isolation.mjs +175 -0
  100. package/resources/gofer/node-scripts/gofer-local-settings-cleanup.mjs +388 -0
  101. package/resources/gofer/node-scripts/gofer-loop-audit.mjs +54 -3
  102. package/resources/gofer/node-scripts/gofer-native-adapter.mjs +567 -0
  103. package/resources/gofer/node-scripts/gofer-native-benchmark-dispatch.mjs +145 -0
  104. package/resources/gofer/node-scripts/gofer-native-runtime.mjs +169 -0
  105. package/resources/gofer/node-scripts/gofer-pre-edit-check.mjs +204 -0
  106. package/resources/gofer/node-scripts/gofer-priority-check.mjs +214 -0
  107. package/resources/gofer/node-scripts/gofer-response-check.mjs +56 -0
  108. package/resources/gofer/node-scripts/gofer-reviewed-learning.mjs +1232 -0
  109. package/resources/gofer/node-scripts/gofer-run-verified-task.mjs +330 -0
  110. package/resources/gofer/node-scripts/gofer-runtime-ledger.mjs +289 -0
  111. package/resources/gofer/node-scripts/gofer-semantic-drift.mjs +584 -0
  112. package/resources/gofer/node-scripts/gofer-surface-update.mjs +1178 -0
  113. package/resources/gofer/node-scripts/gofer-trust-bootstrap.mjs +77 -0
  114. package/resources/gofer/node-scripts/gofer-trusted-benchmark.mjs +102 -0
  115. package/resources/gofer/node-scripts/gofer-trusted-evaluator.mjs +274 -0
  116. package/resources/gofer/node-scripts/gofer-tty-prompt.mjs +34 -0
  117. package/resources/gofer/node-scripts/gofer-typesafe-credentials.mjs +435 -0
  118. package/resources/gofer/node-scripts/gofer-ui-preview.mjs +1212 -0
  119. package/resources/gofer/node-scripts/gofer-verified-execution.mjs +484 -0
  120. package/resources/gofer/node-scripts/gofer-verifier-key-ceremony.mjs +101 -0
  121. package/resources/gofer/node-scripts/gofer-worktree-layout.mjs +32 -0
  122. package/resources/gofer/node-scripts/object-type-routing-phase-bundle.mjs +360 -0
  123. package/resources/gofer/node-scripts/package-agent-plugin.mjs +574 -82
  124. package/resources/gofer/node-scripts/parse-stage-command.mjs +1 -1
  125. package/resources/gofer/node-scripts/render-delivery-lineage.mjs +178 -0
  126. package/resources/gofer/node-scripts/sync-extension-resources.mjs +78 -2
  127. package/resources/gofer/node-scripts/validate-object-type-identifiers.mjs +767 -0
  128. package/resources/gofer/node-scripts/validate-object-type-routing-workspace.mjs +1325 -0
  129. package/resources/gofer/node-scripts/validate-v4-resource-contract.d.mts +44 -0
  130. package/resources/gofer/node-scripts/validate-v4-resource-contract.mjs +957 -0
  131. package/resources/gofer/node-scripts/workspace-bootstrap-lib.mjs +685 -116
  132. package/resources/gofer/references/agent-catalog.json +126 -0
  133. package/resources/gofer/references/blocker-mediation.md +156 -0
  134. package/resources/gofer/references/business-updates-and-goal-checks.md +109 -0
  135. package/resources/gofer/references/delivery-lineage.md +48 -0
  136. package/resources/gofer/references/mvp-capability-validation.md +102 -0
  137. package/resources/gofer/references/platform/README.md +1 -1
  138. package/resources/gofer/references/platform/eai-app-template.md +15 -13
  139. package/resources/gofer/references/platform/eai-auth-access.md +104 -0
  140. package/resources/gofer/references/platform/eai-config-driven-ui.md +5 -5
  141. package/resources/gofer/references/platform/eai-error-catalog.yaml +77 -19
  142. package/resources/gofer/references/platform/eai-repo-contract.md +61 -23
  143. package/resources/gofer/references/platform/eai-service-patterns.md +102 -105
  144. package/resources/gofer/references/platform/eai.md +44 -8
  145. package/resources/gofer/references/platform/vertical-template.md +10 -9
  146. package/resources/gofer/references/priority-outcome-protection.md +152 -0
  147. package/resources/gofer/references/verified-agent-execution.md +179 -0
  148. package/resources/gofer/schemas/gofer-learning-trace-v1.json +70 -0
  149. package/resources/gofer/specify-commands/0_gofer_start.md +440 -99
  150. package/resources/gofer/specify-commands/0a_problem_validation.md +187 -27
  151. package/resources/gofer/specify-commands/10_gofer_cloud.md +183 -23
  152. package/resources/gofer/specify-commands/1_gofer_research.md +239 -30
  153. package/resources/gofer/specify-commands/2_gofer_specify.md +239 -35
  154. package/resources/gofer/specify-commands/3_gofer_plan.md +259 -43
  155. package/resources/gofer/specify-commands/4_gofer_tasks.md +401 -81
  156. package/resources/gofer/specify-commands/5_gofer_implement.md +375 -84
  157. package/resources/gofer/specify-commands/6_gofer_validate.md +284 -53
  158. package/resources/gofer/specify-commands/7_gofer_save.md +187 -26
  159. package/resources/gofer/specify-commands/7a_stakeholder_comms.md +183 -23
  160. package/resources/gofer/specify-commands/8_gofer_branding.md +183 -23
  161. package/resources/gofer/specify-commands/9_gofer_tests.md +212 -23
  162. package/resources/gofer/specify-commands/gofer_bootstrap_workspace.md +121 -8
  163. package/resources/gofer/specify-commands/gofer_check_workspace.md +123 -8
  164. package/resources/gofer/specify-commands/gofer_constitution.md +183 -23
  165. package/resources/gofer/specify-commands/gofer_diagnose.md +183 -23
  166. package/resources/gofer/specify-commands/gofer_eai_first_run.md +254 -25
  167. package/resources/gofer/specify-commands/gofer_hydrate.md +183 -23
  168. package/resources/gofer/specify-commands/gofer_personality.md +117 -6
  169. package/resources/gofer/specify-commands/gofer_plan.md +117 -6
  170. package/resources/gofer/specify-commands/gofer_side.md +117 -6
  171. package/resources/gofer/specify-commands/gofer_spec_summary.md +183 -23
  172. package/resources/gofer/specify-commands/gofer_tdd.md +183 -23
  173. package/resources/gofer/specify-commands/gofer_vocabulary.md +185 -25
  174. package/resources/gofer/specify-commands/gofer_zoom_out.md +183 -23
  175. package/resources/gofer/specify-config/object-type-routing.json +24 -0
  176. package/resources/gofer/specify-config/typesafe-learning-review.json +16 -0
  177. package/resources/gofer/specify-config/typesafe-semantic-review.json +9 -0
  178. package/resources/gofer/system-skills/eai/SKILL.md +365 -0
  179. package/resources/gofer/system-skills/eai-update/SKILL.md +56 -0
  180. package/resources/gofer/system-skills/gofer-documentation/SKILL.md +93 -0
  181. package/resources/gofer/templates/build-map-template.md +64 -0
  182. package/resources/gofer/templates/business-scenarios-template.json +14 -0
  183. package/resources/gofer/templates/ciso-security-summary-template.md +3 -3
  184. package/resources/gofer/templates/context-bundle-template.md +4 -3
  185. package/resources/gofer/templates/contract-pack-template.md +8 -8
  186. package/resources/gofer/templates/cto-architecture-summary-template.md +3 -3
  187. package/resources/gofer/templates/delivery-lineage-template.json +8 -0
  188. package/resources/gofer/templates/eai-preflight-template.md +109 -68
  189. package/resources/gofer/templates/goal-ledger-template.json +1 -1
  190. package/resources/gofer/templates/gofer-model-policy.yaml +28 -6
  191. package/resources/gofer/templates/loop-contract-template.json +3 -1
  192. package/resources/gofer/templates/plan-template.md +8 -7
  193. package/resources/gofer/templates/priority-plan-template.json +29 -0
  194. package/resources/gofer/templates/proposal-review-template.md +6 -3
  195. package/resources/gofer/templates/release-capability-ledger-template.md +24 -0
  196. package/resources/gofer/templates/research-template.md +7 -6
  197. package/resources/gofer/templates/spec-summary-template.md +1 -1
  198. package/resources/gofer/templates/spec-template.md +45 -6
  199. package/resources/gofer/templates/tasks-template.md +12 -3
  200. package/resources/gofer/templates/ui-preview-brief-template.md +55 -5
  201. package/resources/gofer/templates/ui-review-log-template.md +21 -3
  202. package/resources/gofer/templates/ui-show-and-tell-template.md +46 -0
  203. package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
  204. package/resources/gofer/templates/working-backwards-prfaq-template.md +9 -8
  205. package/resources/linked-sources.json +4 -4
  206. package/resources/gofer/agents-skills/0_gofer_start/SKILL.md +0 -1287
  207. package/resources/gofer/agents-skills/0a_problem_validation/SKILL.md +0 -429
  208. package/resources/gofer/agents-skills/10_gofer_cloud/SKILL.md +0 -635
  209. package/resources/gofer/agents-skills/1_gofer_research/SKILL.md +0 -1159
  210. package/resources/gofer/agents-skills/2_gofer_specify/SKILL.md +0 -860
  211. package/resources/gofer/agents-skills/3_gofer_plan/SKILL.md +0 -899
  212. package/resources/gofer/agents-skills/4_gofer_tasks/SKILL.md +0 -760
  213. package/resources/gofer/agents-skills/5_gofer_implement/SKILL.md +0 -944
  214. package/resources/gofer/agents-skills/6_gofer_validate/SKILL.md +0 -2302
  215. package/resources/gofer/agents-skills/7_gofer_save/SKILL.md +0 -423
  216. package/resources/gofer/agents-skills/7a_stakeholder_comms/SKILL.md +0 -508
  217. package/resources/gofer/agents-skills/8_gofer_branding/SKILL.md +0 -315
  218. package/resources/gofer/agents-skills/9_gofer_tests/SKILL.md +0 -558
  219. package/resources/gofer/agents-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
  220. package/resources/gofer/agents-skills/gofer_check_workspace/SKILL.md +0 -119
  221. package/resources/gofer/agents-skills/gofer_constitution/SKILL.md +0 -612
  222. package/resources/gofer/agents-skills/gofer_diagnose/SKILL.md +0 -114
  223. package/resources/gofer/agents-skills/gofer_eai_first_run/SKILL.md +0 -397
  224. package/resources/gofer/agents-skills/gofer_hydrate/SKILL.md +0 -561
  225. package/resources/gofer/agents-skills/gofer_personality/SKILL.md +0 -44
  226. package/resources/gofer/agents-skills/gofer_plan/SKILL.md +0 -40
  227. package/resources/gofer/agents-skills/gofer_side/SKILL.md +0 -45
  228. package/resources/gofer/agents-skills/gofer_spec_summary/SKILL.md +0 -112
  229. package/resources/gofer/agents-skills/gofer_tdd/SKILL.md +0 -112
  230. package/resources/gofer/agents-skills/gofer_vocabulary/SKILL.md +0 -112
  231. package/resources/gofer/agents-skills/gofer_zoom_out/SKILL.md +0 -112
  232. package/resources/gofer/claude-commands/0_gofer_start.md +0 -1282
  233. package/resources/gofer/claude-commands/0a_problem_validation.md +0 -424
  234. package/resources/gofer/claude-commands/10_gofer_cloud.md +0 -630
  235. package/resources/gofer/claude-commands/1_gofer_research.md +0 -1154
  236. package/resources/gofer/claude-commands/2_gofer_specify.md +0 -855
  237. package/resources/gofer/claude-commands/3_gofer_plan.md +0 -894
  238. package/resources/gofer/claude-commands/4_gofer_tasks.md +0 -755
  239. package/resources/gofer/claude-commands/5_gofer_implement.md +0 -939
  240. package/resources/gofer/claude-commands/6_gofer_validate.md +0 -2297
  241. package/resources/gofer/claude-commands/7_gofer_save.md +0 -418
  242. package/resources/gofer/claude-commands/7a_stakeholder_comms.md +0 -503
  243. package/resources/gofer/claude-commands/8_gofer_branding.md +0 -310
  244. package/resources/gofer/claude-commands/9_gofer_tests.md +0 -553
  245. package/resources/gofer/claude-commands/gofer_bootstrap_workspace.md +0 -143
  246. package/resources/gofer/claude-commands/gofer_check_workspace.md +0 -114
  247. package/resources/gofer/claude-commands/gofer_constitution.md +0 -607
  248. package/resources/gofer/claude-commands/gofer_diagnose.md +0 -109
  249. package/resources/gofer/claude-commands/gofer_eai_first_run.md +0 -392
  250. package/resources/gofer/claude-commands/gofer_hydrate.md +0 -556
  251. package/resources/gofer/claude-commands/gofer_personality.md +0 -39
  252. package/resources/gofer/claude-commands/gofer_plan.md +0 -35
  253. package/resources/gofer/claude-commands/gofer_side.md +0 -40
  254. package/resources/gofer/claude-commands/gofer_spec_summary.md +0 -107
  255. package/resources/gofer/claude-commands/gofer_tdd.md +0 -107
  256. package/resources/gofer/claude-commands/gofer_vocabulary.md +0 -107
  257. package/resources/gofer/claude-commands/gofer_zoom_out.md +0 -107
  258. package/resources/gofer/claude-skills/eai-gofer/SKILL.md +0 -56
  259. package/resources/gofer/copilot-prompts/0_gofer_start.prompt.md +0 -1308
  260. package/resources/gofer/copilot-prompts/0a_problem_validation.prompt.md +0 -429
  261. package/resources/gofer/copilot-prompts/10_gofer_cloud.prompt.md +0 -644
  262. package/resources/gofer/copilot-prompts/1_gofer_research.prompt.md +0 -1177
  263. package/resources/gofer/copilot-prompts/2_gofer_specify.prompt.md +0 -878
  264. package/resources/gofer/copilot-prompts/3_gofer_plan.prompt.md +0 -917
  265. package/resources/gofer/copilot-prompts/4_gofer_tasks.prompt.md +0 -778
  266. package/resources/gofer/copilot-prompts/5_gofer_implement.prompt.md +0 -962
  267. package/resources/gofer/copilot-prompts/6_gofer_validate.prompt.md +0 -2310
  268. package/resources/gofer/copilot-prompts/7_gofer_save.prompt.md +0 -433
  269. package/resources/gofer/copilot-prompts/7a_stakeholder_comms.prompt.md +0 -516
  270. package/resources/gofer/copilot-prompts/8_gofer_branding.prompt.md +0 -323
  271. package/resources/gofer/copilot-prompts/9_gofer_tests.prompt.md +0 -566
  272. package/resources/gofer/copilot-prompts/gofer_bootstrap_workspace.prompt.md +0 -161
  273. package/resources/gofer/copilot-prompts/gofer_check_workspace.prompt.md +0 -132
  274. package/resources/gofer/copilot-prompts/gofer_constitution.prompt.md +0 -621
  275. package/resources/gofer/copilot-prompts/gofer_diagnose.prompt.md +0 -127
  276. package/resources/gofer/copilot-prompts/gofer_eai_first_run.prompt.md +0 -410
  277. package/resources/gofer/copilot-prompts/gofer_hydrate.prompt.md +0 -571
  278. package/resources/gofer/copilot-prompts/gofer_personality.prompt.md +0 -57
  279. package/resources/gofer/copilot-prompts/gofer_plan.prompt.md +0 -53
  280. package/resources/gofer/copilot-prompts/gofer_side.prompt.md +0 -58
  281. package/resources/gofer/copilot-prompts/gofer_spec_summary.prompt.md +0 -125
  282. package/resources/gofer/copilot-prompts/gofer_tdd.prompt.md +0 -125
  283. package/resources/gofer/copilot-prompts/gofer_vocabulary.prompt.md +0 -125
  284. package/resources/gofer/copilot-prompts/gofer_zoom_out.prompt.md +0 -125
  285. package/resources/gofer/gemini/commands/gofer/0_gofer_start.md +0 -1282
  286. package/resources/gofer/gemini/commands/gofer/0_gofer_start.toml +0 -2
  287. package/resources/gofer/gemini/commands/gofer/0a_problem_validation.md +0 -424
  288. package/resources/gofer/gemini/commands/gofer/0a_problem_validation.toml +0 -2
  289. package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.md +0 -630
  290. package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.toml +0 -2
  291. package/resources/gofer/gemini/commands/gofer/1_gofer_research.md +0 -1154
  292. package/resources/gofer/gemini/commands/gofer/1_gofer_research.toml +0 -2
  293. package/resources/gofer/gemini/commands/gofer/2_gofer_specify.md +0 -855
  294. package/resources/gofer/gemini/commands/gofer/2_gofer_specify.toml +0 -2
  295. package/resources/gofer/gemini/commands/gofer/3_gofer_plan.md +0 -894
  296. package/resources/gofer/gemini/commands/gofer/3_gofer_plan.toml +0 -2
  297. package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.md +0 -755
  298. package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.toml +0 -2
  299. package/resources/gofer/gemini/commands/gofer/5_gofer_implement.md +0 -939
  300. package/resources/gofer/gemini/commands/gofer/5_gofer_implement.toml +0 -2
  301. package/resources/gofer/gemini/commands/gofer/6_gofer_validate.md +0 -2297
  302. package/resources/gofer/gemini/commands/gofer/6_gofer_validate.toml +0 -2
  303. package/resources/gofer/gemini/commands/gofer/7_gofer_save.md +0 -418
  304. package/resources/gofer/gemini/commands/gofer/7_gofer_save.toml +0 -2
  305. package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.md +0 -503
  306. package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.toml +0 -2
  307. package/resources/gofer/gemini/commands/gofer/8_gofer_branding.md +0 -310
  308. package/resources/gofer/gemini/commands/gofer/8_gofer_branding.toml +0 -2
  309. package/resources/gofer/gemini/commands/gofer/9_gofer_tests.md +0 -553
  310. package/resources/gofer/gemini/commands/gofer/9_gofer_tests.toml +0 -2
  311. package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.md +0 -143
  312. package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.toml +0 -2
  313. package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.md +0 -114
  314. package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.toml +0 -2
  315. package/resources/gofer/gemini/commands/gofer/gofer_constitution.md +0 -607
  316. package/resources/gofer/gemini/commands/gofer/gofer_constitution.toml +0 -2
  317. package/resources/gofer/gemini/commands/gofer/gofer_diagnose.md +0 -109
  318. package/resources/gofer/gemini/commands/gofer/gofer_diagnose.toml +0 -2
  319. package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.md +0 -392
  320. package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.toml +0 -2
  321. package/resources/gofer/gemini/commands/gofer/gofer_hydrate.md +0 -556
  322. package/resources/gofer/gemini/commands/gofer/gofer_hydrate.toml +0 -2
  323. package/resources/gofer/gemini/commands/gofer/gofer_personality.md +0 -39
  324. package/resources/gofer/gemini/commands/gofer/gofer_personality.toml +0 -2
  325. package/resources/gofer/gemini/commands/gofer/gofer_plan.md +0 -35
  326. package/resources/gofer/gemini/commands/gofer/gofer_plan.toml +0 -2
  327. package/resources/gofer/gemini/commands/gofer/gofer_side.md +0 -40
  328. package/resources/gofer/gemini/commands/gofer/gofer_side.toml +0 -2
  329. package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.md +0 -107
  330. package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.toml +0 -2
  331. package/resources/gofer/gemini/commands/gofer/gofer_tdd.md +0 -107
  332. package/resources/gofer/gemini/commands/gofer/gofer_tdd.toml +0 -2
  333. package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.md +0 -107
  334. package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.toml +0 -2
  335. package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.md +0 -107
  336. package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.toml +0 -2
  337. package/resources/gofer/github-skills/eai-gofer/SKILL.md +0 -56
  338. package/resources/gofer/system-skills/0_gofer_start/SKILL.md +0 -1287
  339. package/resources/gofer/system-skills/0a_problem_validation/SKILL.md +0 -429
  340. package/resources/gofer/system-skills/10_gofer_cloud/SKILL.md +0 -635
  341. package/resources/gofer/system-skills/1_gofer_research/SKILL.md +0 -1159
  342. package/resources/gofer/system-skills/2_gofer_specify/SKILL.md +0 -860
  343. package/resources/gofer/system-skills/3_gofer_plan/SKILL.md +0 -899
  344. package/resources/gofer/system-skills/4_gofer_tasks/SKILL.md +0 -760
  345. package/resources/gofer/system-skills/5_gofer_implement/SKILL.md +0 -944
  346. package/resources/gofer/system-skills/6_gofer_validate/SKILL.md +0 -2302
  347. package/resources/gofer/system-skills/7_gofer_save/SKILL.md +0 -423
  348. package/resources/gofer/system-skills/7a_stakeholder_comms/SKILL.md +0 -508
  349. package/resources/gofer/system-skills/8_gofer_branding/SKILL.md +0 -315
  350. package/resources/gofer/system-skills/9_gofer_tests/SKILL.md +0 -558
  351. package/resources/gofer/system-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
  352. package/resources/gofer/system-skills/gofer_check_workspace/SKILL.md +0 -119
  353. package/resources/gofer/system-skills/gofer_constitution/SKILL.md +0 -612
  354. package/resources/gofer/system-skills/gofer_diagnose/SKILL.md +0 -114
  355. package/resources/gofer/system-skills/gofer_eai_first_run/SKILL.md +0 -397
  356. package/resources/gofer/system-skills/gofer_hydrate/SKILL.md +0 -561
  357. package/resources/gofer/system-skills/gofer_personality/SKILL.md +0 -44
  358. package/resources/gofer/system-skills/gofer_plan/SKILL.md +0 -40
  359. package/resources/gofer/system-skills/gofer_side/SKILL.md +0 -45
  360. package/resources/gofer/system-skills/gofer_spec_summary/SKILL.md +0 -112
  361. package/resources/gofer/system-skills/gofer_tdd/SKILL.md +0 -112
  362. package/resources/gofer/system-skills/gofer_vocabulary/SKILL.md +0 -112
  363. package/resources/gofer/system-skills/gofer_zoom_out/SKILL.md +0 -112
  364. package/resources/gofer/templates/ui-approval-template.md +0 -32
@@ -0,0 +1,1212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { createWriteStream } from 'node:fs';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import net from 'node:net';
9
+ import { once } from 'node:events';
10
+
11
+ export const DEFAULT_APP_PREVIEW_PORT = 3001;
12
+ export const DEFAULT_PREVIEW_PORTS = [DEFAULT_APP_PREVIEW_PORT, 3000, 5173, 4173, 6006, 8080, 8000];
13
+ export const PREVIEW_SCRIPT_PRIORITY = ['dev', 'start', 'preview', 'serve', 'storybook', 'docs:dev'];
14
+ export const BUSINESS_SCENARIO_SCRIPT_PRIORITY = [
15
+ 'test:business-scenarios',
16
+ 'test:e2e',
17
+ 'test:playwright',
18
+ 'e2e',
19
+ 'playwright',
20
+ ];
21
+ export const BUSINESS_SCENARIO_MANIFEST = 'business-scenarios.json';
22
+ export const UI_REVIEW_LOG_COLUMNS = [
23
+ 'Time',
24
+ 'Change Trigger',
25
+ 'Command',
26
+ 'URL',
27
+ 'Browser Target',
28
+ 'Screenshot',
29
+ 'Package Lane',
30
+ 'Coupling Status',
31
+ 'Storybook Story IDs',
32
+ 'Theme Override Points',
33
+ 'Self-Review',
34
+ 'Stakeholder Feedback',
35
+ 'User Feedback Applied',
36
+ 'Open Issues',
37
+ ];
38
+
39
+ const __filename = fileURLToPath(import.meta.url);
40
+
41
+ export function detectPackageManagerFromNames(fileNames) {
42
+ const names = new Set(fileNames);
43
+ if (names.has('pnpm-lock.yaml')) return 'pnpm';
44
+ if (names.has('bun.lockb') || names.has('bun.lock')) return 'bun';
45
+ if (names.has('yarn.lock')) return 'yarn';
46
+ if (names.has('package-lock.json') || names.has('npm-shrinkwrap.json')) return 'npm';
47
+ return 'npm';
48
+ }
49
+
50
+ export async function detectPackageManager(workspaceRoot) {
51
+ const entries = await fs.readdir(workspaceRoot).catch(() => []);
52
+ return detectPackageManagerFromNames(entries);
53
+ }
54
+
55
+ export function selectPreviewScript(scripts = {}) {
56
+ for (const scriptName of PREVIEW_SCRIPT_PRIORITY) {
57
+ if (typeof scripts[scriptName] === 'string' && scripts[scriptName].trim()) {
58
+ return scriptName;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ export function selectBusinessScenarioScript(scripts = {}) {
65
+ for (const scriptName of BUSINESS_SCENARIO_SCRIPT_PRIORITY) {
66
+ if (typeof scripts[scriptName] === 'string' && scripts[scriptName].trim()) {
67
+ return scriptName;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ export function buildPackageScriptCommand(packageManager, scriptName) {
74
+ if (!scriptName) return null;
75
+ if (packageManager === 'pnpm') return `pnpm run ${scriptName}`;
76
+ if (packageManager === 'yarn') return `yarn ${scriptName}`;
77
+ if (packageManager === 'bun') return `bun run ${scriptName}`;
78
+ return `npm run ${scriptName}`;
79
+ }
80
+
81
+ async function fileExists(filePath) {
82
+ try {
83
+ await fs.access(filePath);
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ export async function discoverRepoRunnerCommand(
91
+ workspaceRoot,
92
+ port = DEFAULT_APP_PREVIEW_PORT,
93
+ platform = process.platform
94
+ ) {
95
+ const normalizedPort = Number(port);
96
+ if (!Number.isInteger(normalizedPort) || normalizedPort < 1 || normalizedPort > 65535) {
97
+ throw new Error(`Preview port is invalid: ${port}`);
98
+ }
99
+
100
+ if (platform === 'win32') {
101
+ if (await fileExists(path.join(workspaceRoot, 'run.bat'))) {
102
+ return {
103
+ command: `run.bat dev ${normalizedPort}`,
104
+ source: 'repo-runner',
105
+ packageManager: null,
106
+ scriptName: null,
107
+ };
108
+ }
109
+ if (await fileExists(path.join(workspaceRoot, 'run.ps1'))) {
110
+ return {
111
+ command: `powershell -NoProfile -ExecutionPolicy Bypass -File .\\run.ps1 dev ${normalizedPort}`,
112
+ source: 'repo-runner',
113
+ packageManager: null,
114
+ scriptName: null,
115
+ };
116
+ }
117
+ }
118
+
119
+ if (await fileExists(path.join(workspaceRoot, 'run.sh'))) {
120
+ return {
121
+ command: `./run.sh dev ${normalizedPort}`,
122
+ source: 'repo-runner',
123
+ packageManager: null,
124
+ scriptName: null,
125
+ };
126
+ }
127
+
128
+ return {
129
+ command: null,
130
+ source: 'missing-repo-runner',
131
+ packageManager: null,
132
+ scriptName: null,
133
+ };
134
+ }
135
+
136
+ export async function discoverPreviewCommand(workspaceRoot, explicitCommand = null) {
137
+ if (explicitCommand?.trim()) {
138
+ return {
139
+ command: explicitCommand.trim(),
140
+ source: 'explicit',
141
+ packageManager: null,
142
+ scriptName: null,
143
+ };
144
+ }
145
+
146
+ const repoRunner = await discoverRepoRunnerCommand(workspaceRoot);
147
+ if (repoRunner.command) {
148
+ return repoRunner;
149
+ }
150
+
151
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
152
+ let packageJson;
153
+ try {
154
+ packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
155
+ } catch (error) {
156
+ if (error?.code === 'ENOENT') {
157
+ return {
158
+ command: null,
159
+ source: 'missing-package-json',
160
+ packageManager: null,
161
+ scriptName: null,
162
+ };
163
+ }
164
+ throw new Error(`Unable to read ${packageJsonPath}: ${error.message}`);
165
+ }
166
+
167
+ const scriptName = selectPreviewScript(packageJson.scripts);
168
+ const packageManager = await detectPackageManager(workspaceRoot);
169
+ return {
170
+ command: buildPackageScriptCommand(packageManager, scriptName),
171
+ source: scriptName ? 'package-json' : 'missing-preview-script',
172
+ packageManager,
173
+ scriptName,
174
+ };
175
+ }
176
+
177
+ export function isBrowserScenarioCommand(command = '') {
178
+ return /\b(playwright|cypress|puppeteer|webdriver|browser)\b/i.test(command);
179
+ }
180
+
181
+ export function validateBusinessScenarioManifest(manifest) {
182
+ const errors = [];
183
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
184
+ return { valid: false, errors: ['Manifest must be a JSON object.'], scenarioCount: 0 };
185
+ }
186
+ if (manifest.schemaVersion !== '1.0') {
187
+ errors.push('schemaVersion must be "1.0".');
188
+ }
189
+ if (!Array.isArray(manifest.scenarios) || manifest.scenarios.length === 0) {
190
+ errors.push('scenarios must contain at least one business journey.');
191
+ }
192
+
193
+ const ids = new Set();
194
+ for (const [index, scenario] of (manifest.scenarios ?? []).entries()) {
195
+ const label = `scenarios[${index}]`;
196
+ if (!scenario?.id || typeof scenario.id !== 'string') {
197
+ errors.push(`${label}.id is required.`);
198
+ } else if (ids.has(scenario.id)) {
199
+ errors.push(`${label}.id duplicates "${scenario.id}".`);
200
+ } else {
201
+ ids.add(scenario.id);
202
+ }
203
+ if (!scenario?.userStory || typeof scenario.userStory !== 'string') {
204
+ errors.push(`${label}.userStory is required.`);
205
+ }
206
+ if (!Array.isArray(scenario?.screens) || scenario.screens.length === 0) {
207
+ errors.push(`${label}.screens must name at least one screen.`);
208
+ }
209
+ if (!Array.isArray(scenario?.testFiles) || scenario.testFiles.length === 0) {
210
+ errors.push(`${label}.testFiles must name at least one executable browser test.`);
211
+ }
212
+ }
213
+
214
+ return {
215
+ valid: errors.length === 0,
216
+ errors,
217
+ scenarioCount: Array.isArray(manifest.scenarios) ? manifest.scenarios.length : 0,
218
+ };
219
+ }
220
+
221
+ export async function loadBusinessScenarioManifest(workspaceRoot, featureDir) {
222
+ if (!featureDir) {
223
+ return {
224
+ path: null,
225
+ exists: false,
226
+ valid: false,
227
+ errors: ['A feature directory is required for business-scenario validation.'],
228
+ scenarioCount: 0,
229
+ manifest: null,
230
+ };
231
+ }
232
+
233
+ const manifestPath = path.join(featureDir, BUSINESS_SCENARIO_MANIFEST);
234
+ let manifest;
235
+ try {
236
+ manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
237
+ } catch (error) {
238
+ return {
239
+ path: manifestPath,
240
+ exists: error?.code !== 'ENOENT',
241
+ valid: false,
242
+ errors: [
243
+ error?.code === 'ENOENT'
244
+ ? `${BUSINESS_SCENARIO_MANIFEST} is missing.`
245
+ : `${BUSINESS_SCENARIO_MANIFEST} is invalid JSON: ${error.message}`,
246
+ ],
247
+ scenarioCount: 0,
248
+ manifest: null,
249
+ };
250
+ }
251
+
252
+ const validation = validateBusinessScenarioManifest(manifest);
253
+ const errors = [...validation.errors];
254
+ const rootPrefix = `${path.resolve(workspaceRoot)}${path.sep}`;
255
+ for (const scenario of manifest.scenarios ?? []) {
256
+ for (const testFile of scenario.testFiles ?? []) {
257
+ const resolvedTestFile = path.resolve(workspaceRoot, testFile);
258
+ if (
259
+ resolvedTestFile !== path.resolve(workspaceRoot) &&
260
+ !resolvedTestFile.startsWith(rootPrefix)
261
+ ) {
262
+ errors.push(`${scenario.id} test file escapes the workspace: ${testFile}`);
263
+ continue;
264
+ }
265
+ try {
266
+ await fs.access(resolvedTestFile);
267
+ } catch {
268
+ errors.push(`${scenario.id} test file is missing: ${testFile}`);
269
+ }
270
+ }
271
+ }
272
+
273
+ return {
274
+ path: manifestPath,
275
+ exists: true,
276
+ valid: errors.length === 0,
277
+ errors,
278
+ scenarioCount: validation.scenarioCount,
279
+ manifest,
280
+ };
281
+ }
282
+
283
+ export async function discoverBusinessScenarioCommand(
284
+ workspaceRoot,
285
+ explicitCommand = null,
286
+ manifestCommand = null
287
+ ) {
288
+ const selectedExplicitCommand = explicitCommand?.trim() || manifestCommand?.trim();
289
+ if (selectedExplicitCommand) {
290
+ let browserRunner = isBrowserScenarioCommand(selectedExplicitCommand);
291
+ if (!browserRunner) {
292
+ const scriptMatch = selectedExplicitCommand.match(
293
+ /^(?:npm|pnpm|bun)\s+run\s+([^\s]+)|^yarn\s+([^\s]+)/
294
+ );
295
+ const scriptName = scriptMatch?.[1] ?? scriptMatch?.[2];
296
+ if (scriptName) {
297
+ try {
298
+ const packageJson = JSON.parse(
299
+ await fs.readFile(path.join(workspaceRoot, 'package.json'), 'utf8')
300
+ );
301
+ browserRunner = isBrowserScenarioCommand(packageJson.scripts?.[scriptName] ?? '');
302
+ } catch {
303
+ browserRunner = false;
304
+ }
305
+ }
306
+ }
307
+ return {
308
+ command: selectedExplicitCommand,
309
+ source: explicitCommand?.trim() ? 'explicit' : 'manifest',
310
+ packageManager: null,
311
+ scriptName: null,
312
+ browserRunner,
313
+ };
314
+ }
315
+
316
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
317
+ let packageJson;
318
+ try {
319
+ packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
320
+ } catch (error) {
321
+ return {
322
+ command: null,
323
+ source: error?.code === 'ENOENT' ? 'missing-package-json' : 'invalid-package-json',
324
+ packageManager: null,
325
+ scriptName: null,
326
+ browserRunner: false,
327
+ };
328
+ }
329
+
330
+ const scriptName = selectBusinessScenarioScript(packageJson.scripts);
331
+ const packageManager = await detectPackageManager(workspaceRoot);
332
+ const command = buildPackageScriptCommand(packageManager, scriptName);
333
+ return {
334
+ command,
335
+ source: scriptName ? 'package-json' : 'missing-business-scenario-script',
336
+ packageManager,
337
+ scriptName,
338
+ browserRunner: isBrowserScenarioCommand(packageJson.scripts?.[scriptName] ?? ''),
339
+ };
340
+ }
341
+
342
+ export function extractPortsFromCommand(command = '') {
343
+ const ports = new Set();
344
+ const patterns = [
345
+ /\bPORT=(\d{2,5})\b/g,
346
+ /(?:^|\s)--port(?:=|\s+)(\d{2,5})\b/g,
347
+ /(?:^|\s)-p(?:=|\s+)(\d{2,5})\b/g,
348
+ /(?:^|\s)(?:\.\/|\.\\)?run\.(?:sh|bat|ps1)(?:\s+\S+)?\s+(\d{2,5})\b/g,
349
+ /\blocalhost:(\d{2,5})\b/g,
350
+ /\b127\.0\.0\.1:(\d{2,5})\b/g,
351
+ ];
352
+
353
+ for (const pattern of patterns) {
354
+ for (const match of command.matchAll(pattern)) {
355
+ const port = Number(match[1]);
356
+ if (Number.isInteger(port) && port > 0 && port <= 65535) {
357
+ ports.add(port);
358
+ }
359
+ }
360
+ }
361
+
362
+ return [...ports];
363
+ }
364
+
365
+ export function buildCandidateUrls({ explicitUrl = null, command = null, ports = DEFAULT_PREVIEW_PORTS } = {}) {
366
+ if (explicitUrl?.trim()) return [sanitizePreviewUrl(explicitUrl)];
367
+
368
+ const commandPorts = extractPortsFromCommand(command ?? '');
369
+ // Do not mistake another app on a fallback port for this preview.
370
+ const orderedPorts = (commandPorts.length ? commandPorts : ports).slice(0, 1);
371
+ return orderedPorts.map((port) => `http://localhost:${port}`);
372
+ }
373
+
374
+ export function buildOpenBrowserCommand(url, platform = process.platform) {
375
+ const safeUrl = sanitizePreviewUrl(url);
376
+ if (platform === 'darwin') return { command: 'open', args: [safeUrl] };
377
+ if (platform === 'win32') return { command: 'explorer.exe', args: [safeUrl] };
378
+ return { command: 'xdg-open', args: [safeUrl] };
379
+ }
380
+
381
+ function isLoopbackHostname(hostname) {
382
+ const normalized = hostname.toLowerCase();
383
+ return (
384
+ normalized === 'localhost' ||
385
+ normalized.endsWith('.localhost') ||
386
+ normalized === '127.0.0.1' ||
387
+ normalized === '::1' ||
388
+ normalized === '[::1]'
389
+ );
390
+ }
391
+
392
+ export function sanitizePreviewUrl(value) {
393
+ let parsed;
394
+ try {
395
+ parsed = new URL(String(value ?? '').trim());
396
+ } catch {
397
+ throw new Error(`Preview URL is invalid: ${value}`);
398
+ }
399
+
400
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
401
+ throw new Error(`Preview URL must use http or https: ${parsed.protocol}`);
402
+ }
403
+ if (!isLoopbackHostname(parsed.hostname)) {
404
+ throw new Error(`Preview URL must be a local loopback URL: ${parsed.hostname}`);
405
+ }
406
+
407
+ parsed.username = '';
408
+ parsed.password = '';
409
+ return parsed.href;
410
+ }
411
+
412
+ function resolveRedirectTarget(baseUrl, location) {
413
+ try {
414
+ return new URL(location, baseUrl).href;
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+
420
+ function isSafePreviewRedirect(baseUrl, location) {
421
+ const target = resolveRedirectTarget(baseUrl, location);
422
+ if (!target) return false;
423
+ try {
424
+ sanitizePreviewUrl(target);
425
+ return true;
426
+ } catch {
427
+ return false;
428
+ }
429
+ }
430
+
431
+ export function markdownCell(value) {
432
+ return String(value ?? '')
433
+ .replace(/\r\n/g, '\n')
434
+ .replace(/[\\|\r\n]/g, (character) => {
435
+ if (character === '\\') return '\\\\';
436
+ if (character === '|') return '\\|';
437
+ return '<br>';
438
+ })
439
+ .trim();
440
+ }
441
+
442
+ export function resolveFeatureLogPaths(workspaceRoot, featureDir = null) {
443
+ const baseDir = featureDir
444
+ ? path.resolve(workspaceRoot, featureDir)
445
+ : path.join(workspaceRoot, '.specify', 'logs', 'ui-preview');
446
+ return {
447
+ baseDir,
448
+ previewDir: featureDir ? path.join(baseDir, 'preview') : baseDir,
449
+ reviewLogPath: featureDir ? path.join(baseDir, 'ui-review-log.md') : null,
450
+ processLogPath: path.join(baseDir, 'preview-server.log'),
451
+ pidPath: path.join(baseDir, 'preview-server.pid'),
452
+ scenarioLogPath: path.join(baseDir, 'business-scenarios.log'),
453
+ scenarioReportPath: path.join(baseDir, 'business-scenario-report.json'),
454
+ };
455
+ }
456
+
457
+ export async function waitForReachableUrl(urls, timeoutMs = 45000, intervalMs = 750) {
458
+ const safeUrls = urls.map((url) => sanitizePreviewUrl(url));
459
+ const deadline = Date.now() + timeoutMs;
460
+ let lastError = null;
461
+
462
+ while (Date.now() <= deadline) {
463
+ for (let index = 0; index < safeUrls.length; index += 1) {
464
+ const url = safeUrls[index];
465
+ let timer = null;
466
+ try {
467
+ const controller = new AbortController();
468
+ timer = setTimeout(() => controller.abort(), Math.min(intervalMs, 5000));
469
+ const response = await fetch(url, {
470
+ method: 'GET',
471
+ redirect: 'manual',
472
+ signal: controller.signal,
473
+ });
474
+ if (response.status >= 300 && response.status < 400) {
475
+ const location = response.headers.get('location');
476
+ if (location && !isSafePreviewRedirect(url, location)) {
477
+ lastError = 'Unsafe redirect from local preview URL';
478
+ continue;
479
+ }
480
+ }
481
+ if (response.status >= 200 && response.status < 300) {
482
+ return { ok: true, urlIndex: index, status: response.status };
483
+ }
484
+ lastError = `HTTP ${response.status} from ${url}`;
485
+ } catch (error) {
486
+ lastError = `${url}: ${error.name === 'AbortError' ? 'timeout' : error.message}`;
487
+ } finally {
488
+ if (timer) clearTimeout(timer);
489
+ }
490
+ }
491
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
492
+ }
493
+
494
+ return { ok: false, urlIndex: null, status: null, error: lastError ?? 'No candidate URL responded' };
495
+ }
496
+
497
+ export async function checkPreviewPortAvailable(url) {
498
+ const parsed = new URL(sanitizePreviewUrl(url));
499
+ const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
500
+ const connect = host => new Promise(resolve => {
501
+ const socket = net.connect({ host, port });
502
+ const finish = result => { socket.destroy(); resolve(result); };
503
+ socket.setTimeout(1000, () => finish({ ok: false, code: 'PORT_CHECK_TIMEOUT' }));
504
+ socket.once('connect', () => finish({ ok: false, code: 'EADDRINUSE' }));
505
+ socket.once('error', error => finish(['ECONNREFUSED', 'EAFNOSUPPORT', 'EADDRNOTAVAIL', 'ENETUNREACH'].includes(error.code)
506
+ ? { ok: true } : { ok: false, code: error.code }));
507
+ });
508
+ for (const host of ['127.0.0.1', '::1']) {
509
+ const result = await connect(host);
510
+ if (!result.ok) return result;
511
+ }
512
+ const probe = host => new Promise(resolve => {
513
+ const server = net.createServer();
514
+ server.once('error', error => resolve({ ok: false, code: error.code }));
515
+ server.listen({ host, port, exclusive: true }, () => server.close(() => resolve({ ok: true })));
516
+ });
517
+ // Probe both families. Refuse ambiguous failures rather than launching a runner
518
+ // that could terminate another app. Runners must also verify ownership themselves.
519
+ for (const host of ['0.0.0.0', '::']) {
520
+ const result = await probe(host);
521
+ if (!result.ok && !['EAFNOSUPPORT', 'EADDRNOTAVAIL'].includes(result.code)) return result;
522
+ }
523
+ return { ok: true };
524
+ }
525
+
526
+ async function startPreviewServer(command, workspaceRoot, logPath, pidPath) {
527
+ await fs.mkdir(path.dirname(logPath), { recursive: true });
528
+ const out = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
529
+ await once(out, 'open');
530
+ out.write(`\n\n[${new Date().toISOString()}] Starting Gofer UI preview: ${command}\n`);
531
+
532
+ const child = spawn(command, {
533
+ cwd: workspaceRoot,
534
+ env: {
535
+ ...process.env,
536
+ BROWSER: process.env.BROWSER ?? 'none',
537
+ },
538
+ shell: true,
539
+ detached: true,
540
+ stdio: ['ignore', out, out],
541
+ windowsHide: true,
542
+ });
543
+
544
+ try { await once(child, 'spawn'); }
545
+ finally { out.end(); }
546
+ child.unref();
547
+ await fs.writeFile(pidPath, `${child.pid}\n`, 'utf8');
548
+ return { pid: child.pid, logPath, pidPath };
549
+ }
550
+
551
+ async function runBusinessScenarioCommand(command, workspaceRoot, logPath, timeoutMs = 300000) {
552
+ await fs.mkdir(path.dirname(logPath), { recursive: true });
553
+ const out = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
554
+ out.write(`\n\n[${new Date().toISOString()}] Running business scenarios: ${command}\n`);
555
+
556
+ return await new Promise((resolve) => {
557
+ const child = spawn(command, {
558
+ cwd: workspaceRoot,
559
+ env: {
560
+ ...process.env,
561
+ CI: process.env.CI ?? '1',
562
+ },
563
+ shell: true,
564
+ stdio: ['ignore', 'pipe', 'pipe'],
565
+ windowsHide: true,
566
+ });
567
+ child.stdout.pipe(out, { end: false });
568
+ child.stderr.pipe(out, { end: false });
569
+ let settled = false;
570
+ let timer = null;
571
+ const finish = (report) => {
572
+ if (settled) return;
573
+ settled = true;
574
+ if (timer) clearTimeout(timer);
575
+ out.end();
576
+ resolve(report);
577
+ };
578
+ timer = setTimeout(() => {
579
+ child.kill('SIGTERM');
580
+ finish({
581
+ ok: false,
582
+ status: 'failed',
583
+ exitCode: null,
584
+ error: `Business-scenario command exceeded ${timeoutMs}ms.`,
585
+ logPath,
586
+ });
587
+ }, timeoutMs);
588
+
589
+ child.once('error', (error) => {
590
+ finish({
591
+ ok: false,
592
+ status: 'failed',
593
+ exitCode: null,
594
+ error: error.message,
595
+ logPath,
596
+ });
597
+ });
598
+ child.once('exit', (code, signal) => {
599
+ finish({
600
+ ok: code === 0,
601
+ status: code === 0 ? 'passed' : 'failed',
602
+ exitCode: code,
603
+ error:
604
+ code === 0
605
+ ? null
606
+ : `Business-scenario command exited with ${
607
+ code === null ? `signal ${signal}` : `code ${code}`
608
+ }.`,
609
+ logPath,
610
+ });
611
+ });
612
+ });
613
+ }
614
+
615
+ async function openBrowser(url, mode) {
616
+ if (mode === 'none') {
617
+ return { attempted: false, ok: true, reason: 'open disabled' };
618
+ }
619
+
620
+ const { command, args } = buildOpenBrowserCommand(url);
621
+ try {
622
+ return await new Promise((resolve) => {
623
+ let settled = false;
624
+ const child = spawn(command, args, {
625
+ stdio: 'ignore',
626
+ windowsHide: true,
627
+ });
628
+
629
+ const finish = (result) => {
630
+ if (settled) return;
631
+ settled = true;
632
+ resolve({
633
+ attempted: true,
634
+ command: [command, ...args].join(' '),
635
+ ...result,
636
+ });
637
+ };
638
+
639
+ const timer = setTimeout(() => {
640
+ child.unref();
641
+ finish({ ok: true, reason: 'browser opener still running after launch window' });
642
+ }, 1500);
643
+
644
+ child.once('error', (error) => {
645
+ clearTimeout(timer);
646
+ finish({ ok: false, error: error.message });
647
+ });
648
+
649
+ child.once('exit', (code, signal) => {
650
+ clearTimeout(timer);
651
+ if (code === 0) {
652
+ finish({ ok: true, reason: 'browser opener exited successfully' });
653
+ return;
654
+ }
655
+ finish({
656
+ ok: false,
657
+ error: `browser opener exited with ${code === null ? `signal ${signal}` : `code ${code}`}`,
658
+ });
659
+ });
660
+ });
661
+ } catch (error) {
662
+ return { attempted: true, ok: false, error: error.message };
663
+ }
664
+ }
665
+
666
+ export async function captureScreenshot(url, outputPath, engine = null) {
667
+ const safeUrl = sanitizePreviewUrl(url);
668
+ let chromium;
669
+ try {
670
+ chromium = engine ?? (await import('playwright')).chromium;
671
+ } catch (error) {
672
+ return {
673
+ ok: false,
674
+ path: null,
675
+ error: `Playwright is not available: ${error.message}`,
676
+ };
677
+ }
678
+
679
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
680
+ let browser;
681
+ try {
682
+ browser = await chromium.launch({ headless: true });
683
+ const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
684
+ const pageErrors = [];
685
+ page.on('pageerror', () => pageErrors.push('Page script failed'));
686
+ const response = await page.goto(safeUrl, { waitUntil: 'networkidle', timeout: 30000 });
687
+ sanitizePreviewUrl(page.url());
688
+ if (!response || !response.ok()) throw new Error('The preview page returned an error');
689
+ if (new URL(page.url()).origin !== new URL(safeUrl).origin) throw new Error('The preview moved to another app');
690
+ if (!(await page.locator('body').innerText()).trim() || pageErrors.length) throw new Error('The preview is empty or has a script error');
691
+ await page.screenshot({ path: outputPath, fullPage: true });
692
+ return { ok: true, path: outputPath };
693
+ } catch (error) {
694
+ return { ok: false, path: null, error: error.message };
695
+ } finally {
696
+ await browser?.close().catch(() => {});
697
+ }
698
+ }
699
+
700
+ export function buildReviewLogRow({
701
+ time = new Date().toISOString(),
702
+ change,
703
+ command,
704
+ url,
705
+ browserTarget,
706
+ screenshotPath,
707
+ packageLane = 'unspecified',
708
+ couplingStatus = 'pending service-fit validation',
709
+ storybookStoryIds = 'not recorded',
710
+ themeOverridePoints = 'not recorded',
711
+ selfReview,
712
+ stakeholderFeedback = 'pending',
713
+ changesAccepted = 'pending user feedback',
714
+ openIssues,
715
+ }) {
716
+ return [
717
+ time,
718
+ change,
719
+ command,
720
+ url,
721
+ browserTarget,
722
+ screenshotPath ?? 'not captured',
723
+ packageLane,
724
+ couplingStatus,
725
+ storybookStoryIds,
726
+ themeOverridePoints,
727
+ selfReview,
728
+ stakeholderFeedback,
729
+ changesAccepted,
730
+ openIssues,
731
+ ]
732
+ .map(markdownCell)
733
+ .join(' | ');
734
+ }
735
+
736
+ function buildReviewLogHeader() {
737
+ const separator = UI_REVIEW_LOG_COLUMNS.map(() => '---');
738
+ return [
739
+ '# UI Review Log',
740
+ '',
741
+ `| ${UI_REVIEW_LOG_COLUMNS.join(' | ')} |`,
742
+ `| ${separator.join(' | ')} |`,
743
+ ].join('\n') + '\n';
744
+ }
745
+
746
+ function buildPreviewSelfReview({ browser, screenshotPath, status, notes }) {
747
+ const browserStatus =
748
+ browser?.attempted === false
749
+ ? 'Browser opening disabled by option.'
750
+ : browser?.ok
751
+ ? 'Browser opener launched successfully.'
752
+ : `Browser opener failed: ${browser?.error ?? 'unknown error'}.`;
753
+ const screenshotStatus = screenshotPath
754
+ ? `Screenshot captured at ${screenshotPath}.`
755
+ : `Screenshot not captured: ${notes}`;
756
+ return `${browserStatus} ${screenshotStatus} Status: ${status}.`;
757
+ }
758
+
759
+ function buildPreviewOpenIssues({ browser, screenshotPath, status, notes }) {
760
+ const issues = [];
761
+ if (browser?.attempted !== false && !browser?.ok) {
762
+ issues.push('Browser opener failed; use URL manually or rerun with --no-open.');
763
+ }
764
+ if (!screenshotPath) {
765
+ issues.push(`Screenshot missing: ${notes}`);
766
+ }
767
+ if (status === 'blocked') {
768
+ issues.push('Preview or required business-scenario evidence is blocked; inspect the scenario report and preview log.');
769
+ }
770
+ if (status === 'unverified') issues.push('Current preview checks are incomplete; do not claim readiness.');
771
+ return issues.length > 0 ? issues.join(' ') : 'none';
772
+ }
773
+
774
+ function buildBrowserTargetLabel(openMode, browser) {
775
+ if (openMode === 'none') return 'none';
776
+ if (browser?.ok) return `${openMode} (${browser.reason ?? 'opened'})`;
777
+ return `${openMode} (failed)`;
778
+ }
779
+
780
+ async function appendReviewLog({
781
+ reviewLogPath,
782
+ change,
783
+ command,
784
+ url,
785
+ browserTarget,
786
+ screenshotPath,
787
+ status,
788
+ notes,
789
+ browser = null,
790
+ packageLane,
791
+ couplingStatus,
792
+ storybookStoryIds,
793
+ themeOverridePoints,
794
+ stakeholderFeedback,
795
+ changesAccepted,
796
+ }) {
797
+ if (!reviewLogPath) return null;
798
+
799
+ await fs.mkdir(path.dirname(reviewLogPath), { recursive: true });
800
+ const handle = await fs.open(reviewLogPath, 'a+', 0o600);
801
+ const header = buildReviewLogHeader();
802
+ const row = buildReviewLogRow({
803
+ change,
804
+ command,
805
+ url,
806
+ browserTarget,
807
+ screenshotPath,
808
+ packageLane,
809
+ couplingStatus,
810
+ storybookStoryIds,
811
+ themeOverridePoints,
812
+ selfReview: buildPreviewSelfReview({ browser, screenshotPath, status, notes }),
813
+ stakeholderFeedback,
814
+ changesAccepted,
815
+ openIssues: buildPreviewOpenIssues({ browser, screenshotPath, status, notes }),
816
+ });
817
+
818
+ try {
819
+ const stats = await handle.stat();
820
+ if (stats.size === 0) {
821
+ await handle.writeFile(header, 'utf8');
822
+ }
823
+ await handle.writeFile(`| ${row} |\n`, 'utf8');
824
+ } finally {
825
+ await handle.close();
826
+ }
827
+ return reviewLogPath;
828
+ }
829
+
830
+ export function parseArgs(argv) {
831
+ const options = {
832
+ workspace: process.cwd(),
833
+ featureDir: null,
834
+ command: null,
835
+ url: null,
836
+ open: 'auto',
837
+ screenshot: true,
838
+ timeoutMs: 45000,
839
+ scenarioTimeoutMs: 300000,
840
+ scenarioCommand: null,
841
+ scenarios: 'auto',
842
+ change: 'manual preview refresh',
843
+ json: false,
844
+ dryRun: false,
845
+ help: false,
846
+ };
847
+
848
+ for (let index = 0; index < argv.length; index += 1) {
849
+ const arg = argv[index];
850
+ const next = () => argv[++index];
851
+
852
+ if (arg === '--workspace') options.workspace = next();
853
+ else if (arg === '--feature-dir') options.featureDir = next();
854
+ else if (arg === '--command') options.command = next();
855
+ else if (arg === '--url') options.url = next();
856
+ else if (arg === '--open') options.open = next();
857
+ else if (arg === '--no-open') options.open = 'none';
858
+ else if (arg === '--screenshot') options.screenshot = true;
859
+ else if (arg === '--no-screenshot') options.screenshot = false;
860
+ else if (arg === '--timeout-ms') options.timeoutMs = Number(next());
861
+ else if (arg === '--scenario-timeout-ms') options.scenarioTimeoutMs = Number(next());
862
+ else if (arg === '--scenario-command') options.scenarioCommand = next();
863
+ else if (arg === '--require-scenarios') options.scenarios = 'required';
864
+ else if (arg === '--skip-scenarios') options.scenarios = 'skip';
865
+ else if (arg === '--change') options.change = next();
866
+ else if (arg === '--json') options.json = true;
867
+ else if (arg === '--dry-run') options.dryRun = true;
868
+ else if (arg === '--help' || arg === '-h') options.help = true;
869
+ else throw new Error(`Unknown option: ${arg}`);
870
+ }
871
+
872
+ if (!['auto', 'external', 'none'].includes(options.open)) {
873
+ throw new Error('--open must be auto, external, or none');
874
+ }
875
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1000) {
876
+ throw new Error('--timeout-ms must be at least 1000');
877
+ }
878
+ if (!Number.isFinite(options.scenarioTimeoutMs) || options.scenarioTimeoutMs < 1000) {
879
+ throw new Error('--scenario-timeout-ms must be at least 1000');
880
+ }
881
+
882
+ return options;
883
+ }
884
+
885
+ function helpText() {
886
+ return `Gofer UI Preview
887
+
888
+ Starts or reuses a local UI preview, opens it quickly, captures evidence when
889
+ Playwright is available, and appends ui-review-log.md for app-delivery features.
890
+
891
+ Usage:
892
+ node .specify/scripts/node/gofer-ui-preview.mjs [options]
893
+
894
+ Options:
895
+ --workspace <path> Workspace root. Defaults to cwd.
896
+ --feature-dir <path> Feature artifact directory, e.g. .specify/specs/my-feature.
897
+ --command <cmd> Explicit preview command, e.g. "./run.sh dev 3001".
898
+ --url <url> Existing preview URL. Skips server startup.
899
+ --open <auto|external|none>
900
+ Open URL in the host/default browser. Defaults to auto.
901
+ --no-open Do not open a browser.
902
+ --screenshot Capture screenshot evidence with Playwright. Default.
903
+ --no-screenshot Skip screenshot capture.
904
+ --timeout-ms <ms> URL readiness timeout. Defaults to 45000.
905
+ --scenario-command <cmd> Executable browser business-scenario command.
906
+ --require-scenarios Block unless a valid manifest and browser suite pass.
907
+ This is automatic when --feature-dir is present.
908
+ --skip-scenarios Explicitly skip scenario execution (early red work only).
909
+ --scenario-timeout-ms <ms>
910
+ Scenario timeout. Defaults to 300000.
911
+ --change <text> Change trigger recorded in ui-review-log.md.
912
+ --json Print machine-readable output.
913
+ --dry-run Detect command/URLs only; do not start/open/capture.
914
+ `;
915
+ }
916
+
917
+ export async function runUiPreview(rawOptions) {
918
+ const workspaceRoot = path.resolve(rawOptions.workspace);
919
+ const paths = resolveFeatureLogPaths(workspaceRoot, rawOptions.featureDir);
920
+ const featureDir = rawOptions.featureDir
921
+ ? path.resolve(workspaceRoot, rawOptions.featureDir)
922
+ : null;
923
+ const scenariosRequired =
924
+ rawOptions.scenarios !== 'skip' &&
925
+ (rawOptions.scenarios === 'required' || Boolean(featureDir));
926
+ const scenarioManifest =
927
+ rawOptions.scenarios === 'skip'
928
+ ? null
929
+ : await loadBusinessScenarioManifest(workspaceRoot, featureDir);
930
+ const scenarioCommandInfo =
931
+ rawOptions.scenarios === 'skip'
932
+ ? null
933
+ : await discoverBusinessScenarioCommand(
934
+ workspaceRoot,
935
+ rawOptions.scenarioCommand,
936
+ scenarioManifest?.manifest?.command
937
+ );
938
+ const commandInfo = rawOptions.url
939
+ ? {
940
+ command: rawOptions.command?.trim() || null,
941
+ source: 'explicit-url',
942
+ packageManager: null,
943
+ scriptName: null,
944
+ }
945
+ : await discoverPreviewCommand(workspaceRoot, rawOptions.command);
946
+ const candidateUrls = buildCandidateUrls({
947
+ explicitUrl: rawOptions.url,
948
+ command: commandInfo.command,
949
+ });
950
+
951
+ const baseReport = {
952
+ status: 'planned',
953
+ readyToShow: false,
954
+ workspaceRoot,
955
+ featureDir,
956
+ command: commandInfo.command,
957
+ commandSource: commandInfo.source,
958
+ candidateUrls,
959
+ selectedUrl: null,
960
+ browser: null,
961
+ screenshot: null,
962
+ reviewLogPath: paths.reviewLogPath,
963
+ server: null,
964
+ businessScenarios: {
965
+ required: scenariosRequired,
966
+ manifest: scenarioManifest,
967
+ command: scenarioCommandInfo,
968
+ run: null,
969
+ reportPath: paths.scenarioReportPath,
970
+ },
971
+ nextActions: [],
972
+ };
973
+
974
+ if (rawOptions.dryRun) {
975
+ const scenarioReady =
976
+ !scenariosRequired ||
977
+ (scenarioManifest?.valid &&
978
+ scenarioCommandInfo?.command &&
979
+ scenarioCommandInfo?.browserRunner);
980
+ return {
981
+ ...baseReport,
982
+ status:
983
+ (commandInfo.command || rawOptions.url) && scenarioReady ? 'planned' : 'blocked',
984
+ nextActions:
985
+ (commandInfo.command || rawOptions.url) && scenarioReady
986
+ ? ['Run without --dry-run after a UI-facing change.']
987
+ : [
988
+ 'Provide a preview command/URL and a valid business-scenarios.json.',
989
+ 'Add a Playwright/Cypress/browser package script or pass --scenario-command.',
990
+ ],
991
+ };
992
+ }
993
+
994
+ if (!rawOptions.url && !commandInfo.command) {
995
+ return {
996
+ ...baseReport,
997
+ status: 'blocked',
998
+ nextActions: [
999
+ 'Pass --command or --url, add run.sh/run.bat, or add a package.json dev/start/preview script.',
1000
+ 'Record the chosen preview command in ui-preview-brief.md.',
1001
+ ],
1002
+ };
1003
+ }
1004
+
1005
+ let server = null;
1006
+ if (!rawOptions.url) {
1007
+ const portCheck = await checkPreviewPortAvailable(candidateUrls[0]);
1008
+ if (!portCheck.ok) {
1009
+ return { ...baseReport, status: 'blocked', portCheck,
1010
+ nextActions: ['The chosen connection is in use or cannot be checked. No app was stopped and no runner was started.',
1011
+ 'Verify that the process belongs to this exact app before restarting it. Leave other or unknown apps running and ask the user.',
1012
+ 'Use --url only for an existing preview that you have confirmed belongs to this app.'] };
1013
+ }
1014
+ server = await startPreviewServer(commandInfo.command, workspaceRoot, paths.processLogPath, paths.pidPath);
1015
+ }
1016
+
1017
+ const readiness = await waitForReachableUrl(candidateUrls, rawOptions.timeoutMs);
1018
+ if (!readiness.ok) {
1019
+ const report = {
1020
+ ...baseReport,
1021
+ status: 'blocked',
1022
+ server,
1023
+ nextActions: [
1024
+ `No preview URL became reachable. Check ${paths.processLogPath}.`,
1025
+ 'Pass --url when the app uses a non-standard host or port.',
1026
+ ],
1027
+ };
1028
+ await appendReviewLog({
1029
+ reviewLogPath: paths.reviewLogPath,
1030
+ change: rawOptions.change,
1031
+ command: commandInfo.command ?? 'existing URL',
1032
+ url: candidateUrls.join(', '),
1033
+ browserTarget: rawOptions.open,
1034
+ screenshotPath: null,
1035
+ status: 'blocked',
1036
+ notes: 'No candidate URL became reachable before timeout.',
1037
+ browser: {
1038
+ attempted: rawOptions.open !== 'none',
1039
+ ok: false,
1040
+ error: 'preview URL not reachable',
1041
+ },
1042
+ });
1043
+ return report;
1044
+ }
1045
+
1046
+ const selectedUrl = candidateUrls[readiness.urlIndex];
1047
+ const browser = await openBrowser(selectedUrl, rawOptions.open);
1048
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1049
+ let screenshot = { ok: false, path: null, error: 'Screenshot disabled' };
1050
+ if (rawOptions.screenshot) {
1051
+ screenshot = await captureScreenshot(
1052
+ selectedUrl,
1053
+ path.join(paths.previewDir, `ui-preview-${timestamp}.png`)
1054
+ );
1055
+ }
1056
+
1057
+ let scenarioRun = {
1058
+ ok: rawOptions.scenarios === 'skip',
1059
+ status: rawOptions.scenarios === 'skip' ? 'skipped' : 'not-run',
1060
+ exitCode: null,
1061
+ error: null,
1062
+ logPath: null,
1063
+ };
1064
+ const scenarioPreflightErrors = [];
1065
+ if (rawOptions.scenarios !== 'skip') {
1066
+ if (!scenarioManifest?.valid) {
1067
+ scenarioPreflightErrors.push(...(scenarioManifest?.errors ?? []));
1068
+ }
1069
+ if (!scenarioCommandInfo?.command) {
1070
+ scenarioPreflightErrors.push('No executable business-scenario command was found.');
1071
+ } else if (!scenarioCommandInfo.browserRunner) {
1072
+ scenarioPreflightErrors.push(
1073
+ 'The business-scenario command does not use a recognized browser runner.'
1074
+ );
1075
+ }
1076
+
1077
+ if (scenarioPreflightErrors.length === 0) {
1078
+ scenarioRun = await runBusinessScenarioCommand(
1079
+ scenarioCommandInfo.command,
1080
+ workspaceRoot,
1081
+ paths.scenarioLogPath,
1082
+ rawOptions.scenarioTimeoutMs
1083
+ );
1084
+ } else {
1085
+ scenarioRun = {
1086
+ ok: false,
1087
+ status: 'blocked',
1088
+ exitCode: null,
1089
+ error: scenarioPreflightErrors.join(' '),
1090
+ logPath: null,
1091
+ };
1092
+ }
1093
+ }
1094
+
1095
+ const businessScenarios = {
1096
+ required: scenariosRequired,
1097
+ manifest: scenarioManifest,
1098
+ command: scenarioCommandInfo,
1099
+ run: scenarioRun,
1100
+ reportPath: paths.scenarioReportPath,
1101
+ };
1102
+ await fs.mkdir(path.dirname(paths.scenarioReportPath), { recursive: true });
1103
+ await fs.writeFile(
1104
+ paths.scenarioReportPath,
1105
+ `${JSON.stringify(
1106
+ {
1107
+ generatedAt: new Date().toISOString(),
1108
+ workspaceRoot,
1109
+ featureDir,
1110
+ selectedUrl,
1111
+ status: scenarioRun.status === 'passed' ? 'passed' : scenarioRun.status === 'skipped' ? 'skipped' : 'blocked',
1112
+ manifestPath: scenarioManifest?.path ?? null,
1113
+ scenarioCount: scenarioManifest?.scenarioCount ?? 0,
1114
+ command: scenarioCommandInfo?.command ?? null,
1115
+ browserRunner: scenarioCommandInfo?.browserRunner ?? false,
1116
+ run: scenarioRun,
1117
+ },
1118
+ null,
1119
+ 2
1120
+ )}\n`,
1121
+ { encoding: 'utf8', mode: 0o600 }
1122
+ );
1123
+
1124
+ const scenarioBlocked =
1125
+ scenariosRequired &&
1126
+ (!scenarioManifest?.valid ||
1127
+ !scenarioCommandInfo?.command ||
1128
+ !scenarioCommandInfo?.browserRunner ||
1129
+ !scenarioRun.ok);
1130
+ const readyToShow = readiness.ok && screenshot.ok && scenarioRun.status === 'passed';
1131
+ const previewStatus = readyToShow ? (browser.attempted && browser.ok ? 'shown' : 'verified') : 'unverified';
1132
+ const status = scenarioBlocked ? 'blocked' : previewStatus;
1133
+
1134
+ const reviewLogPath = await appendReviewLog({
1135
+ reviewLogPath: paths.reviewLogPath,
1136
+ change: rawOptions.change,
1137
+ command: commandInfo.command ?? 'existing URL',
1138
+ url: selectedUrl,
1139
+ browserTarget: buildBrowserTargetLabel(rawOptions.open, browser),
1140
+ screenshotPath: screenshot.path,
1141
+ status,
1142
+ notes: [
1143
+ screenshot.ok ? 'Preview opened and screenshot captured.' : screenshot.error,
1144
+ `Business scenarios: ${scenarioRun.status}.`,
1145
+ scenarioRun.error,
1146
+ ]
1147
+ .filter(Boolean)
1148
+ .join(' '),
1149
+ browser,
1150
+ });
1151
+
1152
+ return {
1153
+ ...baseReport,
1154
+ status,
1155
+ readyToShow: !scenarioBlocked && readyToShow,
1156
+ selectedUrl,
1157
+ browser,
1158
+ screenshot,
1159
+ reviewLogPath,
1160
+ server,
1161
+ businessScenarios,
1162
+ nextActions: [
1163
+ readyToShow ? 'Report the checked preview URL, evidence and behaviour tested; do not claim the whole feature is complete.'
1164
+ : 'The preview is not verified. Explain the missing or failing checks; do not say it is ready or working.',
1165
+ scenarioBlocked
1166
+ ? 'Fix the business-scenario manifest or browser suite, then rerun this helper.'
1167
+ : 'After the next UI-facing change, rerun this helper and browser scenarios before reporting completion.',
1168
+ ],
1169
+ };
1170
+ }
1171
+
1172
+ function printReport(report, json) {
1173
+ if (json) {
1174
+ console.log(JSON.stringify(report, null, 2));
1175
+ return;
1176
+ }
1177
+
1178
+ console.log(`Gofer UI preview: ${report.status}`);
1179
+ if (report.command) console.log(`Command: ${report.command}`);
1180
+ if (report.selectedUrl) console.log(`URL: ${report.selectedUrl}`);
1181
+ if (report.screenshot?.path) console.log(`Screenshot: ${report.screenshot.path}`);
1182
+ if (report.reviewLogPath) console.log(`Review log: ${report.reviewLogPath}`);
1183
+ if (report.businessScenarios?.reportPath) {
1184
+ console.log(`Business scenarios: ${report.businessScenarios.run?.status ?? 'not-run'}`);
1185
+ console.log(`Scenario report: ${report.businessScenarios.reportPath}`);
1186
+ }
1187
+ if (report.server?.logPath) console.log(`Server log: ${report.server.logPath}`);
1188
+ for (const action of report.nextActions ?? []) {
1189
+ console.log(`Next: ${action}`);
1190
+ }
1191
+ }
1192
+
1193
+ async function main() {
1194
+ try {
1195
+ const options = parseArgs(process.argv.slice(2));
1196
+ if (options.help) {
1197
+ console.log(helpText());
1198
+ return;
1199
+ }
1200
+
1201
+ const report = await runUiPreview(options);
1202
+ printReport(report, options.json);
1203
+ process.exitCode = ['blocked', 'unverified'].includes(report.status) ? 2 : 0;
1204
+ } catch (error) {
1205
+ console.error(`Gofer UI preview failed: ${error.message}`);
1206
+ process.exitCode = 1;
1207
+ }
1208
+ }
1209
+
1210
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
1211
+ await main();
1212
+ }