@open-mercato/cli 0.6.7 → 0.6.8-canary.319.1.5b4be4bc52

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 (287) hide show
  1. package/.turbo/turbo-build.log +77 -4
  2. package/build.mjs +68 -11
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  4. package/dist/agentic/codex/enforcement-rules.md +2 -16
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  9. package/dist/agentic/guides/ai-workflows.md +76 -0
  10. package/dist/agentic/guides/architecture.md +107 -0
  11. package/dist/agentic/guides/backend-ui.md +84 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +121 -0
  14. package/dist/agentic/guides/framework-extension-points.md +36 -0
  15. package/dist/agentic/guides/integrations.md +81 -0
  16. package/dist/agentic/guides/module-facts.json +116757 -3951
  17. package/dist/agentic/guides/module-system.md +12 -127
  18. package/dist/agentic/guides/modules/ai_assistant.md +227 -28
  19. package/dist/agentic/guides/modules/api_docs.md +58 -2
  20. package/dist/agentic/guides/modules/api_keys.md +92 -7
  21. package/dist/agentic/guides/modules/attachments.md +119 -14
  22. package/dist/agentic/guides/modules/audit_logs.md +131 -10
  23. package/dist/agentic/guides/modules/auth.md +276 -33
  24. package/dist/agentic/guides/modules/business_rules.md +149 -14
  25. package/dist/agentic/guides/modules/catalog.md +482 -43
  26. package/dist/agentic/guides/modules/channel_gmail.md +82 -3
  27. package/dist/agentic/guides/modules/channel_imap.md +82 -3
  28. package/dist/agentic/guides/modules/checkout.md +305 -27
  29. package/dist/agentic/guides/modules/communication_channels.md +315 -33
  30. package/dist/agentic/guides/modules/configs.md +104 -8
  31. package/dist/agentic/guides/modules/content.md +57 -2
  32. package/dist/agentic/guides/modules/currencies.md +152 -17
  33. package/dist/agentic/guides/modules/customer_accounts.md +387 -45
  34. package/dist/agentic/guides/modules/customers.md +746 -85
  35. package/dist/agentic/guides/modules/dashboards.md +155 -10
  36. package/dist/agentic/guides/modules/data_sync.md +149 -16
  37. package/dist/agentic/guides/modules/design_system.md +102 -0
  38. package/dist/agentic/guides/modules/dictionaries.md +107 -12
  39. package/dist/agentic/guides/modules/directory.md +148 -15
  40. package/dist/agentic/guides/modules/entities.md +145 -13
  41. package/dist/agentic/guides/modules/events.md +59 -2
  42. package/dist/agentic/guides/modules/feature_toggles.md +119 -9
  43. package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
  44. package/dist/agentic/guides/modules/generators.md +45 -2
  45. package/dist/agentic/guides/modules/inbox_ops.md +207 -27
  46. package/dist/agentic/guides/modules/integrations.md +149 -16
  47. package/dist/agentic/guides/modules/messages.md +223 -24
  48. package/dist/agentic/guides/modules/notifications.md +96 -8
  49. package/dist/agentic/guides/modules/onboarding.md +77 -6
  50. package/dist/agentic/guides/modules/payment_gateways.md +168 -21
  51. package/dist/agentic/guides/modules/perspectives.md +77 -7
  52. package/dist/agentic/guides/modules/planner.md +136 -18
  53. package/dist/agentic/guides/modules/portal.md +93 -2
  54. package/dist/agentic/guides/modules/progress.md +104 -14
  55. package/dist/agentic/guides/modules/query_index.md +154 -15
  56. package/dist/agentic/guides/modules/record_locks.md +206 -21
  57. package/dist/agentic/guides/modules/resources.md +197 -30
  58. package/dist/agentic/guides/modules/sales.md +687 -84
  59. package/dist/agentic/guides/modules/scheduler.md +133 -14
  60. package/dist/agentic/guides/modules/search.md +170 -6
  61. package/dist/agentic/guides/modules/security.md +294 -35
  62. package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
  63. package/dist/agentic/guides/modules/sso.md +151 -26
  64. package/dist/agentic/guides/modules/staff.md +372 -57
  65. package/dist/agentic/guides/modules/storage_s3.md +95 -5
  66. package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
  67. package/dist/agentic/guides/modules/sync_excel.md +75 -6
  68. package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
  69. package/dist/agentic/guides/modules/translations.md +104 -10
  70. package/dist/agentic/guides/modules/webhooks.md +203 -24
  71. package/dist/agentic/guides/modules/wms.md +469 -47
  72. package/dist/agentic/guides/modules/workflows.md +302 -43
  73. package/dist/agentic/guides/reference-module-facts.json +6370 -0
  74. package/dist/agentic/guides/reference-modules/example.md +451 -0
  75. package/dist/agentic/guides/spec-delivery.md +42 -0
  76. package/dist/agentic/guides/testing-debugging.md +80 -0
  77. package/dist/agentic/guides/upstream/AGENTS.md +299 -0
  78. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
  79. package/dist/agentic/guides/upstream/manifest.json +8 -0
  80. package/dist/agentic/shared/AGENTS.md.template +120 -311
  81. package/dist/agentic/shared/ai/agentic.config.json +2 -0
  82. package/dist/agentic/shared/ai/harness/README.md +50 -0
  83. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  84. package/dist/agentic/shared/ai/harness/cases.json +15050 -0
  85. package/dist/agentic/shared/ai/harness/cases.schema.json +188 -0
  86. package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
  87. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -0
  88. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
  89. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
  90. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
  91. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  92. package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
  93. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  94. package/dist/agentic/shared/ai/harness/result.schema.json +109 -0
  95. package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
  96. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  97. package/dist/agentic/shared/ai/harness/validators.json +82 -0
  98. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
  99. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  100. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +412 -0
  101. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  102. package/dist/agentic/shared/ai/lessons.md +26 -3
  103. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  104. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
  105. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  106. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
  107. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +21 -241
  108. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -0
  109. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  110. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +15 -0
  111. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  112. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  113. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  114. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  115. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  116. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  117. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  118. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  119. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  120. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  121. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +17 -601
  122. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
  123. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
  124. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -0
  125. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  126. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +15 -310
  127. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
  128. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -0
  129. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
  130. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  131. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +115 -0
  132. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  133. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  134. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  135. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  136. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  137. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
  138. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  139. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
  140. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
  141. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
  142. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
  143. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
  144. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +25 -708
  145. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  146. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +25 -0
  147. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  148. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  149. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  150. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  151. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  152. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  153. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  154. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  155. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  156. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  157. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +32 -0
  158. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
  159. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +14 -0
  160. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  161. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +40 -0
  162. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  163. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  164. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  165. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
  166. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
  167. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
  168. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
  169. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
  170. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
  171. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
  172. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
  173. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +20 -843
  174. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -0
  175. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
  176. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  177. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +43 -0
  178. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  179. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  180. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  181. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +17 -467
  182. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -0
  183. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  184. package/dist/agentic/shared/ai/skills/tiers.json +143 -21
  185. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  186. package/dist/agentic/shared/ai/specs/README.md +4 -3
  187. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
  188. package/dist/agentic/shared/ai/trackers/github.md +108 -4
  189. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +224 -0
  190. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  191. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3585 -0
  192. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  193. package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
  194. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  195. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  196. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  197. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
  198. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +911 -0
  199. package/dist/bin.js +13 -1
  200. package/dist/bin.js.map +2 -2
  201. package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
  202. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  203. package/dist/lib/agentic-init.js +13 -3
  204. package/dist/lib/agentic-init.js.map +2 -2
  205. package/dist/lib/agentic-setup.js +434 -186
  206. package/dist/lib/agentic-setup.js.map +2 -2
  207. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  208. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  209. package/dist/lib/generators/module-extension-facts.js +1872 -0
  210. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  211. package/dist/lib/generators/module-fact-sources.js +70 -0
  212. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  213. package/dist/lib/generators/module-facts-discovery.js +64 -7
  214. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  215. package/dist/lib/generators/module-facts.js +1629 -52
  216. package/dist/lib/generators/module-facts.js.map +3 -3
  217. package/dist/lib/generators/module-override-targets.js +688 -0
  218. package/dist/lib/generators/module-override-targets.js.map +7 -0
  219. package/dist/lib/generators/module-registry.js +11 -5
  220. package/dist/lib/generators/module-registry.js.map +2 -2
  221. package/dist/lib/load-env.js +24 -0
  222. package/dist/lib/load-env.js.map +7 -0
  223. package/dist/lib/resolver.js +2 -0
  224. package/dist/lib/resolver.js.map +2 -2
  225. package/dist/lib/telemetry-init.js +358 -0
  226. package/dist/lib/telemetry-init.js.map +7 -0
  227. package/dist/lib/testing/integration.js +5 -2
  228. package/dist/lib/testing/integration.js.map +2 -2
  229. package/dist/mercato.js +23 -1
  230. package/dist/mercato.js.map +2 -2
  231. package/package.json +13 -11
  232. package/src/bin.ts +33 -1
  233. package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
  234. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  235. package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
  236. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  237. package/src/lib/__tests__/load-env.test.ts +65 -0
  238. package/src/lib/__tests__/telemetry-init.test.ts +298 -0
  239. package/src/lib/agentic-init.ts +15 -3
  240. package/src/lib/agentic-setup.ts +573 -222
  241. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  242. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  243. package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
  244. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
  245. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  246. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  247. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  248. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  249. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +391 -0
  250. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  251. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  252. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
  253. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  254. package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
  255. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  256. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  257. package/src/lib/generators/module-extension-facts.ts +2433 -0
  258. package/src/lib/generators/module-fact-sources.ts +108 -0
  259. package/src/lib/generators/module-facts-discovery.ts +104 -7
  260. package/src/lib/generators/module-facts.ts +2387 -138
  261. package/src/lib/generators/module-override-targets.ts +863 -0
  262. package/src/lib/generators/module-registry.ts +16 -6
  263. package/src/lib/load-env.ts +30 -0
  264. package/src/lib/resolver.ts +3 -0
  265. package/src/lib/telemetry-init.ts +461 -0
  266. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  267. package/src/lib/testing/integration.ts +5 -2
  268. package/src/mercato.ts +26 -1
  269. package/dist/agentic/guides/cache.md +0 -45
  270. package/dist/agentic/guides/core.md +0 -453
  271. package/dist/agentic/guides/events.md +0 -117
  272. package/dist/agentic/guides/queue.md +0 -53
  273. package/dist/agentic/guides/search.md +0 -115
  274. package/dist/agentic/guides/shared.md +0 -108
  275. package/dist/agentic/guides/ui.md +0 -347
  276. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  277. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  278. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  279. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  280. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  281. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  282. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  283. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  284. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  285. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  286. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  287. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -6,16 +6,35 @@
