@sun-asterisk/sungen 3.2.2 → 3.2.3-beta.3

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 (355) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +26 -3
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add-flow.d.ts.map +1 -1
  5. package/dist/cli/commands/add-flow.js +2 -0
  6. package/dist/cli/commands/add-flow.js.map +1 -1
  7. package/dist/cli/commands/add.d.ts.map +1 -1
  8. package/dist/cli/commands/add.js +4 -0
  9. package/dist/cli/commands/add.js.map +1 -1
  10. package/dist/cli/commands/capability.d.ts.map +1 -1
  11. package/dist/cli/commands/capability.js +61 -0
  12. package/dist/cli/commands/capability.js.map +1 -1
  13. package/dist/cli/commands/delivery.d.ts.map +1 -1
  14. package/dist/cli/commands/delivery.js +89 -2
  15. package/dist/cli/commands/delivery.js.map +1 -1
  16. package/dist/cli/commands/eval.js +3 -3
  17. package/dist/cli/commands/eval.js.map +1 -1
  18. package/dist/cli/commands/gate.d.ts +3 -0
  19. package/dist/cli/commands/gate.d.ts.map +1 -0
  20. package/dist/cli/commands/gate.js +83 -0
  21. package/dist/cli/commands/gate.js.map +1 -0
  22. package/dist/cli/commands/generate.d.ts.map +1 -1
  23. package/dist/cli/commands/generate.js +2 -1
  24. package/dist/cli/commands/generate.js.map +1 -1
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +32 -2
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/journey.d.ts +3 -0
  29. package/dist/cli/commands/journey.d.ts.map +1 -0
  30. package/dist/cli/commands/journey.js +114 -0
  31. package/dist/cli/commands/journey.js.map +1 -0
  32. package/dist/cli/commands/update.d.ts.map +1 -1
  33. package/dist/cli/commands/update.js +10 -3
  34. package/dist/cli/commands/update.js.map +1 -1
  35. package/dist/cli/index.js +4 -0
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  38. package/dist/dashboard/snapshot-builder.js +10 -1
  39. package/dist/dashboard/snapshot-builder.js.map +1 -1
  40. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  41. package/dist/exporters/csv-exporter.js +3 -1
  42. package/dist/exporters/csv-exporter.js.map +1 -1
  43. package/dist/exporters/json-exporter.js +3 -1
  44. package/dist/exporters/json-exporter.js.map +1 -1
  45. package/dist/exporters/mobile-app-id.d.ts +22 -0
  46. package/dist/exporters/mobile-app-id.d.ts.map +1 -0
  47. package/dist/exporters/mobile-app-id.js +37 -0
  48. package/dist/exporters/mobile-app-id.js.map +1 -0
  49. package/dist/exporters/spec-parser.d.ts.map +1 -1
  50. package/dist/exporters/spec-parser.js +6 -2
  51. package/dist/exporters/spec-parser.js.map +1 -1
  52. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
  53. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  54. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  55. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  56. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  57. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  58. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  59. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  60. package/dist/generators/test-generator/code-generator.js +48 -9
  61. package/dist/generators/test-generator/code-generator.js.map +1 -1
  62. package/dist/generators/test-generator/template-engine.d.ts +3 -0
  63. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  64. package/dist/generators/test-generator/template-engine.js +11 -1
  65. package/dist/generators/test-generator/template-engine.js.map +1 -1
  66. package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
  67. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  68. package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
  69. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  70. package/dist/harness/audit.d.ts.map +1 -1
  71. package/dist/harness/audit.js +8 -1
  72. package/dist/harness/audit.js.map +1 -1
  73. package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
  74. package/dist/harness/eval/skill-lint.d.ts +3 -3
  75. package/dist/harness/eval/skill-lint.d.ts.map +1 -1
  76. package/dist/harness/eval/skill-lint.js +88 -68
  77. package/dist/harness/eval/skill-lint.js.map +1 -1
  78. package/dist/harness/journey.d.ts +88 -0
  79. package/dist/harness/journey.d.ts.map +1 -0
  80. package/dist/harness/journey.js +414 -0
  81. package/dist/harness/journey.js.map +1 -0
  82. package/dist/harness/quality-gates.d.ts +13 -0
  83. package/dist/harness/quality-gates.d.ts.map +1 -1
  84. package/dist/harness/quality-gates.js +23 -0
  85. package/dist/harness/quality-gates.js.map +1 -1
  86. package/dist/harness/sensors.d.ts +1 -1
  87. package/dist/harness/sensors.d.ts.map +1 -1
  88. package/dist/harness/sensors.js +32 -9
  89. package/dist/harness/sensors.js.map +1 -1
  90. package/dist/orchestrator/ai-rules-updater.d.ts +11 -3
  91. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  92. package/dist/orchestrator/ai-rules-updater.js +44 -180
  93. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  94. package/dist/orchestrator/ai-skills/adapters.d.ts +28 -0
  95. package/dist/orchestrator/ai-skills/adapters.d.ts.map +1 -0
  96. package/dist/orchestrator/ai-skills/adapters.js +34 -0
  97. package/dist/orchestrator/ai-skills/adapters.js.map +1 -0
  98. package/dist/orchestrator/ai-skills/build.d.ts +10 -0
  99. package/dist/orchestrator/ai-skills/build.d.ts.map +1 -0
  100. package/dist/orchestrator/ai-skills/build.js +128 -0
  101. package/dist/orchestrator/ai-skills/build.js.map +1 -0
  102. package/dist/orchestrator/ai-skills/cap-stripper.d.ts +27 -0
  103. package/dist/orchestrator/ai-skills/cap-stripper.d.ts.map +1 -0
  104. package/dist/orchestrator/ai-skills/cap-stripper.js +78 -0
  105. package/dist/orchestrator/ai-skills/cap-stripper.js.map +1 -0
  106. package/dist/orchestrator/ai-skills/commands.d.ts +22 -0
  107. package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -0
  108. package/dist/orchestrator/ai-skills/commands.js +109 -0
  109. package/dist/orchestrator/ai-skills/commands.js.map +1 -0
  110. package/dist/orchestrator/ai-skills/generator.d.ts +23 -0
  111. package/dist/orchestrator/ai-skills/generator.d.ts.map +1 -0
  112. package/dist/orchestrator/ai-skills/generator.js +79 -0
  113. package/dist/orchestrator/ai-skills/generator.js.map +1 -0
  114. package/dist/orchestrator/ai-skills/golden-skills.test.d.ts +2 -0
  115. package/dist/orchestrator/ai-skills/golden-skills.test.d.ts.map +1 -0
  116. package/dist/orchestrator/ai-skills/golden-skills.test.js +183 -0
  117. package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -0
  118. package/dist/orchestrator/ai-skills/resolve-assistants.d.ts +8 -0
  119. package/dist/orchestrator/ai-skills/resolve-assistants.d.ts.map +1 -0
  120. package/dist/orchestrator/ai-skills/resolve-assistants.js +28 -0
  121. package/dist/orchestrator/ai-skills/resolve-assistants.js.map +1 -0
  122. package/dist/orchestrator/ai-skills/writer.d.ts +15 -0
  123. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -0
  124. package/dist/orchestrator/ai-skills/writer.js +129 -0
  125. package/dist/orchestrator/ai-skills/writer.js.map +1 -0
  126. package/dist/orchestrator/flow-manager.d.ts +11 -0
  127. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  128. package/dist/orchestrator/flow-manager.js +147 -29
  129. package/dist/orchestrator/flow-manager.js.map +1 -1
  130. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
  131. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
  132. package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
  133. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
  134. package/dist/orchestrator/project-initializer.d.ts +44 -7
  135. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  136. package/dist/orchestrator/project-initializer.js +147 -79
  137. package/dist/orchestrator/project-initializer.js.map +1 -1
  138. package/dist/orchestrator/screen-manager.d.ts +30 -0
  139. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  140. package/dist/orchestrator/screen-manager.js +205 -22
  141. package/dist/orchestrator/screen-manager.js.map +1 -1
  142. package/dist/orchestrator/templates/ai-src/README.md +58 -0
  143. package/dist/orchestrator/templates/{ai-instructions/claude-cmd-add-flow.md → ai-src/commands/add-flow.md} +63 -2
  144. package/dist/orchestrator/templates/{ai-instructions/claude-cmd-add-screen.md → ai-src/commands/add-screen.md} +171 -6
  145. package/dist/orchestrator/templates/{ai-instructions/claude-cmd-create-test.md → ai-src/commands/create-test.md} +118 -4
  146. package/{src/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md → dist/orchestrator/templates/ai-src/commands/dashboard.md} +25 -3
  147. package/{src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md → dist/orchestrator/templates/ai-src/commands/delivery.md} +67 -3
  148. package/dist/orchestrator/templates/ai-src/commands/design.md +29 -0
  149. package/{src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md → dist/orchestrator/templates/ai-src/commands/feedback.md} +34 -3
  150. package/dist/orchestrator/templates/{ai-instructions/claude-cmd-locale.md → ai-src/commands/locale.md} +56 -4
  151. package/{src/orchestrator/templates/ai-instructions/claude-cmd-review.md → dist/orchestrator/templates/ai-src/commands/review.md} +57 -2
  152. package/{src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md → dist/orchestrator/templates/ai-src/commands/run-test.md} +116 -5
  153. package/dist/orchestrator/templates/ai-src/config/codex.md +8 -0
  154. package/{src/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md → dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md} +11 -0
  155. package/dist/orchestrator/templates/{ai-instructions/github-skill-sungen-error-mapping.md → ai-src/skills/sungen-error-mapping/SKILL.md} +49 -9
  156. package/{src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md → dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md} +5 -5
  157. package/{src/orchestrator/templates/ai-instructions/claude-skill-locale.md → dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md} +275 -3
  158. package/{src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md → dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md} +17 -0
  159. package/dist/orchestrator/templates/{ai-instructions/claude-skill-selector-fix-mobile.md → ai-src/skills/sungen-selector-fix-mobile/SKILL.md} +5 -0
  160. package/dist/orchestrator/templates/{ai-instructions/claude-skill-tc-generation.md → ai-src/skills/sungen-tc-generation/SKILL.md} +5 -0
  161. package/dist/orchestrator/templates/playwright.config.ts +7 -4
  162. package/package.json +3 -3
  163. package/src/capabilities/discover.ts +29 -3
  164. package/src/cli/commands/add-flow.ts +2 -0
  165. package/src/cli/commands/add.ts +4 -0
  166. package/src/cli/commands/capability.ts +64 -1
  167. package/src/cli/commands/delivery.ts +89 -2
  168. package/src/cli/commands/eval.ts +3 -3
  169. package/src/cli/commands/gate.ts +44 -0
  170. package/src/cli/commands/generate.ts +2 -1
  171. package/src/cli/commands/init.ts +50 -3
  172. package/src/cli/commands/journey.ts +72 -0
  173. package/src/cli/commands/update.ts +14 -4
  174. package/src/cli/index.ts +4 -0
  175. package/src/dashboard/snapshot-builder.ts +10 -1
  176. package/src/exporters/csv-exporter.ts +3 -1
  177. package/src/exporters/json-exporter.ts +3 -1
  178. package/src/exporters/mobile-app-id.ts +34 -0
  179. package/src/exporters/spec-parser.ts +6 -2
  180. package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
  181. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  182. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  183. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  184. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  185. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  186. package/src/generators/test-generator/code-generator.ts +49 -9
  187. package/src/generators/test-generator/template-engine.ts +14 -1
  188. package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
  189. package/src/harness/audit.ts +9 -2
  190. package/src/harness/catalog/universal-viewpoints.yaml +15 -0
  191. package/src/harness/eval/skill-lint.ts +71 -60
  192. package/src/harness/journey.ts +434 -0
  193. package/src/harness/quality-gates.ts +20 -0
  194. package/src/harness/sensors.ts +25 -8
  195. package/src/orchestrator/ai-rules-updater.ts +35 -197
  196. package/src/orchestrator/ai-skills/adapters.ts +52 -0
  197. package/src/orchestrator/ai-skills/build.ts +103 -0
  198. package/src/orchestrator/ai-skills/cap-stripper.ts +85 -0
  199. package/src/orchestrator/ai-skills/commands.ts +112 -0
  200. package/src/orchestrator/ai-skills/generator.ts +47 -0
  201. package/src/orchestrator/ai-skills/golden-skills.test.ts +124 -0
  202. package/src/orchestrator/ai-skills/resolve-assistants.ts +18 -0
  203. package/src/orchestrator/ai-skills/writer.ts +92 -0
  204. package/src/orchestrator/flow-manager.ts +165 -29
  205. package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
  206. package/src/orchestrator/project-initializer.ts +159 -86
  207. package/src/orchestrator/screen-manager.ts +237 -22
  208. package/src/orchestrator/templates/ai-src/README.md +58 -0
  209. package/src/orchestrator/templates/{ai-instructions/claude-cmd-add-flow.md → ai-src/commands/add-flow.md} +63 -2
  210. package/src/orchestrator/templates/{ai-instructions/claude-cmd-add-screen.md → ai-src/commands/add-screen.md} +171 -6
  211. package/src/orchestrator/templates/{ai-instructions/claude-cmd-create-test.md → ai-src/commands/create-test.md} +118 -4
  212. package/{dist/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md → src/orchestrator/templates/ai-src/commands/dashboard.md} +25 -3
  213. package/{dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md → src/orchestrator/templates/ai-src/commands/delivery.md} +67 -3
  214. package/src/orchestrator/templates/ai-src/commands/design.md +29 -0
  215. package/{dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md → src/orchestrator/templates/ai-src/commands/feedback.md} +34 -3
  216. package/src/orchestrator/templates/{ai-instructions/claude-cmd-locale.md → ai-src/commands/locale.md} +56 -4
  217. package/{dist/orchestrator/templates/ai-instructions/claude-cmd-review.md → src/orchestrator/templates/ai-src/commands/review.md} +57 -2
  218. package/{dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md → src/orchestrator/templates/ai-src/commands/run-test.md} +116 -5
  219. package/src/orchestrator/templates/ai-src/config/codex.md +8 -0
  220. package/{dist/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md → src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md} +11 -0
  221. package/{dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md → src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md} +49 -9
  222. package/{dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md → src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md} +5 -5
  223. package/{dist/orchestrator/templates/ai-instructions/claude-skill-locale.md → src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md} +275 -3
  224. package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md → src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md} +17 -0
  225. package/src/orchestrator/templates/{ai-instructions/claude-skill-selector-fix-mobile.md → ai-src/skills/sungen-selector-fix-mobile/SKILL.md} +5 -0
  226. package/src/orchestrator/templates/{ai-instructions/claude-skill-tc-generation.md → ai-src/skills/sungen-tc-generation/SKILL.md} +5 -0
  227. package/src/orchestrator/templates/playwright.config.ts +7 -4
  228. package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +0 -12
  229. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +0 -347
  230. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +0 -86
  231. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +0 -131
  232. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +0 -93
  233. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +0 -65
  234. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +0 -83
  235. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +0 -13
  236. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +0 -24
  237. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +0 -70
  238. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +0 -38
  239. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +0 -155
  240. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +0 -184
  241. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +0 -85
  242. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +0 -291
  243. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +0 -316
  244. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +0 -221
  245. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +0 -617
  246. package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +0 -12
  247. package/src/orchestrator/templates/ai-instructions/claude-skill-api-design.md +0 -62
  248. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +0 -82
  249. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-pat.md +0 -117
  250. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +0 -60
  251. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +0 -38
  252. package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +0 -35
  253. package/src/orchestrator/templates/ai-instructions/claude-skill-dashboard.md +0 -121
  254. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +0 -141
  255. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +0 -157
  256. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +0 -446
  257. package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +0 -79
  258. package/src/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +0 -109
  259. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +0 -347
  260. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +0 -320
  261. package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +0 -118
  262. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-a-data-entry.md +0 -203
  263. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-b-data-ops.md +0 -179
  264. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-c-data-explore.md +0 -233
  265. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-d-display.md +0 -226
  266. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-e-identity.md +0 -177
  267. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +0 -118
  268. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +0 -86
  269. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +0 -131
  270. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +0 -93
  271. package/src/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +0 -65
  272. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +0 -83
  273. package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +0 -13
  274. package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +0 -24
  275. package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +0 -70
  276. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +0 -38
  277. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +0 -155
  278. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +0 -62
  279. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +0 -184
  280. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +0 -82
  281. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md +0 -117
  282. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +0 -60
  283. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +0 -38
  284. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +0 -35
  285. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-dashboard.md +0 -121
  286. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +0 -141
  287. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +0 -157
  288. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +0 -446
  289. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +0 -85
  290. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +0 -79
  291. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +0 -291
  292. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +0 -109
  293. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +0 -316
  294. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +0 -221
  295. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +0 -617
  296. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +0 -320
  297. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +0 -118
  298. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-a-data-entry.md +0 -203
  299. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-b-data-ops.md +0 -179
  300. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-c-data-explore.md +0 -233
  301. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-d-display.md +0 -226
  302. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-e-identity.md +0 -177
  303. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +0 -118
  304. /package/dist/orchestrator/templates/{ai-instructions/claude-agent-challenge.md → ai-src/agents/challenge.md} +0 -0
  305. /package/dist/orchestrator/templates/{ai-instructions/claude-agent-discovery.md → ai-src/agents/discovery.md} +0 -0
  306. /package/dist/orchestrator/templates/{ai-instructions/claude-agent-generator.md → ai-src/agents/generator.md} +0 -0
  307. /package/dist/orchestrator/templates/{ai-instructions/claude-agent-reviewer.md → ai-src/agents/reviewer.md} +0 -0
  308. /package/dist/orchestrator/templates/{ai-instructions/claude-config.md → ai-src/config/claude.md} +0 -0
  309. /package/dist/orchestrator/templates/{ai-instructions/copilot-config.md → ai-src/config/copilot.md} +0 -0
  310. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-api-design.md → ai-src/skills/sungen-api-design/SKILL.md} +0 -0
  311. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture.md → ai-src/skills/sungen-capture/SKILL.md} +0 -0
  312. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-figma-mcp.md → ai-src/skills/sungen-capture/mode-figma-mcp.md} +0 -0
  313. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-figma-pat.md → ai-src/skills/sungen-capture/mode-figma-pat.md} +0 -0
  314. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-live.md → ai-src/skills/sungen-capture/mode-live.md} +0 -0
  315. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-local.md → ai-src/skills/sungen-capture/mode-local.md} +0 -0
  316. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-dashboard.md → ai-src/skills/sungen-dashboard/SKILL.md} +0 -0
  317. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-delivery.md → ai-src/skills/sungen-delivery/SKILL.md} +0 -0
  318. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-gherkin-syntax.md → ai-src/skills/sungen-gherkin-syntax/SKILL.md} +0 -0
  319. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-ingest-legacy.md → ai-src/skills/sungen-ingest-legacy/SKILL.md} +0 -0
  320. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-mobile-gestures.md → ai-src/skills/sungen-mobile-gestures/SKILL.md} +0 -0
  321. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-selector-keys.md → ai-src/skills/sungen-selector-keys/SKILL.md} +0 -0
  322. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-tc-review.md → ai-src/skills/sungen-tc-review/SKILL.md} +0 -0
  323. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-test-design-techniques.md → ai-src/skills/sungen-test-design-techniques/SKILL.md} +0 -0
  324. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint.md → ai-src/skills/sungen-viewpoint/SKILL.md} +0 -0
  325. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-a-data-entry.md → ai-src/skills/sungen-viewpoint/group-a-data-entry.md} +0 -0
  326. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-b-data-ops.md → ai-src/skills/sungen-viewpoint/group-b-data-ops.md} +0 -0
  327. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-c-data-explore.md → ai-src/skills/sungen-viewpoint/group-c-data-explore.md} +0 -0
  328. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-d-display.md → ai-src/skills/sungen-viewpoint/group-d-display.md} +0 -0
  329. /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-e-identity.md → ai-src/skills/sungen-viewpoint/group-e-identity.md} +0 -0
  330. /package/src/orchestrator/templates/{ai-instructions/claude-agent-challenge.md → ai-src/agents/challenge.md} +0 -0
  331. /package/src/orchestrator/templates/{ai-instructions/claude-agent-discovery.md → ai-src/agents/discovery.md} +0 -0
  332. /package/src/orchestrator/templates/{ai-instructions/claude-agent-generator.md → ai-src/agents/generator.md} +0 -0
  333. /package/src/orchestrator/templates/{ai-instructions/claude-agent-reviewer.md → ai-src/agents/reviewer.md} +0 -0
  334. /package/src/orchestrator/templates/{ai-instructions/claude-config.md → ai-src/config/claude.md} +0 -0
  335. /package/src/orchestrator/templates/{ai-instructions/copilot-config.md → ai-src/config/copilot.md} +0 -0
  336. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md → src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md} +0 -0
  337. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md → src/orchestrator/templates/ai-src/skills/sungen-capture/SKILL.md} +0 -0
  338. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-figma-mcp.md} +0 -0
  339. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-figma-pat.md} +0 -0
  340. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-live.md} +0 -0
  341. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md} +0 -0
  342. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-dashboard.md → src/orchestrator/templates/ai-src/skills/sungen-dashboard/SKILL.md} +0 -0
  343. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md → src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md} +0 -0
  344. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md → src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md} +0 -0
  345. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md → src/orchestrator/templates/ai-src/skills/sungen-ingest-legacy/SKILL.md} +0 -0
  346. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md → src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md} +0 -0
  347. /package/src/orchestrator/templates/{ai-instructions/claude-skill-selector-keys.md → ai-src/skills/sungen-selector-keys/SKILL.md} +0 -0
  348. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md → src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md} +0 -0
  349. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md → src/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md} +0 -0
  350. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md} +0 -0
  351. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-a-data-entry.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-a-data-entry.md} +0 -0
  352. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-b-data-ops.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-b-data-ops.md} +0 -0
  353. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-c-data-explore.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-c-data-explore.md} +0 -0
  354. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-d-display.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-d-display.md} +0 -0
  355. /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-e-identity.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md} +0 -0