6
6
  * This module reads those files at runtime — no embedded string constants.
7
7
  */
8
8
 
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, lstatSync, unlinkSync, readdirSync } from 'node:fs'
9
+ import {
10
+ chmodSync,
11
+ copyFileSync,
12
+ existsSync,
13
+ lstatSync,
14
+ mkdirSync,
15
+ mkdtempSync,
16
+ readFileSync,
17
+ readdirSync,
18
+ realpathSync,
19
+ renameSync,
20
+ rmSync,
21
+ statSync,
22
+ writeFileSync,
23
+ } from 'node:fs'
24
+ import { createHash } from 'node:crypto'
10
25
  import { spawnSync } from 'node:child_process'
11
- import { join, dirname, basename } from 'node:path'
26
+ import { tmpdir } from 'node:os'
27
+ import { join, dirname, basename, relative, resolve, sep } from 'node:path'
12
28
  import { fileURLToPath } from 'node:url'
13
29
  import { Project, SyntaxKind } from 'ts-morph'
14
30
 
15
- const __dirname = dirname(fileURLToPath(import.meta.url))
16
- // In the built output (dist/lib/agentic-setup.js), __dirname is dist/lib/.
31
+ const moduleDir = dirname(fileURLToPath(import.meta.url))
32
+ // In the built output (dist/lib/agentic-setup.js), moduleDir is dist/lib/.
17
33
  // agentic/ is copied to dist/agentic/ by build.mjs.
18
- const AGENTIC_DIR = join(__dirname, '..', 'agentic')
34
+ const bundledAgenticDir = join(moduleDir, '..', 'agentic')
35
+ const AGENTIC_DIR = existsSync(bundledAgenticDir)
36
+ ? bundledAgenticDir
37
+ : join(moduleDir, '..', '..', '..', 'create-app', 'agentic')
19
38
  const GUIDES_DIR = join(AGENTIC_DIR, 'guides')
20
39
 
21
40
  type AskFn = (question: string) => Promise<string>
@@ -23,6 +42,7 @@ type AskFn = (question: string) => Promise<string>
23
42
  interface AgenticSetupOptions {
24
43
  tool?: string
25
44
  force?: boolean
45
+ updateHarness?: boolean
26
46
  }
27
47
 