@@ -165,7 +165,9 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
165
165
  function formatTestEnvironment(baseURL: string, featurePath: string | undefined, projectName: string): string {
166
166
  const base = (baseURL || '').replace(/\/+$/, '');
167
167
  const p = (featurePath || '').replace(/^\/+/, '');
168
- const url = p ? `${base}/${p}` : base;
168
+ // Join with '/' only when there is a baseURL (web). For mobile baseURL is empty
169
+ // and `p` is an app id (e.g. com.kngroup.media.converter) — emit it bare, no '/'.
170
+ const url = base && p ? `${base}/${p}` : (p || base);
169
171
  return `${url} (${projectName})`;
170
172
  }
171
173
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve the OS-specific app id from a feature `Path:` for the delivery /
3
+ * dashboard "Test Environment" column.
4
+ *
5
+ * Mobile features carry a DUAL-ID Path: `<androidPkg>[/<activity>] | <iosBundleId>`
6
+ * (e.g. `com.kngroup.media.converter | com.kngroup.media.converter.dev`). The
7
+ * deliverable's environment column must show ONLY the id for the OS the results
8
+ * came from — Android → the package (launch activity stripped), iOS → the bundle
9
+ * id — never the combined dual-id.
10
+ *
11
+ * Web features carry a URL Path (no `|`) and are returned unchanged.
12
+ *
13
+ * @param featurePath the feature `Path:` value (dual-id for mobile, URL for web)
14
+ * @param platform the OS/result axis the row belongs to. The OS is the FIRST
15
+ * dot-segment, so env-suffixed result variants resolve too —
16
+ * `'ios'`, `'ios.staging'`, `'ios.ja'` all select the bundle id;
17
+ * anything else (`'android'`, `'android.staging'`, a bare locale,
18
+ * base) falls back to the Android package.
19
+ * @returns the resolved single id, or `featurePath` unchanged when it is not a dual-id.
20
+ */
21
+ export function resolvePlatformAppId(
22
+ featurePath: string | undefined,
23
+ platform: string | undefined,
24
+ ): string | undefined {
25
+ if (!featurePath || !featurePath.includes('|')) return featurePath;
26
+ const [androidPart = '', iosPart = ''] = featurePath.split('|').map((s) => s.trim());
27
+ const androidId = androidPart.split('/')[0].trim(); // package only (drop /MainActivity)
28
+ const iosId = iosPart.trim();
29
+ // OS = first dot-segment so `ios.staging` / `ios.ja` (env-suffixed result files
30
+ // <name>-test-result.<os>[.<env>].json) still resolve to the iOS bundle id.
31
+ const os = (platform || '').toLowerCase().split('.')[0];
32
+ if (os === 'ios') return iosId || androidId;
33
+ return androidId || iosId;
34
+ }
@@ -25,7 +25,9 @@ function extractTestBlock(content: string, startIdx: number): {
25
25
  // The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
26
26
  // `}` — e.g. `@query:q(p={{var}})` — doesn't truncate the match (issue #271). The header is
27
27
  // single-line, so `.*?` anchored on `}, async` is safe.
28
- const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
28
+ // Accept both Playwright `test(...)` and mobile/Appium mocha `it(...)` (issue #391). The leading
29
+ // `\b` prevents `it(` from matching the tail of a larger word like `submit(` / `edit(`.
30
+ const testRegex = /\b(?:test|it)\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
29
31
  testRegex.lastIndex = startIdx;
30
32
  const match = testRegex.exec(content);
31
33
  if (!match) return null;
@@ -57,7 +59,9 @@ function extractTestBlock(content: string, startIdx: number): {
57
59
  */
58
60
  function extractDescribeBlocks(content: string): Array<{ name: string; bodyStart: number; bodyEnd: number }> {
59
61
  const result: Array<{ name: string; bodyStart: number; bodyEnd: number }> = [];
60
- const regex = /test\.describe(?:\.serial)?\s*\(\s*['"]([^'"]+)['"]\s*,\s*\(\s*\)\s*=>\s*\{/g;
62
+ // Accept Playwright `test.describe(...)` / `test.describe.serial(...)` AND mobile/Appium mocha
63
+ // `describe(...)` (issue #391). The optional `test.` prefix keeps web specs matching unchanged.
64
+ const regex = /(?:test\.)?describe(?:\.serial)?\s*\(\s*['"]([^'"]+)['"]\s*,\s*\(\s*\)\s*=>\s*\{/g;
61
65
  let match: RegExpExecArray | null;
62
66
  while ((match = regex.exec(content)) !== null) {
63
67
  const name = match[1];
@@ -23,11 +23,14 @@ export interface TestFileData {
23
23
  featureFileName?: string; // Feature file name for TestDataLoader.load()
24
24
  isParallel?: boolean; // @parallel tag: fresh page per test (opt-out from serial default)
25
25
  flowMode?: boolean; // flow: cross-screen → longer per-test timeout (bug #243)
26
+ platform?: string; // mobile @platform:* value (mobile|android|ios) → `// sungen:platform=` marker for wdio per-OS routing
26
27
  cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
27
28
  backgroundSteps?: Array<{ comment?: string; code: string }>; // Raw background steps for serial beforeAll
28
29
  scenarios: string[];
29
30
  authGroups?: AuthGroup[]; // Grouped by auth role for nested describes
30
31
  singleAuthRole?: string; // Auth role when all scenarios share the same role
32
+ appPackage?: string; // Mobile (appium): Android package from the feature `Path:` → drives the per-scenario relaunch reset (beforeEach terminate+activate)
33
+ iosBundleId?: string; // Mobile (appium): iOS bundle id from a dual-id `Path: <pkg>/<activity> | <bundleId>`
31
34
  }
32
35
 
33
36
  export interface ScenarioData {
@@ -1,10 +1,13 @@
1
1
  if (driver.isIOS) {
2
- // iOS (XCUITest) has no UiScrollable. Swipe the screen up in bounded steps until the target is on
3
- // screen (a below-fold element isn't in the XCUITest tree until scrolled, so getElementRect/tap on
4
- // it would otherwise fail). Bounded so a missing target can't loop forever.
2
+ // iOS (XCUITest) has no UiScrollable. Swipe up in bounded steps until the target is on screen (a
3
+ // below-fold element isn't in the XCUITest tree until scrolled). A settle `pause` between swipes
4
+ // paces the loop so it doesn't flood `execute/sync` (rapid back-to-back swipes are a common source
5
+ // of flakiness; issue #394). Bounded (cap 10) so a missing target can't loop forever. NOTE: this
6
+ // does NOT fix an app that crashes mid-scroll on its own — that's an app-stability issue, not codegen.
5
7
  const __t = $({{> appium-selector-expr}});
6
- for (let __i = 0; __i < 12 && !(await __t.isDisplayed().catch(() => false)); __i++) {
8
+ for (let __i = 0; __i < 10 && !(await __t.isDisplayed().catch(() => false)); __i++) {
7
9
  await driver.execute('mobile: swipe', { direction: 'up' });
10
+ await driver.pause(400); // settle: let the app render between swipes (don't flood execute/sync)
8
11
  }
9
12
  } else {
10
13
  {{#if (eq strategy 'accessibility-id')}}
@@ -1,3 +1,20 @@
1
- {{!-- A switch/toggle flips state on tap (mirrors the web toggle pattern: unconditional click).
2
- Use `check`/`uncheck` instead when you need to drive it to a specific on/off state idempotently. --}}
3
- await {{> locator}}.click();
1
+ {{!-- A switch/toggle flips state on tap. On Flutter (and many native layouts) the row's a11y node
2
+ spans the FULL width and merges the label with the switch, so a centre .click() lands on the
3
+ LABEL and does NOT flip the control — only the trailing visual switch toggles. So tap the
4
+ RIGHT portion (vertical centre) via getElementRect (W3C-standard → Android + iOS). Falls back to
5
+ a plain centre click when the rect is unavailable. Use `check`/`uncheck` to drive a specific
6
+ on/off state idempotently instead. --}}
7
+ await (async () => {
8
+ const e = await {{> locator}};
9
+ const r = await driver.getElementRect(e.elementId);
10
+ if (r && r.width) {
11
+ // Trailing switch: ~right edge, vertical centre. The inset clears the label/track padding while
12
+ // staying on the switch even on a full-width composite row.
13
+ const x = Math.round(r.x + r.width - Math.max(48, r.width * 0.08));
14
+ const y = Math.round(r.y + r.height / 2);
15
+ if (driver.isIOS) { await driver.execute('mobile: tap', { x, y }); }
16
+ else { await driver.execute('mobile: clickGesture', { x, y }); }
17
+ } else {
18
+ await e.click();
19
+ }
20
+ })();
@@ -1 +1,4 @@
1
- await expect({{> locator}}).toBeSelected();
1
+ // Mobile toggle state lives in the `checked` attribute (Android) or `value` "1"/"0" (iOS) — NOT
2
+ // `selected` (what toBeSelected() reads; always false for a Flutter Switch). The composite row node
3
+ // carries it, so the row selector resolves the state directly.
4
+ await expect({{> locator}}).toHaveAttribute(driver.isIOS ? 'value' : 'checked', driver.isIOS ? '1' : 'true');
@@ -1 +1,4 @@
1
- await expect({{> locator}}).not.toBeSelected();
1
+ // Mobile toggle state lives in the `checked` attribute (Android) or `value` "1"/"0" (iOS) — NOT
2
+ // `selected` (what toBeSelected() reads; always false for a Flutter Switch). The composite row node
3
+ // carries it, so the row selector resolves the state directly.
4
+ await expect({{> locator}}).not.toHaveAttribute(driver.isIOS ? 'value' : 'checked', driver.isIOS ? '1' : 'true');
@@ -152,6 +152,9 @@ describe('{{featureName}}', () => {
152
152
  const __APP_PKG__ = __resolveAppId();
153
153
  try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
154
154
  await driver.activateApp(__APP_PKG__);
155
+ // Restore portrait so a prior scenario's `rotate to landscape` can't leak into this one — the
156
+ // app relaunch above resets in-app state, but device orientation is OS-level and persists.
157
+ try { await driver.setOrientation('PORTRAIT'); } catch { /* orientation control not supported */ }
155
158
  // Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
156
159
  // (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
157
160
  // That skeleton is static, so a "two equal reads" check alone false-positives on it and we'd
@@ -74,7 +74,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
74
74
  * Any tag not recognized by sungen as functional → pass through.
75
75
  */
76
76
  const FUNCTIONAL_TAG_PREFIXES = [
77
- '@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
77
+ '@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
78
78
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
79
79
  '@flow', '@cases:', '@deferred:', '@owned-by:',
80
80
  ];
@@ -173,8 +173,10 @@ function getEffectiveAuthRole(
173
173
  * @manual at scenario level → skip that scenario
174
174
  */
175
175
  function isManual(tags: string[]): boolean {
176
- // @deferred:flow is owned by a flow not automated on this screen, so it skips like @manual (H6).
177
- return tags.some(tag => tag === '@manual' || tag === '@deferred:flow');
176
+ // Recognize bare `@manual` AND the reason-coded `@manual:Mx` convention (what create-test emits)
177
+ // must match harness/parse.ts + capability-plan.ts, else an `@manual:Mx` scenario is wrongly
178
+ // GENERATED (and run) instead of skipped. @deferred:flow is owned by a flow → also skips here (H6).
179
+ return tags.some(tag => /^@manual\b/i.test(tag) || tag === '@deferred:flow');
178
180
  }
179
181
 
180
182
  /**
@@ -271,10 +273,13 @@ export class CodeGenerator {
271
273
  const depth = outputSubdir ? outputSubdir.split('/').length : 0;
272
274
  const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
273
275
 
274
- // Serial + @cleanup tags need cleanupPage import from base
275
- const isParallelFeature = (feature.tags || []).includes('@parallel');
276
+ // Isolation model: per-scenario fresh page is the DEFAULT (no cascade — one failure can't skip
277
+ // the rest). `@serial` opts INTO the shared-page serial suite (a genuinely stateful, ordered
278
+ // sequence). `@parallel` is a back-compat alias of the default (isolated). See #414.
279
+ const isSerialFeature = (feature.tags || []).includes('@serial');
280
+ const isParallelFeature = !isSerialFeature; // isolated unless explicitly @serial
276
281
  const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
277
- const needsCleanupImport = !isParallelFeature && hasCleanupTags;
282
+ const needsCleanupImport = isSerialFeature && hasCleanupTags; // afterEach cleanup only on the shared-page path
278
283
 
279
284
  // Active capabilities for this feature (registry-driven): the default UI + any whose annotation
280
285
  // tags appear (@query) or whose detectsStep matches (declarative DB steps). Each active
@@ -480,14 +485,23 @@ export class CodeGenerator {
480
485
  }
481
486
  }
482
487
 
483
- // Detect @parallel opt-out (default is serial)
484
- const isParallel = (feature.tags || []).includes('@parallel');
488
+ // Per-scenario ISOLATION is the default (test.describe + a fresh page each scenario → a failure
489
+ // never skip-cascades). `@serial` opts into the shared-page serial suite for a stateful sequence.
490
+ const isParallel = !(feature.tags || []).includes('@serial');
485
491
 
486
492
  // Generate background if exists
487
493
  let background: string | undefined;
488
494
  let backgroundSteps: Array<{ comment?: string; code: string }> | undefined;
489
495
  if (feature.background) {
490
- if (isParallel) {
496
+ // Mobile (appium) ALWAYS inlines Background via `backgroundSteps`: the test-file template emits a
497
+ // relaunch-per-scenario beforeEach and renders Background INSIDE it (it has no separate `background`
498
+ // beforeEach slot), and that per-scenario relaunch already gives @parallel its reset semantics.
499
+ // Routing a @parallel *mobile* feature to generateBeforeEach() computes the nav steps into
500
+ // `background`, which the mobile template then ignores → the Background nav (e.g. `tap [Settings]
501
+ // tab`) is silently dropped and every scenario runs on the launcher. So branch on isParallel only
502
+ // for web; for appium always use generateBackgroundSteps().
503
+ const isMobile = this.adapter.frameworkName === 'appium';
504
+ if (isParallel && !isMobile) {
491
505
  background = await this.generateBeforeEach(feature.background);
492
506
  } else {
493
507
  backgroundSteps = await this.generateBackgroundSteps(feature.background);
@@ -597,6 +611,29 @@ export class CodeGenerator {
597
611
  const cleanup = extractCleanupFlags(feature.tags || []);
598
612
  const screenshotOnFailure = hasScreenshotOnFailure(feature.tags || []);
599
613
 
614
+ // Mobile: the @platform:* value (mobile|android|ios). Emitted as a `// sungen:platform=` marker
615
+ // so wdio.conf routes the spec per OS — android/ios = that OS only, mobile (default) = both.
616
+ // Only the appium adapter consumes it; web features have no @platform tag → undefined → no marker.
617
+ const platform = this.adapter.frameworkName === 'appium'
618
+ ? (feature.tags || []).find(t => t.startsWith('@platform:'))?.split(':')[1]
619
+ : undefined;
620
+
621
+ // Mobile: parse the feature `Path:` (the app entry) into the app id(s) the per-scenario relaunch
622
+ // reset needs. Dual-id form: `<androidPkg>[/<activity>] | <iosBundleId>` — appPackage is the Android
623
+ // package (activity stripped, it isn't needed for terminateApp/activateApp); iosBundleId is the part
624
+ // after `|` when present. Without these the test-file template's `{{#if appPackage}}` stays false and
625
+ // the beforeEach (terminate+activate) reset is never emitted → nav scenarios cascade-fail.
626
+ let appPackage: string | undefined;
627
+ let iosBundleId: string | undefined;
628
+ if (this.adapter.frameworkName === 'appium' && feature.path) {
629
+ const [androidPart, iosPart] = feature.path.split('|').map(s => s.trim());
630
+ // Guard against a web-style URL Path (`/login`): only treat as a package when it isn't a route.
631
+ if (androidPart && !androidPart.startsWith('/')) {
632
+ appPackage = androidPart.split('/')[0].trim() || undefined;
633
+ }
634
+ if (iosPart) iosBundleId = iosPart;
635
+ }
636
+
600
637
  // Use adapter to render the complete test file structure
601
638
  return this.adapter.renderTestFile({
602
639
  imports: '', // Not used in template as it's rendered separately
@@ -613,11 +650,14 @@ export class CodeGenerator {
613
650
  featureFileName: featureName,
614
651
  isParallel,
615
652
  flowMode: isFlowFeature,
653
+ platform,
616
654
  cleanup,
617
655
  backgroundSteps,
618
656
  scenarios: needsGrouping ? [] : scenarios,
619
657
  authGroups: needsGrouping ? authGroups : undefined,
620
658
  singleAuthRole,
659
+ appPackage,
660
+ iosBundleId,
621
661
  });
622
662
  }
623
663
 
@@ -21,9 +21,19 @@ export class TemplateEngine {
21
21
  return text.split('\n').map(line => indentation + line).join('\n');
22
22
  });
23
23
 
24
+ // Escape a raw string for embedding inside a single-quoted JS string literal. Every caller wraps
25
+ // the result in '…' (selector values, attribute values, scope), so a value containing a newline,
26
+ // backslash, or quote must be escaped or the generated spec is a syntax error (e.g. a Flutter
27
+ // composite content-desc like "Quick Convert\nSelect File\n…" used as an accessibility-id).
28
+ // Order matters: backslash first, then the rest.
24
29
  Handlebars.registerHelper('escapeQuotes', function(text: string) {
25
30
  if (text == null) return '';
26
- return String(text).replace(/'/g, "\\'");
31
+ return String(text)
32
+ .replace(/\\/g, '\\\\')
33
+ .replace(/'/g, "\\'")
34
+ .replace(/\n/g, '\\n')
35
+ .replace(/\r/g, '\\r')
36
+ .replace(/\t/g, '\\t');
27
37
  });
28
38
 
29
39
  Handlebars.registerHelper('escapeRegex', function(text: string) {
@@ -259,11 +269,14 @@ export class TemplateEngine {
259
269
  screenName?: string;
260
270
  featureFileName?: string;
261
271
  isParallel?: boolean;
272
+ platform?: string;
262
273
  cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
263
274
  backgroundSteps?: Array<{ comment?: string; code: string }>;
264
275
  scenarios: string[];
265
276
  authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
266
277
  singleAuthRole?: string;
278
+ appPackage?: string;
279
+ iosBundleId?: string;
267
280
  }): string {
268
281
  return this.render('test-file', data);
269
282
  }
@@ -20,6 +20,12 @@ interface SelectorEntry {
20
20
  scope?: string; // Parent landmark aria-label to scope within (e.g., 'desktop navigation')
21
21
  match?: 'exact' | 'partial'; // For getByText matching (default: partial)
22
22
 
23
+ // === Mobile per-platform variant (issue #392) ===
24
+ // `android:`/`ios:` sub-selectors for one logical element whose locator differs per OS (composite
25
+ // content-desc, partial/dynamic text, native predicate). Each is a full SelectorEntry.
26
+ android?: SelectorEntry;
27
+ ios?: SelectorEntry;
28
+
23
29
  // === v2: Complex Elements ===
24
30
  variant?: 'native' | 'custom' | 'dragdrop'; // Dropdown/upload subtype
25
31
  trigger?: string; // Visible button that triggers hidden file input
@@ -52,8 +58,17 @@ interface LegacySelectorFile {
52
58
  }
53
59
 
54
60
  export interface ResolvedSelector {
55
- strategy: 'testid' | 'id' | 'placeholder' | 'label' | 'text' | 'css' | 'role' | 'locator';
61
+ // Web (Playwright) strategies + mobile (Appium) strategies. Mobile types pass through from
62
+ // selectors.yaml so the appium adapter's `appium-selector` switch can emit the right WDIO selector
63
+ // (`~` / raw xpath / `android=` / `-ios predicate string:` / `id=`). See issue #392.
64
+ strategy: 'testid' | 'id' | 'placeholder' | 'label' | 'text' | 'css' | 'role' | 'locator'
65
+ | 'accessibility-id' | 'xpath' | 'android-uiautomator' | 'ios-predicate';
56
66
  value: string;
67
+ // Mobile per-platform variant (`android:`/`ios:` in selectors.yaml). When set, the appium codegen
68
+ // emits a runtime `driver.isIOS ? '<ios>' : '<android>'` so one @platform:mobile spec resolves the
69
+ // right native selector per OS. Each side carries its own strategy+value (issue #392).
70
+ androidSelector?: { strategy: ResolvedSelector['strategy']; value: string; name?: string };
71
+ iosSelector?: { strategy: ResolvedSelector['strategy']; value: string; name?: string };
57
72
  role?: string; // For role-based selectors (e.g., 'button', 'link')
58
73
  name?: string; // For accessible name in role selectors
59
74
  locator?: string; // CSS locator for fallback
@@ -472,6 +487,25 @@ export class SelectorResolver {
472
487
  return { ...resolved, ...v2Fields };
473
488
  };
474
489
 
490
+ // Mobile per-platform variant (issue #392): `{ android: {...}, ios: {...} }`. Resolve each side
491
+ // and emit androidSelector/iosSelector so the appium codegen renders `driver.isIOS ? ios : android`.
492
+ // The base strategy/value falls back to whichever side is present (a missing side falls back to it).
493
+ // `nth`/`scope` are shared (top-level) — they cannot diverge per-OS (the codegen ternary swaps only
494
+ // strategy+value); a case needing per-OS nth/scope is a sub-feature, not a variant.
495
+ if (entry.android || entry.ios) {
496
+ const av = entry.android ? this.resolveFromEntry(entry.android, originalLabel) : undefined;
497
+ const iv = entry.ios ? this.resolveFromEntry(entry.ios, originalLabel) : undefined;
498
+ const base = (av || iv) as ResolvedSelector;
499
+ return withExtras({
500
+ strategy: base.strategy,
501
+ value: base.value,
502
+ name: base.name,
503
+ nth,
504
+ ...(av ? { androidSelector: { strategy: av.strategy, value: av.value, name: av.name } } : {}),
505
+ ...(iv ? { iosSelector: { strategy: iv.strategy, value: iv.value, name: iv.name } } : {}),
506
+ });
507
+ }
508
+
475
509
  // If type is 'locator', use locator field (or value as fallback) as the CSS locator directly
476
510
  if (type === 'locator') {
477
511
  const locatorValue = locator && locator.trim() ? locator : value;
@@ -536,6 +570,22 @@ export class SelectorResolver {
536
570
  exact,
537
571
  });
538
572
 
573
+ // Mobile (Appium) native strategies — pass through unchanged; the appium adapter's
574
+ // `appium-selector` switch renders the right WDIO selector (`~` / raw xpath / `android=` /
575
+ // `-ios predicate string:` / `id=`). On web these types never appear. See issue #392.
576
+ case 'accessibility-id':
577
+ case 'xpath':
578
+ case 'android-uiautomator':
579
+ case 'ios-predicate':
580
+ case 'id':
581
+ return withExtras({
582
+ strategy: type,
583
+ value,
584
+ name,
585
+ nth,
586
+ exact,
587
+ });
588
+
539
589
  default:
540
590
  // Fallback to placeholder
541
591
  return withExtras({
@@ -22,7 +22,7 @@ import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
22
22
  import { getProvenance, Provenance } from './provenance';
23
23
  import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
24
24
  import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
25
- negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk } from './quality-gates';
25
+ negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
26
26
  import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
27
27
  import { capabilityRegistry } from '../capabilities/registry';
28
28
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
@@ -146,7 +146,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
146
146
  const capGate = scoringCap?.gateProvider as
147
147
  ((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
148
148
  const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
149
- const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog);
149
+ const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile');
150
150
  const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
151
151
 
152
152
  // TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
@@ -244,6 +244,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
244
244
  if (isoRisk) {
245
245
  findings.push('ISOLATION-RISK: this @parallel flow mutates the cart but has no per-scenario isolation → cart/count/quantity asserts can go flaky when scenarios share state. Add `@cleanup:cart` (or `@isolate`, or a `Given User has an empty cart` background) so each scenario starts clean.');
246
246
  }
247
+ // #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
248
+ // failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
249
+ // the default, so this only fires when @serial was added to a suite that didn't need it.
250
+ const cascadeN = serialCascadeRisk(featureText, scenarios.length);
251
+ if (cascadeN) {
252
+ findings.push(`SERIAL-CASCADE-RISK: this \`@serial\` suite has ${cascadeN} INDEPENDENT scenarios (each re-navigates via Background) that don't need a shared-page sequence. \`describe.serial\` SKIPS every test after the first failure → one early red MASKS the rest as never-run. **Drop \`@serial\`** — the default per-scenario isolation runs every scenario and isolates failures. Keep \`@serial\` only for a genuinely stateful ordered sequence (where scenario N depends on N-1's state).`);
253
+ }
247
254
  if (flowDepth.stateful && flowDepth.missing.length) {
248
255
  const how: Record<string, string> = {
249
256
  'count-proof': 'assert the cart ROW COUNT / item QUANTITY (e.g. `... table with {{two_rows}}`, `Quantity column with {{qty}}`), not just the row presence',
@@ -91,6 +91,21 @@ page_types:
91
91
  keywords: ["success message", "saved", "created", "confirmation"]
92
92
  template: "User see [Success] message with {{success_text}}"
93
93
 
94
+ # Mobile launcher / home screen (Appium). Display + navigation only — no web form/validation.
95
+ # A `mobile-*` candidate (considered ONLY for mobile targets; never for web). Keywords are
96
+ # launcher-DISTINCTIVE (a tab-bar landing screen) and deliberately exclude generic tokens like
97
+ # "tab"/"navigation"/"home" that a mobile commerce/list screen also carries — so such a screen
98
+ # still selects ecommerce-list, not mobile-home. Both themes are presence/navigation (no `depth:`)
99
+ # — seeing the content and leaving Home (a destination marker OR the Home-only anchor going
100
+ # `is hidden`) IS the assertion; no data-assertion is forced.
101
+ mobile-home:
102
+ detect_keywords: ["bottom navigation", launcher, greeting, "home screen", "launcher screen"]
103
+ must_cover:
104
+ - theme: content-visibility
105
+ keywords: [displayed, render, loads, exposes, shown, visible]
106
+ - theme: navigation-correctness
107
+ keywords: [tap, navigate, "leaves home", "is hidden", opens, navigation]
108
+
94
109
  auth:
95
110
  detect_keywords: [login, logout, password, signin, "sign in", credential]
96
111
  must_cover:
@@ -1,87 +1,98 @@
1
1
  /**
2
2
  * Static skill-lint (Eval Harness L1) — deterministic quality checks on Sungen's OWN
3
- * AI-instruction templates, so a broken / unregistered / oversized skill fails before it
4
- * ships. Learned (generically) from the "static validations" tier of an agent-kit evals
5
- * layer. No project data — this lints the sungen package's own templates.
3
+ * single-source AI templates (`templates/ai-src/`), so a broken skill fails before it ships.
6
4
  *
7
- * Design note: the checks are MAPPING-DRIVEN. `AI_RULES_FILE_MAPPING` is the source of
8
- * truth for what each template installs as, so the lint uses the install target (does it
9
- * end in `/SKILL.md`?) to tell a top-level skill from a sub-content fragment — instead of
10
- * guessing from filenames. We deliberately do NOT enforce claude↔github body parity: the
11
- * two variants are hand-tuned per platform and intentionally diverge in wording and even
12
- * structure, so byte/heading equality would be pure false positives.
5
+ * Source of truth is now `ai-src/` (one file per skill/command, cap-fenced for per-assistant
6
+ * deltas) NOT the retired per-assistant `ai-instructions/` duplicates. Checks:
7
+ * - skills: `ai-src/skills/<dir>/SKILL.md` has `name:` + `description:` frontmatter
8
+ * - commands: `ai-src/commands/<name>.md` has `description:` frontmatter
9
+ * - cap-fence validity: every {{#cap}}/{{^cap}} uses a known tag and is closed
10
+ * - line budget: context-cost smell (advisory)
11
+ * Aux fragments (viewpoint group-*.md, capture mode files) load via a parent and need no
12
+ * frontmatter, so only `SKILL.md` is frontmatter-checked.
13
13
  */
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import { AI_RULES_FILE_MAPPING } from '../../orchestrator/ai-rules-updater';
16
+ import { stripCaps, VALID_CAPS } from '../../orchestrator/ai-skills/cap-stripper';
17
17
 
18
18
  export interface SkillLintFinding { level: 'error' | 'warn'; file: string; rule: string; detail: string }
19
19
  export interface SkillLintResult { checked: number; findings: SkillLintFinding[]; errors: number }
20
20
 
21
- const LINE_BUDGET = 700; // a skill much larger than this is a context-cost smell (warn)
22
- const SKILL_RE = /^(claude|github)-skill-/;
21
+ const LINE_BUDGET = 700;
23
22
 
24
- function stripFrontmatter(text: string): { fm: string | null; body: string } {
23
+ function frontmatter(text: string): string | null {
25
24
  const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
26
- if (!m) return { fm: null, body: text };
27
- return { fm: m[1], body: text.slice(m[0].length) };
25
+ return m ? m[1] : null;
28
26
  }
29
27
 
30
- /** Lint the AI-instruction templates in `dir` (default: the sungen source templates). */
31
- export function lintSkills(dir: string): SkillLintResult {
28
+ /** Lint the single-source templates under `srcRoot` (the `ai-src` dir). */
29
+ export function lintSkills(srcRoot: string): SkillLintResult {
32
30
  const findings: SkillLintFinding[] = [];
33
- const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
34
- const skillFiles = files.filter((f) => SKILL_RE.test(f));
31
+ let checked = 0;
35
32
 
36
- // mapping: template file -> install target (source of truth for "is this a top-level skill")
37
- const target = new Map<string, string>(AI_RULES_FILE_MAPPING.map(([tpl, dst]) => [tpl, dst]));
38
- const isTopLevelSkill = (f: string) => (target.get(f) || '').endsWith('/SKILL.md');
33
+ const checkCaps = (rel: string, text: string) => {
34
+ try { for (const cap of VALID_CAPS) stripCaps(text, [cap], rel); stripCaps(text, [], rel); }
35
+ catch (e) { findings.push({ level: 'error', file: rel, rule: 'cap-fence', detail: String((e as Error).message) }); }
36
+ };
37
+ const lineBudget = (rel: string, text: string) => {
38
+ const n = text.split('\n').length;
39
+ if (n > LINE_BUDGET) findings.push({ level: 'warn', file: rel, rule: 'line-budget', detail: `${n} lines > ${LINE_BUDGET} (context-cost smell)` });
40
+ };
39
41
 
40
- // 1) registration integrity (bidirectional) — the highest-value check:
41
- // a skill file missing from the mapping never installs; a mapping to a missing file
42
- // ships a broken/empty skill.
43
- for (const f of skillFiles) {
44
- if (!target.has(f)) findings.push({ level: 'error', file: f, rule: 'unregistered', detail: 'skill template not in AI_RULES_FILE_MAPPING (it would never be installed)' });
45
- }
46
- for (const [tpl] of AI_RULES_FILE_MAPPING) {
47
- if (!fs.existsSync(path.join(dir, tpl))) findings.push({ level: 'error', file: tpl, rule: 'mapped-missing', detail: 'AI_RULES_FILE_MAPPING points to a template that does not exist' });
48
- }
49
-
50
- // 2) frontmatter (name + description) — ONLY for top-level skills (SKILL.md targets).
51
- // Sub-content fragments (mode-*.md, group-*.md) are loaded by their parent router
52
- // and legitimately carry no frontmatter.
53
- for (const f of skillFiles) {
54
- if (!isTopLevelSkill(f)) continue;
55
- const text = fs.readFileSync(path.join(dir, f), 'utf8');
56
- const { fm } = stripFrontmatter(text);
57
- if (!fm) { findings.push({ level: 'error', file: f, rule: 'frontmatter', detail: 'top-level skill (SKILL.md) is missing --- frontmatter --- (Claude/Copilot will not load it)' }); continue; }
58
- if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: f, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
59
- if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: f, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
42
+ // Skills
43
+ const skillsDir = path.join(srcRoot, 'skills');
44
+ for (const dir of fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir) : []) {
45
+ const dp = path.join(skillsDir, dir);
46
+ if (!fs.statSync(dp).isDirectory()) continue;
47
+ for (const file of fs.readdirSync(dp)) {
48
+ if (!file.endsWith('.md')) continue;
49
+ const rel = `skills/${dir}/${file}`;
50
+ const text = fs.readFileSync(path.join(dp, file), 'utf8');
51
+ checkCaps(rel, text);
52
+ lineBudget(rel, text);
53
+ if (file === 'SKILL.md') {
54
+ checked++;
55
+ const fm = frontmatter(text);
56
+ if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'SKILL.md missing --- frontmatter ---' }); continue; }
57
+ if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
58
+ if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
59
+ }
60
+ }
60
61
  }
61
62
 
62
- // 3) line budget — context-cost smell (advisory).
63
- for (const f of skillFiles) {
64
- const lines = fs.readFileSync(path.join(dir, f), 'utf8').split('\n').length;
65
- if (lines > LINE_BUDGET) findings.push({ level: 'warn', file: f, rule: 'line-budget', detail: `${lines} lines > ${LINE_BUDGET} (context-cost smell)` });
63
+ // Agents (Claude-only sub-agents)
64
+ const agentsDir = path.join(srcRoot, 'agents');
65
+ for (const file of fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir) : []) {
66
+ if (!file.endsWith('.md')) continue;
67
+ const rel = `agents/${file}`;
68
+ const text = fs.readFileSync(path.join(agentsDir, file), 'utf8');
69
+ checked++;
70
+ checkCaps(rel, text);
71
+ lineBudget(rel, text);
72
+ const fm = frontmatter(text);
73
+ if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'agent missing --- frontmatter ---' }); continue; }
74
+ if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
75
+ if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
66
76
  }
67
77
 
68
- // 4) variant PRESENCE (not body equality) — every top-level skill should ship for both
69
- // platforms. Catches "added a Claude skill but forgot the Copilot variant". Advisory.
70
- const skillName = (dst: string) => { const m = dst.match(/\/(sungen-[^/]+)\/SKILL\.md$/); return m ? m[1] : null; };
71
- const claudeSkills = new Set<string>(), githubSkills = new Set<string>();
72
- for (const f of skillFiles) {
73
- if (!isTopLevelSkill(f)) continue;
74
- const name = skillName(target.get(f)!); if (!name) continue;
75
- (f.startsWith('claude-') ? claudeSkills : githubSkills).add(name);
78
+ // Commands
79
+ const cmdsDir = path.join(srcRoot, 'commands');
80
+ for (const file of fs.existsSync(cmdsDir) ? fs.readdirSync(cmdsDir) : []) {
81
+ if (!file.endsWith('.md')) continue;
82
+ const rel = `commands/${file}`;
83
+ const text = fs.readFileSync(path.join(cmdsDir, file), 'utf8');
84
+ checked++;
85
+ checkCaps(rel, text);
86
+ lineBudget(rel, text);
87
+ const fm = frontmatter(text);
88
+ if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'command missing --- frontmatter ---' }); continue; }
89
+ if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
76
90
  }
77
- for (const n of claudeSkills) if (!githubSkills.has(n)) findings.push({ level: 'warn', file: `claude .../${n}/SKILL.md`, rule: 'variant-missing', detail: `Claude skill "${n}" has no GitHub (Copilot) variant` });
78
- for (const n of githubSkills) if (!claudeSkills.has(n)) findings.push({ level: 'warn', file: `github .../${n}/SKILL.md`, rule: 'variant-missing', detail: `GitHub skill "${n}" has no Claude variant` });
79
91
 
80
- return { checked: skillFiles.length, findings, errors: findings.filter((f) => f.level === 'error').length };
92
+ return { checked, findings, errors: findings.filter((f) => f.level === 'error').length };
81
93
  }
82
94
 
83
- /** Default templates dir, resolved relative to this module (works from src via tsx and dist). */
95
+ /** Default source dir, resolved relative to this module (works from src via tsx and dist). */
84
96
  export function defaultSkillDir(): string {
85
- // src/harness/eval src/orchestrator/... | dist/harness/eval dist/orchestrator/...
86
- return path.resolve(__dirname, '..', '..', 'orchestrator', 'templates', 'ai-instructions');
97
+ return path.resolve(__dirname, '..', '..', 'orchestrator', 'templates', 'ai-src');
87
98
  }