28
48
  interface AgenticConfig {
@@ -30,6 +50,19 @@ interface AgenticConfig {
30
50
  targetDir: string
31
51
  }
32
52
 
53
+ interface HarnessManifestFile {
54
+ path: string
55
+ sha256: string
56
+ source: string
57
+ userEditable: boolean
58
+ }
59
+
60
+ interface HarnessManifest {
61
+ version: number
62
+ generator: string
63
+ files: HarnessManifestFile[]
64
+ }
65
+
33
66
  // ─── Helpers ─────────────────────────────────────────────────────────────
34
67
 
35
68
  function resolvePlaceholders(content: string, config: AgenticConfig): string {
@@ -59,14 +92,14 @@ function copyFile(srcDir: string, srcRelative: string, destPath: string): void {
59
92
  // AST-parse the static `enabledModules` array literal in the app's src/modules.ts
60
93
  // and collect each entry's `id`. Only the static literal is read (conditional
61
94
  // .push()/spread entries are intentionally not seen — see spec D6).
62
- function readEnabledModuleIds(modulesPath: string): string[] {
63
- if (!existsSync(modulesPath)) return []
95
+ function readEnabledModuleIds(modulesPath: string): { parsed: boolean; ids: string[] } {
96
+ if (!existsSync(modulesPath)) return { parsed: false, ids: [] }
64
97
  try {
65
98
  const project = new Project({ useInMemoryFileSystem: true })
66
99
  const sourceFile = project.createSourceFile('modules.ts', readFileSync(modulesPath, 'utf-8'))
67
100
  const declaration = sourceFile.getVariableDeclaration('enabledModules')
68
101
  const arrayLiteral = declaration?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
69
- if (!arrayLiteral) return []
102
+ if (!arrayLiteral) return { parsed: false, ids: [] }
70
103
  const ids: string[] = []
71
104
  for (const element of arrayLiteral.getElements()) {
72
105
  const objectLiteral = element.asKind(SyntaxKind.ObjectLiteralExpression)
@@ -75,9 +108,9 @@ function readEnabledModuleIds(modulesPath: string): string[] {
75
108
  const idValue = idProperty?.getInitializerIfKind(SyntaxKind.StringLiteral)?.getLiteralValue()
76
109
  if (idValue) ids.push(idValue)
77
110
  }
78
- return ids
111
+ return { parsed: true, ids }
79
112
  } catch {
80
- return []
113
+ return { parsed: false, ids: [] }
81
114
  }
82
115
  }
83
116
 
@@ -92,48 +125,40 @@ function selectModuleFactSheets(targetDir: string, modulesSubdir: string): strin
92
125
  .map((file) => file.replace(/\.md$/, ''))
93
126
  : []
94
127
  if (available.length === 0) return []
95
- const enabled = new Set(readEnabledModuleIds(join(targetDir, 'src', 'modules.ts')))
128
+ const parsed = readEnabledModuleIds(join(targetDir, 'src', 'modules.ts'))
129
+ if (!parsed.parsed) return available
130
+ const enabled = new Set(parsed.ids)
96
131
  const selected = available.filter((moduleId) => enabled.has(moduleId))
97
- return selected.length > 0 ? selected : available
132
+ return selected
98
133
  }
99
134
 
100
135
  const MODULE_GUIDES_START = '<!-- om:module-guides:start -->'
101
136
  const MODULE_GUIDES_END = '<!-- om:module-guides:end -->'
102
137
 
103
- // Read each module's guide label from the bundled `module-facts.json` (emitted by
104
- // build.mjs from the generator's extraction of each module's own `metadata`). The
105
- // label falls back description → title → generic, so the CLI never re-declares
106
- // specific module names or descriptions. A missing/malformed sidecar degrades to an
107
- // empty map (generic labels), never a throw.
108
- function readModuleGuideLabels(guidesDir: string): Record<string, string> {
109
- const factsPath = join(guidesDir, 'module-facts.json')
110
- if (!existsSync(factsPath)) return {}
111
- try {
112
- const parsed = JSON.parse(readFileSync(factsPath, 'utf-8')) as Record<
113
- string,
114
- { description?: unknown; title?: unknown }
115
- >
116
- const labels: Record<string, string> = {}
117
- for (const [moduleId, entry] of Object.entries(parsed)) {
118
- const label =
119
- (entry && typeof entry.description === 'string' && entry.description) ||
120
- (entry && typeof entry.title === 'string' && entry.title) ||
121
- ''
122
- if (label) labels[moduleId] = label
123
- }
124
- return labels
125
- } catch {
126
- return {}
127
- }
138
+ // The generated standalone root must stay well under Codex's 32 KiB
139
+ // project_doc_max_bytes so a routed chain (root + guide + skill) still fits.
140
+ export const STANDALONE_ROOT_TARGET_BYTES = 12 * 1024
141
+
142
+ export type ModuleGuidesRenderOptions = {
143
+ // Emit the O(1) pointer form instead of enumerating every id. The inline index
144
+ // is the better prompt, so callers only ask for this when the enumerated one
145
+ // would push the root past its byte target.
146
+ compact?: boolean
128
147
  }
129
148
 
130
- function renderModuleGuidesBlock(selected: string[], labels: Record<string, string>): string {
149
+ export function renderModuleGuidesBlock(
150
+ selected: string[],
151
+ options: ModuleGuidesRenderOptions = {},
152
+ ): string {
131
153
  if (selected.length === 0) return '_No module fact-sheets are bundled for this app._'
132
- const rows = selected.map((moduleId) => {
133
- const label = labels[moduleId] ?? `Use the ${moduleId} module`
134
- return `| ${label} | \`.ai/guides/modules/${moduleId}.md\` |`
135
- })
136
- return ['| Task | Load |', '|---|---|', ...rows].join('\n')
154
+ const index = options.compact
155
+ ? `Enabled module facts: ${selected.length} sheets bundled, too many to index inline — list the module facts directory to see which modules have one.`
156
+ : `Enabled module facts: ${selected.map((moduleId) => `\`${moduleId}\``).join(',')}.`
157
+ return [
158
+ index,
159
+ '',
160
+ 'Load `.ai/guides/modules/<id>.md` only for a named or targeted installed module/host; never preload all module facts.',
161
+ ].join('\n')
137
162
  }
138
163
 
139
164
  // Regenerate the marker-delimited Module-Specific Guides block in the written
@@ -142,7 +167,7 @@ function renderModuleGuidesBlock(selected: string[], labels: Record<string, stri
142
167
  function injectModuleGuides(
143
168
  agentsMdPath: string,
144
169
  selected: string[],
145
- labels: Record<string, string> = {},
170
+ options: ModuleGuidesRenderOptions = {},
146
171
  ): void {
147
172
  if (!existsSync(agentsMdPath)) return
148
173
  const content = readFileSync(agentsMdPath, 'utf-8')
@@ -156,188 +181,443 @@ function injectModuleGuides(
156
181
  }
157
182
  const before = content.slice(0, startIndex + MODULE_GUIDES_START.length)
158
183
  const after = content.slice(endIndex)
159
- const next = `${before}\n${renderModuleGuidesBlock(selected, labels)}\n${after}`
184
+ const next = `${before}\n${renderModuleGuidesBlock(selected, options)}\n${after}`
160
185
  if (next !== content) writeFileSync(agentsMdPath, next)
161
186
  }
162
187
 
163
- // ─── Generators ──────────────────────────────────────────────────────────
188
+ // Last step of harness generation: every tool generator has finished patching
189
+ // AGENTS.md, so this is the only point where the final root size is known. The
190
+ // enumerated module index is the one block that grows with the app, so it is
191
+ // also the one that sheds bytes when the root would otherwise blow its target.
192
+ export function enforceRootInstructionBudget(
193
+ agentsMdPath: string,
194
+ selected: string[],
195
+ maxBytes: number = STANDALONE_ROOT_TARGET_BYTES,
196
+ ): boolean {
197
+ if (!existsSync(agentsMdPath)) return false
198
+ if (Buffer.byteLength(readFileSync(agentsMdPath)) <= maxBytes) return false
199
+ injectModuleGuides(agentsMdPath, selected, { compact: true })
200
+ return true
201
+ }
164
202
 
165
- function generateShared(config: AgenticConfig): void {
166
- const { targetDir } = config
167
- const srcDir = join(AGENTIC_DIR, 'shared')
203
+ const TEXT_EXTENSIONS = new Set(['.cjs', '.json', '.md', '.mdc', '.mjs', '.sh', '.ts', '.txt'])
168
204
 
169
- // Resolve which per-module fact-sheets this app gets (enabled ∩ bundled allowlist).
170
- const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
171
- const moduleGuideLabels = readModuleGuideLabels(GUIDES_DIR)
205
+ function isTextAsset(path: string): boolean {
206
+ const dot = path.lastIndexOf('.')
207
+ return dot === -1 || TEXT_EXTENSIONS.has(path.slice(dot))
208
+ }
172
209
 
173
- // AGENTS.md
174
- writeTemplate(srcDir, 'AGENTS.md.template', join(targetDir, 'AGENTS.md'), config)
175
- injectModuleGuides(join(targetDir, 'AGENTS.md'), selectedModules, moduleGuideLabels)
176
-
177
- // .ai/ structure
178
- writeTemplate(srcDir, 'ai/specs/README.md', join(targetDir, '.ai', 'specs', 'README.md'), config)
179
- copyFile(srcDir, 'ai/specs/SPEC-000-template.md', join(targetDir, '.ai', 'specs', 'SPEC-000-template.md'))
180
- copyFile(srcDir, 'ai/lessons.md', join(targetDir, '.ai', 'lessons.md'))
181
-
182
- // .ai/skills/ — the shared skills-mixin manifest + tracker + external installer.
183
- // Skills owned by the external open-mercato/skills collection (code review, spec
184
- // writing, integration tests, prepare-issue, the auto-* PR family) are NOT copied
185
- // here; the user installs them with `yarn install-skills` (npx skills add). Only
186
- // standalone-only + kept-local skills and repo-local override folders are copied.
187
- copyFile(srcDir, 'ai/skills/tiers.json', join(targetDir, '.ai', 'skills', 'tiers.json'))
188
- copyFile(srcDir, 'ai/skills/tiers.schema.json', join(targetDir, '.ai', 'skills', 'tiers.schema.json'))
189
- copyFile(srcDir, 'ai/agentic.config.json', join(targetDir, '.ai', 'agentic.config.json'))
190
- copyFile(srcDir, 'ai/trackers/github.md', join(targetDir, '.ai', 'trackers', 'github.md'))
191
- copyFile(srcDir, 'scripts/install-skills.sh', join(targetDir, 'scripts', 'install-skills.sh'))
192
-
193
- copyFile(
194
- srcDir,
195
- 'ai/skills/om-backend-ui-design/SKILL.md',
196
- join(targetDir, '.ai', 'skills', 'om-backend-ui-design', 'SKILL.md'),
197
- )
198
- copyFile(
199
- srcDir,
200
- 'ai/skills/om-backend-ui-design/references/ui-components.md',
201
- join(targetDir, '.ai', 'skills', 'om-backend-ui-design', 'references', 'ui-components.md'),
202
- )
203
- copyFile(srcDir, 'ai/skills/om-integration-builder/SKILL.md', join(targetDir, '.ai', 'skills', 'om-integration-builder', 'SKILL.md'))
204
- if (existsSync(join(srcDir, 'ai', 'skills', 'om-integration-builder', 'STANDALONE.md'))) {
205
- copyFile(
206
- srcDir,
207
- 'ai/skills/om-integration-builder/STANDALONE.md',
208
- join(targetDir, '.ai', 'skills', 'om-integration-builder', 'STANDALONE.md'),
209
- )
210
+ function listFiles(root: string): string[] {
211
+ if (!existsSync(root)) return []
212
+ const files: string[] = []
213
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
214
+ const absolute = join(root, entry.name)
215
+ if (entry.isDirectory()) files.push(...listFiles(absolute))
216
+ else if (entry.isFile()) files.push(absolute)
210
217
  }
211
- copyFile(
212
- srcDir,
213
- 'ai/skills/om-integration-builder/references/adapter-contracts.md',
214
- join(targetDir, '.ai', 'skills', 'om-integration-builder', 'references', 'adapter-contracts.md'),
215
- )
216
- if (existsSync(join(srcDir, 'ai', 'skills', 'om-integration-builder', 'STANDALONE.md'))) {
217
- copyFile(
218
- srcDir,
219
- 'ai/skills/om-integration-builder/STANDALONE.md',
220
- join(targetDir, '.ai', 'skills', 'om-integration-builder', 'STANDALONE.md'),
221
- )
218
+ return files
219
+ }
220
+
221
+ function copyTree(sourceRoot: string, destinationRoot: string, config: AgenticConfig): void {
222
+ for (const sourcePath of listFiles(sourceRoot)) {
223
+ const destinationPath = join(destinationRoot, relative(sourceRoot, sourcePath))
224
+ ensureDir(destinationPath)
225
+ if (isTextAsset(sourcePath)) {
226
+ writeFileSync(destinationPath, resolvePlaceholders(readFileSync(sourcePath, 'utf8'), config))
227
+ } else {
228
+ copyFileSync(sourcePath, destinationPath)
229
+ }
230
+ if (process.platform !== 'win32') chmodSync(destinationPath, statSync(sourcePath).mode & 0o777)
222
231
  }
232
+ }
223
233
 
224
- copyFile(
225
- srcDir,
226
- 'ai/skills/om-system-extension/SKILL.md',
227
- join(targetDir, '.ai', 'skills', 'om-system-extension', 'SKILL.md'),
228
- )
229
- copyFile(
230
- srcDir,
231
- 'ai/skills/om-system-extension/references/extension-contracts.md',
232
- join(targetDir, '.ai', 'skills', 'om-system-extension', 'references', 'extension-contracts.md'),
233
- )
234
+ function hashFile(path: string): string {
235
+ return createHash('sha256').update(readFileSync(path)).digest('hex')
236
+ }
234
237
 
235
- copyFile(
236
- srcDir,
237
- 'ai/skills/om-module-scaffold/SKILL.md',
238
- join(targetDir, '.ai', 'skills', 'om-module-scaffold', 'SKILL.md'),
239
- )
240
- copyFile(
241
- srcDir,
242
- 'ai/skills/om-module-scaffold/references/naming-conventions.md',
243
- join(targetDir, '.ai', 'skills', 'om-module-scaffold', 'references', 'naming-conventions.md'),
244
- )
245
- copyFile(
246
- srcDir,
247
- 'ai/skills/om-module-scaffold/references/navigation-patterns.md',
248
- join(targetDir, '.ai', 'skills', 'om-module-scaffold', 'references', 'navigation-patterns.md'),
249
- )
238
+ function canonicalHarnessRoot(root: string): string {
239
+ const resolved = resolve(root)
240
+ if (!existsSync(resolved)) {
241
+ throw new Error(`Harness root must be an existing directory: ${resolved}`)
242
+ }
243
+ const canonical = realpathSync(resolved)
244
+ const entry = lstatSync(canonical, { throwIfNoEntry: false })
245
+ if (!entry || !entry.isDirectory()) {
246
+ throw new Error(`Harness root must be an existing directory: ${resolved}`)
247
+ }
248
+ return canonical
249
+ }
250
250
 
251
- copyFile(
252
- srcDir,
253
- 'ai/skills/om-troubleshooter/SKILL.md',
254
- join(targetDir, '.ai', 'skills', 'om-troubleshooter', 'SKILL.md'),
255
- )
256
- copyFile(
257
- srcDir,
258
- 'ai/skills/om-troubleshooter/references/diagnostic-commands.md',
259
- join(targetDir, '.ai', 'skills', 'om-troubleshooter', 'references', 'diagnostic-commands.md'),
260
- )
251
+ /**
252
+ * Validate a managed path without following any link below the canonical app
253
+ * root. A lexical containment check alone is insufficient: `app/.ai -> /tmp`
254
+ * would otherwise make an apparently safe update write outside the app.
255
+ */
256
+ function assertManagedPath(root: string, path: string, options: { leaf?: 'file' | 'any' } = {}): void {
257
+ const absolutePath = resolve(path)
258
+ const normalizedRelative = relative(root, absolutePath)
259
+ if (
260
+ normalizedRelative === '..' ||
261
+ normalizedRelative.startsWith(`..${sep}`)
262
+ ) {
263
+ throw new Error(`Harness-managed path escapes its root: ${absolutePath}`)
264
+ }
265
+ if (normalizedRelative === '') return
266
+
267
+ const components = normalizedRelative.split(sep).filter(Boolean)
268
+ let current = root
269
+ for (let index = 0; index < components.length; index += 1) {
270
+ current = join(current, components[index])
271
+ const entry = lstatSync(current, { throwIfNoEntry: false })
272
+ if (!entry) return
273
+ if (entry.isSymbolicLink()) {
274
+ throw new Error(`Harness-managed path contains a symbolic-link component: ${current}`)
275
+ }
276
+ const isLeaf = index === components.length - 1
277
+ if (!isLeaf && !entry.isDirectory()) {
278
+ throw new Error(`Harness-managed path component is not a directory: ${current}`)
279
+ }
280
+ if (isLeaf && options.leaf === 'file' && !entry.isFile()) {
281
+ throw new Error(`Harness-managed file is not a regular file: ${current}`)
282
+ }
283
+ }
284
+ }
261
285
 
262
- copyFile(
263
- srcDir,
264
- 'ai/skills/om-eject-and-customize/SKILL.md',
265
- join(targetDir, '.ai', 'skills', 'om-eject-and-customize', 'SKILL.md'),
266
- )
286
+ function ensureManagedParent(root: string, filePath: string): void {
287
+ const parent = dirname(filePath)
288
+ const normalizedRelative = relative(root, parent)
289
+ if (normalizedRelative === '..' || normalizedRelative.startsWith(`..${sep}`)) {
290
+ throw new Error(`Harness-managed path escapes its root: ${filePath}`)
291
+ }
292
+ let current = root
293
+ for (const component of normalizedRelative.split(sep).filter(Boolean)) {
294
+ assertManagedPath(root, current)
295
+ current = join(current, component)
296
+ const entry = lstatSync(current, { throwIfNoEntry: false })
297
+ if (!entry) mkdirSync(current)
298
+ const created = lstatSync(current, { throwIfNoEntry: false })
299
+ if (!created || created.isSymbolicLink() || !created.isDirectory()) {
300
+ throw new Error(`Harness-managed path component is not a real directory: ${current}`)
301
+ }
302
+ }
303
+ }
267
304
 
268
- copyFile(
269
- srcDir,
270
- 'ai/skills/om-data-model-design/SKILL.md',
271
- join(targetDir, '.ai', 'skills', 'om-data-model-design', 'SKILL.md'),
272
- )
273
- copyFile(
274
- srcDir,
275
- 'ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md',
276
- join(targetDir, '.ai', 'skills', 'om-data-model-design', 'references', 'mikro-orm-cheatsheet.md'),
277
- )
305
+ function harnessGeneratorId(): string {
306
+ try {
307
+ const upstream = JSON.parse(readFileSync(join(GUIDES_DIR, 'upstream', 'manifest.json'), 'utf8')) as {
308
+ generator?: unknown
309
+ }
310
+ const generator = typeof upstream.generator === 'string' ? upstream.generator : ''
311
+ const version = generator.slice(generator.lastIndexOf('@') + 1)
312
+ if (version && version !== generator) return `open-mercato-agentic@${version}`
313
+ } catch {
314
+ // Source-mode tests may not have built upstream snapshots yet.
315
+ }
316
+ return 'open-mercato-agentic@unknown'
317
+ }
278
318
 
279
- copyFile(
280
- srcDir,
281
- 'ai/skills/om-implement-spec/SKILL.md',
282
- join(targetDir, '.ai', 'skills', 'om-implement-spec', 'SKILL.md'),
283
- )
319
+ function externalSkillNames(targetDir: string): Set<string> {
320
+ try {
321
+ const tiers = JSON.parse(readFileSync(join(targetDir, '.ai', 'skills', 'tiers.json'), 'utf8')) as {
322
+ external?: { skills?: unknown }
323
+ }
324
+ return new Set(Array.isArray(tiers.external?.skills) ? tiers.external.skills.filter((name): name is string => typeof name === 'string') : [])
325
+ } catch {
326
+ return new Set()
327
+ }
328
+ }
284
329
 
285
- copyFile(
286
- srcDir,
287
- 'ai/skills/om-help/SKILL.md',
288
- join(targetDir, '.ai', 'skills', 'om-help', 'SKILL.md'),
289
- )
290
- copyFile(
291
- srcDir,
292
- 'ai/skills/om-help/references/skills-catalog.md',
293
- join(targetDir, '.ai', 'skills', 'om-help', 'references', 'skills-catalog.md'),
294
- )
295
- copyFile(
296
- srcDir,
297
- 'ai/skills/om-help/references/workflow-sequences.md',
298
- join(targetDir, '.ai', 'skills', 'om-help', 'references', 'workflow-sequences.md'),
299
- )
330
+ function atomicCopyFile(sourcePath: string, destinationPath: string, managedRoot?: string): void {
331
+ if (managedRoot) {
332
+ assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
333
+ ensureManagedParent(managedRoot, destinationPath)
334
+ } else {
335
+ ensureDir(destinationPath)
336
+ }
337
+ const temporaryPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`
338
+ try {
339
+ if (managedRoot) assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
340
+ copyFileSync(sourcePath, temporaryPath)
341
+ if (process.platform !== 'win32') chmodSync(temporaryPath, statSync(sourcePath).mode & 0o777)
342
+ if (managedRoot) {
343
+ assertManagedPath(managedRoot, dirname(destinationPath))
344
+ assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
345
+ assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
346
+ }
347
+ renameSync(temporaryPath, destinationPath)
348
+ } finally {
349
+ if (!managedRoot || !lstatSync(temporaryPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
350
+ rmSync(temporaryPath, { force: true })
351
+ }
352
+ }
353
+ }
300
354
 
301
- copyFile(
302
- srcDir,
303
- 'ai/skills/om-auto-upgrade-0.4.10-to-0.5.0/SKILL.md',
304
- join(targetDir, '.ai', 'skills', 'om-auto-upgrade-0.4.10-to-0.5.0', 'SKILL.md'),
355
+ function atomicWriteFile(destinationPath: string, content: string, managedRoot?: string): void {
356
+ if (managedRoot) {
357
+ assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
358
+ ensureManagedParent(managedRoot, destinationPath)
359
+ } else {
360
+ ensureDir(destinationPath)
361
+ }
362
+ const temporaryPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`
363
+ try {
364
+ if (managedRoot) assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
365
+ writeFileSync(temporaryPath, content)
366
+ if (managedRoot) {
367
+ assertManagedPath(managedRoot, dirname(destinationPath))
368
+ assertManagedPath(managedRoot, destinationPath, { leaf: 'file' })
369
+ assertManagedPath(managedRoot, temporaryPath, { leaf: 'file' })
370
+ }
371
+ renameSync(temporaryPath, destinationPath)
372
+ } finally {
373
+ if (!managedRoot || !lstatSync(temporaryPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
374
+ rmSync(temporaryPath, { force: true })
375
+ }
376
+ }
377
+ }
378
+
379
+ function resolveManifestPath(root: string, relativePath: string): string | null {
380
+ if (!relativePath || relativePath.includes('\0')) return null
381
+ const absolutePath = resolve(root, relativePath)
382
+ const normalizedRelative = relative(root, absolutePath)
383
+ if (
384
+ normalizedRelative === '' ||
385
+ normalizedRelative === '..' ||
386
+ normalizedRelative.startsWith(`..${sep}`)
387
+ ) {
388
+ return null
389
+ }
390
+ return absolutePath
391
+ }
392
+
393
+ function normalizeManifestRelativePath(path: string): string {
394
+ return path.replace(/\\/g, '/')
395
+ }
396
+
397
+ function readHarnessManifest(manifestPath: string): HarnessManifest | null {
398
+ if (!existsSync(manifestPath)) return null
399
+ try {
400
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as Partial<HarnessManifest>
401
+ if (
402
+ typeof parsed.version !== 'number' ||
403
+ typeof parsed.generator !== 'string' ||
404
+ !Array.isArray(parsed.files)
405
+ ) {
406
+ return null
407
+ }
408
+ const files: HarnessManifestFile[] = []
409
+ for (const entry of parsed.files) {
410
+ if (
411
+ !entry ||
412
+ typeof entry.path !== 'string' ||
413
+ typeof entry.sha256 !== 'string' ||
414
+ !/^[a-f0-9]{64}$/.test(entry.sha256) ||
415
+ typeof entry.source !== 'string' ||
416
+ typeof entry.userEditable !== 'boolean'
417
+ ) {
418
+ return null
419
+ }
420
+ files.push({ ...entry, path: normalizeManifestRelativePath(entry.path) })
421
+ }
422
+ return { version: parsed.version, generator: parsed.generator, files }
423
+ } catch {
424
+ return null
425
+ }
426
+ }
427
+
428
+ function targetPathsForTree(sourceRoot: string, destinationRoot: string): string[] {
429
+ return listFiles(sourceRoot).map((sourcePath) => join(destinationRoot, relative(sourceRoot, sourcePath)))
430
+ }
431
+
432
+ function finalizeHarnessManifest(config: AgenticConfig, selectedTools: string[]): void {
433
+ const { targetDir } = config
434
+ const srcDir = join(AGENTIC_DIR, 'shared')
435
+ const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
436
+ const paths = new Set<string>([
437
+ join(targetDir, 'AGENTS.md'),
438
+ ...targetPathsForTree(join(srcDir, 'ai'), join(targetDir, '.ai')),
439
+ ...targetPathsForTree(join(srcDir, 'scripts'), join(targetDir, 'scripts')),
440
+ ])
441
+ for (const file of readdirSync(GUIDES_DIR)) {
442
+ if (file.endsWith('.md') || file === 'module-facts.json') paths.add(join(targetDir, '.ai', 'guides', file))
443
+ }
444
+ for (const file of listFiles(join(GUIDES_DIR, 'upstream'))) {
445
+ paths.add(join(targetDir, '.ai', 'guides', 'upstream', relative(join(GUIDES_DIR, 'upstream'), file)))
446
+ }
447
+ for (const moduleId of selectedModules) paths.add(join(targetDir, '.ai', 'guides', 'modules', `${moduleId}.md`))
448
+ if (selectedTools.includes('claude-code')) {
449
+ paths.add(join(targetDir, 'CLAUDE.md'))
450
+ paths.add(join(targetDir, '.claude', 'settings.json'))
451
+ paths.add(join(targetDir, '.claude', 'hooks', 'entity-migration-check.ts'))
452
+ paths.add(join(targetDir, '.mcp.json.example'))
453
+ }
454
+ if (selectedTools.includes('codex')) paths.add(join(targetDir, '.codex', 'mcp.json.example'))
455
+ if (selectedTools.includes('cursor')) {
456
+ for (const file of listFiles(join(AGENTIC_DIR, 'cursor'))) {
457
+ const rel = relative(join(AGENTIC_DIR, 'cursor'), file)
458
+ paths.add(join(targetDir, '.cursor', rel))
459
+ }
460
+ }
461
+ const manifestPath = join(targetDir, '.ai', 'harness', 'manifest.json')
462
+ const externalSkills = externalSkillNames(targetDir)
463
+ paths.delete(manifestPath)
464
+ const files = [...paths]
465
+ .filter((path) => existsSync(path))
466
+ .sort((left, right) => left.localeCompare(right))
467
+ .map((path) => {
468
+ const relativePath = normalizeManifestRelativePath(relative(targetDir, path))
469
+ const skillName = relativePath.match(/^\.ai\/skills\/([^/]+)\//)?.[1]
470
+ return {
471
+ path: relativePath,
472
+ sha256: hashFile(path),
473
+ source: skillName ? (externalSkills.has(skillName) ? 'external-override' : 'local-skill') : 'generated',
474
+ userEditable:
475
+ relativePath === 'AGENTS.md'
476
+ || relativePath === '.ai/agentic.config.json'
477
+ || relativePath === '.ai/lessons.md'
478
+ || relativePath.startsWith('.ai/lessons/'),
479
+ }
480
+ })
481
+ atomicWriteFile(
482
+ manifestPath,
483
+ `${JSON.stringify({ version: 1, generator: harnessGeneratorId(), files }, null, 2)}\n`,
305
484
  )
485
+ }
486
+
487
+ /**
488
+ * Publish a fully generated harness candidate using the previous ownership
489
+ * manifest as the only source of ownership. Modified owned files and exact-path
490
+ * collisions with unknown files stay untouched; their candidate is written next
491
+ * to them as `<path>.incoming` for explicit review.
492
+ */
493
+ export type HarnessUpdateConflict = { path: string; candidate: string | null }
494
+
495
+ function preserveIncomingCandidate(
496
+ targetRoot: string,
497
+ destinationPath: string,
498
+ sourcePath: string,
499
+ relativePath: string,
500
+ ): string {
501
+ for (let suffix = 1; ; suffix += 1) {
502
+ const label = suffix === 1 ? '.incoming' : `.incoming.${suffix}`
503
+ const candidatePath = `${destinationPath}${label}`
504
+ assertManagedPath(targetRoot, candidatePath, { leaf: 'file' })
505
+ if (!existsSync(candidatePath)) {
506
+ atomicCopyFile(sourcePath, candidatePath, targetRoot)
507
+ return `${relativePath}${label}`
508
+ }
509
+ if (hashFile(candidatePath) === hashFile(sourcePath)) return `${relativePath}${label}`
510
+ }
511
+ }
306
512
 
307
- // Slim repo-local OVERRIDE folders (SKILL.md only) for the external auto-* PR
308
- // family + single autofix skill. The workflow bodies live in open-mercato/skills
309
- // (installed via `yarn install-skills`); these overrides adjust them for a
310
- // standalone app and are read on top of the external skill's built-in workflow.
311
- // om-prepare-test-env's override carries environment knowledge (cross-platform
312
- // mercato CLI runner commands) rather than tracker adjustments.
313
- for (const autoSkill of [
314
- 'om-auto-create-pr',
315
- 'om-auto-continue-pr',
316
- 'om-auto-create-pr-loop',
317
- 'om-auto-continue-pr-loop',
318
- 'om-auto-review-pr',
319
- 'om-auto-fix-issue',
320
- 'om-prepare-test-env',
321
- ]) {
322
- if (!existsSync(join(srcDir, 'ai', 'skills', autoSkill, 'SKILL.md'))) {
513
+ export function applyHarnessUpdate(
514
+ targetDir: string,
515
+ stagingDir: string,
516
+ options: { force?: boolean } = {},
517
+ ): HarnessUpdateConflict[] {
518
+ const targetRoot = canonicalHarnessRoot(targetDir)
519
+ const stagingRoot = canonicalHarnessRoot(stagingDir)
520
+ const targetManifestPath = join(targetRoot, '.ai', 'harness', 'manifest.json')
521
+ const stagingManifestPath = join(stagingRoot, '.ai', 'harness', 'manifest.json')
522
+ assertManagedPath(targetRoot, targetManifestPath, { leaf: 'file' })
523
+ assertManagedPath(stagingRoot, stagingManifestPath, { leaf: 'file' })
524
+ const candidateManifest = readHarnessManifest(stagingManifestPath)
525
+ if (!candidateManifest) {
526
+ throw new Error('Generated harness candidate has a missing or invalid ownership manifest.')
527
+ }
528
+
529
+ const previousManifest = readHarnessManifest(targetManifestPath)
530
+ const previousFiles = new Map(previousManifest?.files.map((entry) => [entry.path, entry]) ?? [])
531
+ const candidatePaths = new Set(candidateManifest.files.map((entry) => entry.path))
532
+ const candidates: Array<{
533
+ entry: HarnessManifestFile
534
+ sourcePath: string
535
+ destinationPath: string
536
+ }> = []
537
+
538
+ // Validate the complete candidate before writing anything into the app.
539
+ for (const entry of candidateManifest.files) {
540
+ const sourcePath = resolveManifestPath(stagingRoot, entry.path)
541
+ const destinationPath = resolveManifestPath(targetRoot, entry.path)
542
+ if (sourcePath) assertManagedPath(stagingRoot, sourcePath, { leaf: 'file' })
543
+ if (destinationPath) assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
544
+ if (!sourcePath || !destinationPath || !existsSync(sourcePath) || hashFile(sourcePath) !== entry.sha256) {
545
+ throw new Error(`Generated harness candidate is invalid for ${JSON.stringify(entry.path)}.`)
546
+ }
547
+ candidates.push({ entry, sourcePath, destinationPath })
548
+ }
549
+
550
+ const conflicts: HarnessUpdateConflict[] = []
551
+ for (const { entry, sourcePath, destinationPath } of candidates) {
552
+ if (options.force) {
553
+ atomicCopyFile(sourcePath, destinationPath, targetRoot)
323
554
  continue
324
555
  }
325
- copyFile(
326
- srcDir,
327
- `ai/skills/${autoSkill}/SKILL.md`,
328
- join(targetDir, '.ai', 'skills', autoSkill, 'SKILL.md'),
329
- )
556
+ if (!existsSync(destinationPath)) {
557
+ atomicCopyFile(sourcePath, destinationPath, targetRoot)
558
+ continue
559
+ }
560
+
561
+ const currentHash = hashFile(destinationPath)
562
+ const previousEntry = previousFiles.get(entry.path)
563
+ if (currentHash === entry.sha256 || (previousEntry && currentHash === previousEntry.sha256)) {
564
+ atomicCopyFile(sourcePath, destinationPath, targetRoot)
565
+ continue
566
+ }
567
+
568
+ const candidate = preserveIncomingCandidate(targetRoot, destinationPath, sourcePath, entry.path)
569
+ conflicts.push({ path: entry.path, candidate })
330
570
  }
331
571
 
332
- copyFile(
333
- srcDir,
334
- 'ai/skills/om-trim-unused-modules/SKILL.md',
335
- join(targetDir, '.ai', 'skills', 'om-trim-unused-modules', 'SKILL.md'),
336
- )
572
+ const retainedRetiredEntries: HarnessManifestFile[] = []
573
+ for (const previousEntry of previousManifest?.files ?? []) {
574
+ if (candidatePaths.has(previousEntry.path)) continue
575
+ const destinationPath = resolveManifestPath(targetRoot, previousEntry.path)
576
+ if (!destinationPath || !existsSync(destinationPath)) continue
577
+ assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
578
+ let unchanged = false
579
+ try {
580
+ unchanged = hashFile(destinationPath) === previousEntry.sha256
581
+ } catch {
582
+ // A user may replace a generated file with another filesystem object.
583
+ // Preserve it and retain the ownership tombstone for a later review.
584
+ }
585
+ if (unchanged) {
586
+ assertManagedPath(targetRoot, dirname(destinationPath))
587
+ assertManagedPath(targetRoot, destinationPath, { leaf: 'file' })
588
+ rmSync(destinationPath, { force: true })
589
+ } else {
590
+ retainedRetiredEntries.push(previousEntry)
591
+ conflicts.push({ path: previousEntry.path, candidate: null })
592
+ }
593
+ }
337
594
 
338
- copyFile(srcDir, 'ai/qa/tests/playwright.config.ts', join(targetDir, '.ai', 'qa', 'tests', 'playwright.config.ts'))
595
+ // The candidate hashes deliberately remain in the manifest for conflicts. If
596
+ // the user accepts an .incoming file, a later update can recognize it as an
597
+ // unmodified owned file. Until then the hash mismatch keeps preserving it.
598
+ const finalManifest: HarnessManifest = {
599
+ ...candidateManifest,
600
+ files: [...candidateManifest.files, ...retainedRetiredEntries],
601
+ }
602
+ atomicWriteFile(targetManifestPath, `${JSON.stringify(finalManifest, null, 2)}\n`, targetRoot)
603
+ return conflicts
604
+ }
339
605
 
340
- // Package & conceptual guides are copied wholesale (framework-wide). Per-module
606
+ // ─── Generators ──────────────────────────────────────────────────────────
607
+
608
+ function generateShared(config: AgenticConfig): void {
609
+ const { targetDir } = config
610
+ const srcDir = join(AGENTIC_DIR, 'shared')
611
+
612
+ // Resolve which per-module fact-sheets this app gets (enabled ∩ bundled allowlist).
613
+ const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
614
+
615
+ // One recursive mapping mirrors create-app's shared emitter.
616
+ writeTemplate(srcDir, 'AGENTS.md.template', join(targetDir, 'AGENTS.md'), config)
617
+ copyTree(join(srcDir, 'ai'), join(targetDir, '.ai'), config)
618
+ copyTree(join(srcDir, 'scripts'), join(targetDir, 'scripts'), config)
619
+
620
+ // Routed conceptual guides are copied wholesale (framework-wide). Per-module
341
621
  // fact-sheets (.ai/guides/modules/<module>.md) are filtered to the app's enabled
342
622
  // module set; the combined module-facts.json sidecar is copied as-is.
343
623
  if (existsSync(GUIDES_DIR)) {
@@ -357,6 +637,9 @@ function generateShared(config: AgenticConfig): void {
357
637
  copyFileSync(moduleFactsPath, destPath)
358
638
  }
359
639
 
640
+
641
+ copyTree(join(GUIDES_DIR, 'upstream'), join(guidesDestDir, 'upstream'), config)
642
+
360
643
  const modulesSubdir = join(GUIDES_DIR, 'modules')
361
644
  for (const moduleId of selectedModules) {
362
645
  const destPath = join(guidesDestDir, 'modules', `${moduleId}.md`)
@@ -364,6 +647,8 @@ function generateShared(config: AgenticConfig): void {
364
647
  copyFileSync(join(modulesSubdir, `${moduleId}.md`), destPath)
365
648
  }
366
649
  }
650
+
651
+ injectModuleGuides(join(targetDir, 'AGENTS.md'), selectedModules)
367
652
  }
368
653
 
369
654
  function generateClaudeCode(config: AgenticConfig): void {
@@ -375,8 +660,7 @@ function generateClaudeCode(config: AgenticConfig): void {
375
660
  copyFile(srcDir, 'hooks/entity-migration-check.ts', join(targetDir, '.claude', 'hooks', 'entity-migration-check.ts'))
376
661
  copyFile(srcDir, 'mcp.json.example', join(targetDir, '.mcp.json.example'))
377
662
 
378
- // Symlink .claude/skills → ../.ai/skills
379
- ensureSkillsLink(join(targetDir, '.claude', 'skills'), join('..', '.ai', 'skills'))
663
+ // The installer exclusively owns Claude's per-skill compatibility links.
380
664
  }
381
665
 
382
666
  function generateCodex(config: AgenticConfig): void {
@@ -428,17 +712,6 @@ function generateCursor(config: AgenticConfig): void {
428
712
  // which scripts/install-skills.sh populates.
429
713
  }
430
714
 
431
- function ensureSkillsLink(linkPath: string, target: string): void {
432
- ensureDir(linkPath)
433
- if (existsSync(linkPath) && !lstatSync(linkPath).isSymbolicLink()) {
434
- return
435
- }
436
- if (lstatSync(linkPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
437
- unlinkSync(linkPath)
438
- }
439
- symlinkSync(target, linkPath)
440
- }
441
-
442
715
  // ─── Wizard ──────────────────────────────────────────────────────────────
443
716
 
444
717
  const TOOLS = [
@@ -451,6 +724,35 @@ const TOOLS = [
451
724
 
452
725
  const SELECTABLE = TOOLS.filter((t) => t.id !== 'multiple' && t.id !== 'skip')
453
726
 
727
+ function persistedAgentSelection(targetDir: string): string[] | null {
728
+ const selectableIds = SELECTABLE.map((tool) => tool.id)
729
+ const tiersPath = join(targetDir, '.ai', 'skills', 'tiers.json')
730
+ try {
731
+ const tiers = JSON.parse(readFileSync(tiersPath, 'utf8')) as { agents?: { ignore?: unknown } }
732
+ if (Array.isArray(tiers.agents?.ignore)
733
+ && tiers.agents.ignore.every((id): id is string => typeof id === 'string')) {
734
+ const ignored = new Set(tiers.agents.ignore)
735
+ return selectableIds.filter((id) => !ignored.has(id))
736
+ }
737
+ } catch {
738
+ // Fall through to the ownership manifest and existing generated assets.
739
+ }
740
+
741
+ const manifest = readHarnessManifest(join(targetDir, '.ai', 'harness', 'manifest.json'))
742
+ const owned = new Set(manifest?.files.map((entry) => entry.path) ?? [])
743
+ const selected = SELECTABLE.filter((tool) => {
744
+ if (tool.id === 'claude-code') {
745
+ return owned.has('CLAUDE.md') || owned.has('.claude/settings.json')
746
+ || existsSync(join(targetDir, 'CLAUDE.md')) || existsSync(join(targetDir, '.claude', 'settings.json'))
747
+ }
748
+ if (tool.id === 'codex') {
749
+ return owned.has('.codex/mcp.json.example') || existsSync(join(targetDir, '.codex', 'mcp.json.example'))
750
+ }
751
+ return owned.has('.cursor/hooks.json') || existsSync(join(targetDir, '.cursor', 'hooks.json'))
752
+ }).map((tool) => tool.id)
753
+ return selected.length > 0 ? selected : null
754
+ }
755
+
454
756
  async function promptSelection(ask: AskFn): Promise<string[]> {
455
757
  console.log('')
456
758
  console.log('🤖 Agentic workflow setup')
@@ -497,6 +799,8 @@ export async function runAgenticSetup(
497
799
 
498
800
  if (options?.tool) {
499
801
  selectedIds = options.tool.split(',').map((t) => t.trim())
802
+ } else if (options?.updateHarness) {
803
+ selectedIds = persistedAgentSelection(targetDir) ?? await promptSelection(ask)
500
804
  } else {
501
805
  selectedIds = await promptSelection(ask)
502
806
  }
@@ -513,12 +817,37 @@ export async function runAgenticSetup(
513
817
  targetDir,
514
818
  }
515
819
 
516
- generateShared(config)
517
- if (selectedIds.includes('claude-code')) generateClaudeCode(config)
518
- if (selectedIds.includes('codex')) generateCodex(config)
519
- if (selectedIds.includes('cursor')) generateCursor(config)
820
+ const stagingDir = mkdtempSync(join(tmpdir(), 'open-mercato-harness-'))
821
+ try {
822
+ const modulesSourcePath = join(targetDir, 'src', 'modules.ts')
823
+ const modulesCandidatePath = join(stagingDir, 'src', 'modules.ts')
824
+ if (existsSync(modulesSourcePath)) {
825
+ ensureDir(modulesCandidatePath)
826
+ copyFileSync(modulesSourcePath, modulesCandidatePath)
827
+ }
828
+
829
+ const stagingConfig: AgenticConfig = {
830
+ projectName: config.projectName,
831
+ targetDir: stagingDir,
832
+ }
833
+ generateHarness(stagingConfig, selectedIds)
834
+ const conflicts = applyHarnessUpdate(targetDir, stagingDir, {
835
+ force: options?.force,
836
+ })
837
+ if (conflicts.length > 0) {
838
+ console.warn('')
839
+ console.warn(' ⚠ Preserved locally modified harness files:')
840
+ for (const conflict of conflicts) {
841
+ const detail = conflict.candidate
842
+ ? `candidate: ${conflict.candidate}`
843
+ : 'retired asset kept because it has local changes'
844
+ console.warn(` • ${conflict.path} (${detail})`)
845
+ }
846
+ }
847
+ } finally {
848
+ rmSync(stagingDir, { recursive: true, force: true })
849
+ }
520
850
 
521
- persistAgentSelection(targetDir, selectedIds)
522
851
  installSkills(targetDir)
523
852
 
524
853
  console.log('')
@@ -540,6 +869,28 @@ export async function runAgenticSetup(
540
869
  console.log('')
541
870
  }
542
871
 
872
+ function generateHarness(config: AgenticConfig, selectedIds: string[]): void {
873
+ generateShared(config)
874
+ if (selectedIds.includes('claude-code')) generateClaudeCode(config)
875
+ if (selectedIds.includes('codex')) generateCodex(config)
876
+ if (selectedIds.includes('cursor')) generateCursor(config)
877
+
878
+ enforceGeneratedRootBudget(config)
879
+
880
+ persistAgentSelection(config.targetDir, selectedIds)
881
+ finalizeHarnessManifest(config, selectedIds)
882
+ }
883
+
884
+ /** Run after every tool generator has patched AGENTS.md, before the manifest is hashed. */
885
+ function enforceGeneratedRootBudget(
886
+ config: AgenticConfig,
887
+ maxBytes: number = STANDALONE_ROOT_TARGET_BYTES,
888
+ ): boolean {
889
+ const { targetDir } = config
890
+ const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
891
+ return enforceRootInstructionBudget(join(targetDir, 'AGENTS.md'), selectedModules, maxBytes)
892
+ }
893
+
543
894
  /**
544
895
  * Persist the agent selection so later `yarn install-skills` runs keep honoring
545
896
  * it: agents the user did not pick go into `agents.ignore` in tiers.json and
@@ -559,11 +910,11 @@ function persistAgentSelection(targetDir: string, selectedIds: string[]): void {
559
910
  }
560
911
 
561
912
  function installSkills(targetDir: string): void {
562
- const installScript = join(targetDir, 'scripts', 'install-skills.sh')
913
+ const installScript = join(targetDir, 'scripts', 'install-skills.mjs')
563
914
  if (!existsSync(installScript)) return
564
915
  console.log('')
565
916
  console.log(' Installing agent skills (local tiers + external open-mercato/skills subset)...')
566
- const result = spawnSync('sh', [installScript], { cwd: targetDir, stdio: 'inherit' })
917
+ const result = spawnSync(process.execPath, [installScript], { cwd: targetDir, stdio: 'inherit' })
567
918
  if (result.error || result.status !== 0) {
568
919
  console.log(' ⚠ Skill installation did not complete; run `yarn install-skills` inside the app when online.')
569
920
  }