@open-mercato/cli 0.7.1-develop.7191.1.3c9346566b → 0.7.1-develop.7194.1.ab4fc81f82

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 (169) hide show
  1. package/.turbo/turbo-build.log +6 -2
  2. package/dist/agentic/github-copilot/copilot-instructions.md.template +37 -0
  3. package/dist/agentic/github-copilot/instructions/entity-guard.instructions.md +27 -0
  4. package/dist/agentic/github-copilot/instructions/generated-guard.instructions.md +10 -0
  5. package/dist/agentic/github-copilot/mcp.json.example +13 -0
  6. package/dist/agentic/guides/module-facts.json +11527 -283
  7. package/dist/agentic/guides/module-facts.v2.json +11527 -283
  8. package/dist/agentic/guides/modules/agent_orchestrator/acl-features.md +31 -0
  9. package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +12 -0
  10. package/dist/agentic/guides/modules/agent_orchestrator/ai-agents.md +12 -0
  11. package/dist/agentic/guides/modules/agent_orchestrator/ai-tools-mcp-capabilities.md +13 -0
  12. package/dist/agentic/guides/modules/agent_orchestrator/backend-pages.md +28 -0
  13. package/dist/agentic/guides/modules/agent_orchestrator/cli-commands.md +14 -0
  14. package/dist/agentic/guides/modules/agent_orchestrator/contribution-resolutions.md +49 -0
  15. package/dist/agentic/guides/modules/agent_orchestrator/di-registrations-rich.md +42 -0
  16. package/dist/agentic/guides/modules/agent_orchestrator/di-service-tokens.md +24 -0
  17. package/dist/agentic/guides/modules/agent_orchestrator/domain-commands.md +28 -0
  18. package/dist/agentic/guides/modules/agent_orchestrator/encryption.md +20 -0
  19. package/dist/agentic/guides/modules/agent_orchestrator/entities.md +31 -0
  20. package/dist/agentic/guides/modules/agent_orchestrator/events.md +38 -0
  21. package/dist/agentic/guides/modules/agent_orchestrator/exact-override-targets.md +122 -0
  22. package/dist/agentic/guides/modules/agent_orchestrator/host-extension-points.md +10 -0
  23. package/dist/agentic/guides/modules/agent_orchestrator/index.md +31 -0
  24. package/dist/agentic/guides/modules/agent_orchestrator/owned-contract-module-metadata.md +11 -0
  25. package/dist/agentic/guides/modules/agent_orchestrator/setup.md +11 -0
  26. package/dist/agentic/guides/modules/agent_orchestrator/umes-contributions.md +49 -0
  27. package/dist/agentic/guides/modules/agent_orchestrator/umes-hosts.md +61 -0
  28. package/dist/agentic/guides/modules/agent_orchestrator/workers.md +14 -0
  29. package/dist/agentic/guides/modules/ai_assistant/di-registrations-rich.md +3 -2
  30. package/dist/agentic/guides/modules/ai_assistant/di-service-tokens.md +2 -1
  31. package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -2
  32. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  33. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  34. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  35. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  36. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  37. package/dist/agentic/guides/modules/auth/index.md +1 -1
  38. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  39. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  40. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  41. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  42. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  43. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  44. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  45. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  46. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  47. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  48. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  49. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  50. package/dist/agentic/guides/modules/configs/index.md +1 -1
  51. package/dist/agentic/guides/modules/content/index.md +1 -1
  52. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  53. package/dist/agentic/guides/modules/customer_accounts/cli-commands.md +11 -0
  54. package/dist/agentic/guides/modules/customer_accounts/exact-override-targets.md +5 -4
  55. package/dist/agentic/guides/modules/customer_accounts/index.md +3 -2
  56. package/dist/agentic/guides/modules/customer_accounts/setup.md +1 -1
  57. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
  58. package/dist/agentic/guides/modules/customers/exact-override-targets.md +1 -0
  59. package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +3 -0
  60. package/dist/agentic/guides/modules/customers/index.md +2 -2
  61. package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
  62. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  63. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  64. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  65. package/dist/agentic/guides/modules/devices/index.md +1 -1
  66. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  67. package/dist/agentic/guides/modules/directory/index.md +1 -1
  68. package/dist/agentic/guides/modules/documents/index.md +1 -1
  69. package/dist/agentic/guides/modules/entities/index.md +1 -1
  70. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  71. package/dist/agentic/guides/modules/events/index.md +1 -1
  72. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  73. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  74. package/dist/agentic/guides/modules/generators/index.md +1 -1
  75. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  76. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  77. package/dist/agentic/guides/modules/messages/index.md +1 -1
  78. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  79. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  80. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  81. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  82. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  83. package/dist/agentic/guides/modules/planner/index.md +1 -1
  84. package/dist/agentic/guides/modules/portal/index.md +1 -1
  85. package/dist/agentic/guides/modules/progress/index.md +1 -1
  86. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  87. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  88. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  89. package/dist/agentic/guides/modules/resources/index.md +1 -1
  90. package/dist/agentic/guides/modules/sales/index.md +1 -1
  91. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  92. package/dist/agentic/guides/modules/search/index.md +1 -1
  93. package/dist/agentic/guides/modules/security/index.md +1 -1
  94. package/dist/agentic/guides/modules/seeds/cli-commands.md +14 -0
  95. package/dist/agentic/guides/modules/seeds/exact-override-targets.md +14 -0
  96. package/dist/agentic/guides/modules/seeds/index.md +14 -0
  97. package/dist/agentic/guides/modules/seeds/owned-contract-module-metadata.md +11 -0
  98. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  99. package/dist/agentic/guides/modules/sso/index.md +1 -1
  100. package/dist/agentic/guides/modules/staff/index.md +1 -1
  101. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  102. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  103. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  104. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  105. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  106. package/dist/agentic/guides/modules/translations/index.md +1 -1
  107. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  108. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  109. package/dist/agentic/guides/modules/wms/index.md +1 -1
  110. package/dist/agentic/guides/modules/workflows/acl-features.md +11 -1
  111. package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +4 -1
  112. package/dist/agentic/guides/modules/workflows/ai-tools-mcp-capabilities.md +17 -0
  113. package/dist/agentic/guides/modules/workflows/backend-pages.md +3 -0
  114. package/dist/agentic/guides/modules/workflows/contribution-resolutions.md +24 -0
  115. package/dist/agentic/guides/modules/workflows/di-registrations-rich.md +11 -7
  116. package/dist/agentic/guides/modules/workflows/di-service-tokens.md +11 -7
  117. package/dist/agentic/guides/modules/workflows/domain-commands.md +11 -0
  118. package/dist/agentic/guides/modules/workflows/entities.md +2 -0
  119. package/dist/agentic/guides/modules/workflows/events.md +14 -8
  120. package/dist/agentic/guides/modules/workflows/exact-override-targets.md +45 -10
  121. package/dist/agentic/guides/modules/workflows/frontend-pages.md +2 -0
  122. package/dist/agentic/guides/modules/workflows/incoming-installed-contributions.md +17 -0
  123. package/dist/agentic/guides/modules/workflows/index.md +15 -12
  124. package/dist/agentic/guides/modules/workflows/notifications.md +2 -0
  125. package/dist/agentic/guides/modules/workflows/setup.md +1 -1
  126. package/dist/agentic/guides/modules/workflows/umes-contributions.md +24 -0
  127. package/dist/agentic/guides/modules/workflows/umes-hosts.md +14 -6
  128. package/dist/agentic/guides/modules/workflows/workers.md +4 -1
  129. package/dist/agentic/guides/reference-module-facts.json +2 -2
  130. package/dist/agentic/guides/reference-modules/example/index.md +1 -1
  131. package/dist/agentic/guides/upstream/AGENTS.md +3 -1
  132. package/dist/agentic/guides/upstream/manifest.json +2 -2
  133. package/dist/agentic/shared/ai/harness/design-system-inventory.json +118 -106
  134. package/dist/agentic/shared/ai/harness/source-link-inventory.json +8 -8
  135. package/dist/lib/agentic-init.js +4 -0
  136. package/dist/lib/agentic-init.js.map +2 -2
  137. package/dist/lib/agentic-setup.js +19 -5
  138. package/dist/lib/agentic-setup.js.map +2 -2
  139. package/dist/lib/generators/extensions/agent-files.js +913 -0
  140. package/dist/lib/generators/extensions/agent-files.js.map +7 -0
  141. package/dist/lib/generators/extensions/index.js +2 -0
  142. package/dist/lib/generators/extensions/index.js.map +2 -2
  143. package/dist/lib/generators/index.js +3 -1
  144. package/dist/lib/generators/index.js.map +2 -2
  145. package/dist/lib/generators/module-registry.js +40 -11
  146. package/dist/lib/generators/module-registry.js.map +2 -2
  147. package/dist/lib/generators/route-collisions.js +35 -0
  148. package/dist/lib/generators/route-collisions.js.map +7 -0
  149. package/dist/lib/generators/web-research-adapters.js +95 -0
  150. package/dist/lib/generators/web-research-adapters.js.map +7 -0
  151. package/dist/mercato.js +5 -2
  152. package/dist/mercato.js.map +2 -2
  153. package/package.json +6 -6
  154. package/src/__tests__/agent-files.test.ts +199 -0
  155. package/src/__tests__/mercato.test.ts +14 -0
  156. package/src/lib/__tests__/agentic-init.test.ts +11 -0
  157. package/src/lib/__tests__/agentic-setup.ownership.test.ts +5 -0
  158. package/src/lib/agentic-init.ts +4 -0
  159. package/src/lib/agentic-setup.ts +30 -5
  160. package/src/lib/generators/__tests__/agent-files-extension.test.ts +477 -0
  161. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +25 -8
  162. package/src/lib/generators/__tests__/route-collisions.test.ts +155 -0
  163. package/src/lib/generators/extensions/agent-files.ts +1332 -0
  164. package/src/lib/generators/extensions/index.ts +2 -0
  165. package/src/lib/generators/index.ts +1 -0
  166. package/src/lib/generators/module-registry.ts +66 -17
  167. package/src/lib/generators/route-collisions.ts +57 -0
  168. package/src/lib/generators/web-research-adapters.ts +133 -0
  169. package/src/mercato.ts +4 -1
@@ -0,0 +1,477 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import type { ModuleScanContext } from '../extension'
5
+ import { createAgentFilesExtension } from '../extensions/agent-files'
6
+
7
+ /**
8
+ * Build a fake repo root with `docker/opencode` + `packages` markers (so the
9
+ * extension's `findRepoRoot` resolves it) and a module whose `agents/<id>/`
10
+ * fixture lives under `appBase`.
11
+ */
12
+ function makeRepoFixture(): { repoRoot: string; appBase: string; pkgBase: string } {
13
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-files-repo-'))
14
+ fs.mkdirSync(path.join(repoRoot, 'docker', 'opencode'), { recursive: true })
15
+ fs.mkdirSync(path.join(repoRoot, 'packages'), { recursive: true })
16
+
17
+ const appBase = path.join(repoRoot, 'apps', 'mercato', 'src', 'modules', 'agent_examples')
18
+ const pkgBase = path.join(repoRoot, 'packages', 'core', 'src', 'modules', 'agent_examples')
19
+ const agentDir = path.join(appBase, 'agents', 'deals_health_check')
20
+ fs.mkdirSync(agentDir, { recursive: true })
21
+
22
+ fs.writeFileSync(
23
+ path.join(agentDir, 'AGENT.md'),
24
+ [
25
+ '---',
26
+ 'id: deals.health_check',
27
+ 'label: Deal health check',
28
+ 'description: Assess a deal.',
29
+ 'maxSteps: 12',
30
+ '---',
31
+ 'You assess a deal.',
32
+ ].join('\n'),
33
+ 'utf8',
34
+ )
35
+ fs.writeFileSync(
36
+ path.join(agentDir, 'OUTCOME.md'),
37
+ [
38
+ '---',
39
+ 'kind: proposal',
40
+ '---',
41
+ '```json',
42
+ JSON.stringify({
43
+ type: 'object',
44
+ required: ['confidence'],
45
+ properties: { confidence: { type: 'number', minimum: 0, maximum: 1 } },
46
+ }),
47
+ '```',
48
+ ].join('\n'),
49
+ 'utf8',
50
+ )
51
+
52
+ return { repoRoot, appBase, pkgBase }
53
+ }
54
+
55
+ function createScanContext(moduleId: string, appBase: string, pkgBase: string): ModuleScanContext {
56
+ return {
57
+ moduleId,
58
+ roots: { appBase, pkgBase },
59
+ imps: { appBase: `@/modules/${moduleId}`, pkgBase: `@open-mercato/core/modules/${moduleId}` },
60
+ importIdRef: { value: 0 },
61
+ sharedImports: [],
62
+ resolveModuleFile: (() => null) as ModuleScanContext['resolveModuleFile'],
63
+ resolveFirstModuleFile: (() => null) as ModuleScanContext['resolveFirstModuleFile'],
64
+ processStandaloneConfig: () => null,
65
+ sanitizeGeneratedModuleSpecifier: (importPath) => importPath,
66
+ }
67
+ }
68
+
69
+ describe('createAgentFilesExtension', () => {
70
+ let repoRoot = ''
71
+ afterEach(() => {
72
+ if (repoRoot) fs.rmSync(repoRoot, { recursive: true, force: true })
73
+ repoRoot = ''
74
+ })
75
+
76
+ it('discovers agents/<id>/ and emits the manifest + docker agent file', () => {
77
+ const fixture = makeRepoFixture()
78
+ repoRoot = fixture.repoRoot
79
+
80
+ const extension = createAgentFilesExtension()
81
+ expect(extension.id).toBe('registry.agent-files')
82
+
83
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
84
+ const output = extension.generateOutput()
85
+ expect(output.size).toBe(0)
86
+
87
+ const manifestPath = path.join(
88
+ repoRoot,
89
+ 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
90
+ )
91
+ expect(fs.existsSync(manifestPath)).toBe(true)
92
+ const manifest = fs.readFileSync(manifestPath, 'utf8')
93
+ expect(manifest).toContain('AUTO-GENERATED by mercato generate registry')
94
+ expect(manifest).toContain("id: \"deals.health_check\"")
95
+ expect(manifest).toContain("openCodeAgentName: \"deals_health_check\"")
96
+ expect(manifest).toContain('export const fileAgentDescriptors')
97
+
98
+ const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
99
+ expect(fs.existsSync(dockerFile)).toBe(true)
100
+ const dockerContent = fs.readFileSync(dockerFile, 'utf8')
101
+ expect(dockerContent).toContain('mode: primary')
102
+ expect(dockerContent).toContain('open-mercato_agent_orchestrator_submit_outcome')
103
+ expect(dockerContent).toContain('write: deny')
104
+ // No sub-agents declared → `task` denied and no delegate tool (unchanged).
105
+ expect(dockerContent).toContain('task: deny')
106
+ expect(dockerContent).not.toContain('delegate_agent')
107
+ })
108
+
109
+ it('routes file-agent delegation through delegate_agent instead of the task tool', () => {
110
+ const fixture = makeRepoFixture()
111
+ repoRoot = fixture.repoRoot
112
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
113
+
114
+ // Declare a sub-agent under sub-agents/<subid>/ (researcher + non-delegating).
115
+ fs.writeFileSync(
116
+ path.join(agentDir, 'AGENT.md'),
117
+ [
118
+ '---',
119
+ 'id: deals.health_check',
120
+ 'label: Deal health check',
121
+ 'description: Assess a deal.',
122
+ '---',
123
+ 'You assess a deal.',
124
+ ].join('\n'),
125
+ 'utf8',
126
+ )
127
+ const subDir = path.join(agentDir, 'sub-agents', 'company_researcher')
128
+ fs.mkdirSync(subDir, { recursive: true })
129
+ fs.writeFileSync(
130
+ path.join(subDir, 'AGENT.md'),
131
+ [
132
+ '---',
133
+ 'id: deals.company_researcher',
134
+ 'label: Company researcher',
135
+ 'description: Research a company.',
136
+ '---',
137
+ 'You research a company.',
138
+ ].join('\n'),
139
+ 'utf8',
140
+ )
141
+ fs.writeFileSync(
142
+ path.join(subDir, 'OUTCOME.md'),
143
+ [
144
+ '---',
145
+ 'kind: research',
146
+ '---',
147
+ '```json',
148
+ JSON.stringify({ type: 'object', properties: { summary: { type: 'string' } } }),
149
+ '```',
150
+ ].join('\n'),
151
+ 'utf8',
152
+ )
153
+
154
+ const extension = createAgentFilesExtension()
155
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
156
+ extension.generateOutput()
157
+
158
+ const primary = fs.readFileSync(
159
+ path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md'),
160
+ 'utf8',
161
+ )
162
+ // The orchestrator gains the delegate_agent MCP tool, denies `task`, and
163
+ // never grants the built-in task tool.
164
+ expect(primary).toContain('open-mercato_agent_orchestrator_delegate_agent')
165
+ expect(primary).toContain('task: deny')
166
+ expect(primary).not.toContain('"task": true')
167
+ // The prompt delegates through delegate_agent (not `task`) using the FULL
168
+ // sub-agent id.
169
+ expect(primary).toContain('## Sub-agents')
170
+ expect(primary).toContain('delegate_agent')
171
+ expect(primary).toContain('deals.company_researcher')
172
+ expect(primary).not.toContain('by calling the `task` tool')
173
+
174
+ // The sub-agent file gets neither the delegate tool nor `task` (depth cap = 1).
175
+ const sub = fs.readFileSync(
176
+ path.join(repoRoot, 'docker/opencode/agents/deals_company_researcher.md'),
177
+ 'utf8',
178
+ )
179
+ expect(sub).toContain('mode: subagent')
180
+ expect(sub).toContain('task: deny')
181
+ expect(sub).not.toContain('delegate_agent')
182
+ })
183
+
184
+ it('emits an agent SAMPLE.json into the manifest as sampleInput', () => {
185
+ const fixture = makeRepoFixture()
186
+ repoRoot = fixture.repoRoot
187
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
188
+ fs.writeFileSync(
189
+ path.join(agentDir, 'SAMPLE.json'),
190
+ JSON.stringify({ deal: { id: 'demo-deal-1', stage: 'Proposal' } }),
191
+ 'utf8',
192
+ )
193
+
194
+ const extension = createAgentFilesExtension()
195
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
196
+ extension.generateOutput()
197
+
198
+ const manifest = fs.readFileSync(
199
+ path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
200
+ 'utf8',
201
+ )
202
+ expect(manifest).toContain('sampleInput: {"deal":{"id":"demo-deal-1","stage":"Proposal"}}')
203
+ })
204
+
205
+ it('fails generation on a malformed SAMPLE.json', () => {
206
+ const fixture = makeRepoFixture()
207
+ repoRoot = fixture.repoRoot
208
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
209
+ fs.writeFileSync(path.join(agentDir, 'SAMPLE.json'), '{ not valid json', 'utf8')
210
+
211
+ const extension = createAgentFilesExtension()
212
+ expect(() =>
213
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
214
+ ).toThrow(/malformed SAMPLE\.json/)
215
+ })
216
+
217
+ it('emits an agent FACTS.json into the manifest as facts', () => {
218
+ const fixture = makeRepoFixture()
219
+ repoRoot = fixture.repoRoot
220
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
221
+ fs.writeFileSync(
222
+ path.join(agentDir, 'FACTS.json'),
223
+ JSON.stringify({
224
+ facts: [
225
+ { label: 'Stage', source: 'input', path: 'deal.stage' },
226
+ { label: 'Confidence', source: 'payload', path: 'confidence', format: 'percent' },
227
+ ],
228
+ }),
229
+ 'utf8',
230
+ )
231
+
232
+ const extension = createAgentFilesExtension()
233
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
234
+ extension.generateOutput()
235
+
236
+ const manifest = fs.readFileSync(
237
+ path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
238
+ 'utf8',
239
+ )
240
+ expect(manifest).toContain(
241
+ 'facts: [{"label":"Stage","source":"input","path":"deal.stage"},{"label":"Confidence","source":"payload","path":"confidence","format":"percent"}]',
242
+ )
243
+ expect(manifest).toContain('export type FileAgentFact')
244
+ })
245
+
246
+ it('fails generation on a FACTS.json entry missing label/source/path', () => {
247
+ const fixture = makeRepoFixture()
248
+ repoRoot = fixture.repoRoot
249
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
250
+ fs.writeFileSync(
251
+ path.join(agentDir, 'FACTS.json'),
252
+ JSON.stringify({ facts: [{ label: 'Stage', source: 'nowhere', path: 'deal.stage' }] }),
253
+ 'utf8',
254
+ )
255
+
256
+ const extension = createAgentFilesExtension()
257
+ expect(() =>
258
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
259
+ ).toThrow(/malformed FACTS\.json/)
260
+ })
261
+
262
+ it('fails generation on a malformed agent dir (missing OUTCOME.md)', () => {
263
+ const fixture = makeRepoFixture()
264
+ repoRoot = fixture.repoRoot
265
+ // Remove OUTCOME.md to make the dir malformed.
266
+ fs.rmSync(path.join(fixture.appBase, 'agents', 'deals_health_check', 'OUTCOME.md'))
267
+
268
+ const extension = createAgentFilesExtension()
269
+ expect(() =>
270
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
271
+ ).toThrow(/malformed file agent/)
272
+ })
273
+
274
+ it('emits native SKILL.md, unions skill tools, and writes skillsContent to the manifest', () => {
275
+ const fixture = makeRepoFixture()
276
+ repoRoot = fixture.repoRoot
277
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
278
+
279
+ // Reference a skill in AGENT.md and author it under skills/<skill_id>/.
280
+ fs.writeFileSync(
281
+ path.join(agentDir, 'AGENT.md'),
282
+ [
283
+ '---',
284
+ 'id: deals.health_check',
285
+ 'label: Deal health check',
286
+ 'description: Assess a deal.',
287
+ 'skills: [stage_playbook]',
288
+ 'tools: [customers.get_deal]',
289
+ '---',
290
+ 'You assess a deal.',
291
+ ].join('\n'),
292
+ 'utf8',
293
+ )
294
+ const skillDir = path.join(agentDir, 'skills', 'stage_playbook')
295
+ fs.mkdirSync(path.join(skillDir, 'examples'), { recursive: true })
296
+ fs.writeFileSync(
297
+ path.join(skillDir, 'SKILL.md'),
298
+ [
299
+ '---',
300
+ 'id: stage_playbook',
301
+ 'label: Stage playbook',
302
+ 'description: Pick the next stage.',
303
+ 'tools: [customers.analyze_deals]',
304
+ '---',
305
+ 'STAGE_PLAYBOOK_BODY',
306
+ ].join('\n'),
307
+ 'utf8',
308
+ )
309
+ fs.writeFileSync(path.join(skillDir, 'TEMPLATE.md'), 'TEMPLATE_BODY', 'utf8')
310
+ fs.writeFileSync(path.join(skillDir, 'examples', 'a.md'), 'EXAMPLE_ONE', 'utf8')
311
+
312
+ const extension = createAgentFilesExtension()
313
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
314
+ extension.generateOutput()
315
+
316
+ // Native OpenCode skill file (sanitized dir name: stage_playbook → stage-playbook).
317
+ const nativeSkill = path.join(repoRoot, 'docker/opencode/skills/stage-playbook/SKILL.md')
318
+ expect(fs.existsSync(nativeSkill)).toBe(true)
319
+ const nativeContent = fs.readFileSync(nativeSkill, 'utf8')
320
+ expect(nativeContent).toContain('name: stage-playbook')
321
+ expect(nativeContent).toContain('STAGE_PLAYBOOK_BODY')
322
+
323
+ // Manifest carries skillsContent and the unioned tool allowlist.
324
+ const manifest = fs.readFileSync(
325
+ path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
326
+ 'utf8',
327
+ )
328
+ expect(manifest).toContain('skillsContent')
329
+ expect(manifest).toContain('STAGE_PLAYBOOK_BODY')
330
+ expect(manifest).toContain('TEMPLATE_BODY')
331
+ expect(manifest).toContain('EXAMPLE_ONE')
332
+ expect(manifest).toContain('customers.analyze_deals')
333
+
334
+ // The docker agent file's tools block includes the skill tool too.
335
+ const dockerAgent = fs.readFileSync(
336
+ path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md'),
337
+ 'utf8',
338
+ )
339
+ // Declared OM tool ids are mapped to OpenCode's MCP tool id form in the allowlist.
340
+ expect(dockerAgent).toContain('open-mercato_customers_analyze_deals')
341
+ })
342
+
343
+ it('fails generation on a malformed SKILL.md (no frontmatter)', () => {
344
+ const fixture = makeRepoFixture()
345
+ repoRoot = fixture.repoRoot
346
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
347
+ fs.writeFileSync(
348
+ path.join(agentDir, 'AGENT.md'),
349
+ [
350
+ '---',
351
+ 'id: deals.health_check',
352
+ 'label: Deal health check',
353
+ 'description: Assess a deal.',
354
+ 'skills: [stage_playbook]',
355
+ '---',
356
+ 'You assess a deal.',
357
+ ].join('\n'),
358
+ 'utf8',
359
+ )
360
+ const skillDir = path.join(agentDir, 'skills', 'stage_playbook')
361
+ fs.mkdirSync(skillDir, { recursive: true })
362
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'no frontmatter at all', 'utf8')
363
+
364
+ const extension = createAgentFilesExtension()
365
+ expect(() =>
366
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
367
+ ).toThrow(/malformed SKILL\.md/)
368
+ })
369
+
370
+ it('fails generation when an OUTCOME.md schema is outside the supported subset (M2)', () => {
371
+ const fixture = makeRepoFixture()
372
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
373
+ // `oneOf` parses as valid JSON but cannot compile to Zod — must fail at scan,
374
+ // not silently parse and get dropped at load.
375
+ fs.writeFileSync(
376
+ path.join(agentDir, 'OUTCOME.md'),
377
+ [
378
+ '---',
379
+ 'kind: proposal',
380
+ '---',
381
+ '```json',
382
+ JSON.stringify({ type: 'object', properties: { x: { oneOf: [{ type: 'string' }] } } }),
383
+ '```',
384
+ ].join('\n'),
385
+ 'utf8',
386
+ )
387
+ const extension = createAgentFilesExtension()
388
+ expect(() =>
389
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
390
+ ).toThrow(/unsupported keyword "oneOf"/)
391
+ })
392
+
393
+ it('fails generation when an OUTCOME.md schema node has no supported type (M2)', () => {
394
+ const fixture = makeRepoFixture()
395
+ const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
396
+ fs.writeFileSync(
397
+ path.join(agentDir, 'OUTCOME.md'),
398
+ [
399
+ '---',
400
+ 'kind: research',
401
+ '---',
402
+ '```json',
403
+ JSON.stringify({ type: 'object', properties: { x: { minimum: 0 } } }),
404
+ '```',
405
+ ].join('\n'),
406
+ 'utf8',
407
+ )
408
+ const extension = createAgentFilesExtension()
409
+ expect(() =>
410
+ extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
411
+ ).toThrow(/missing\/unsupported "type"/)
412
+ })
413
+
414
+ it('keeps committed agent artifacts when neither the orchestrator nor an agent module is enabled', () => {
415
+ const fixture = makeRepoFixture()
416
+ repoRoot = fixture.repoRoot
417
+ const manifestPath = path.join(
418
+ repoRoot,
419
+ 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
420
+ )
421
+ const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
422
+ const skillFile = path.join(repoRoot, 'docker/opencode/skills/stage-playbook/SKILL.md')
423
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
424
+ fs.mkdirSync(path.dirname(dockerFile), { recursive: true })
425
+ fs.mkdirSync(path.dirname(skillFile), { recursive: true })
426
+ fs.writeFileSync(manifestPath, 'committed manifest', 'utf8')
427
+ fs.writeFileSync(dockerFile, 'committed agent file', 'utf8')
428
+ fs.writeFileSync(skillFile, 'committed skill file', 'utf8')
429
+
430
+ const extension = createAgentFilesExtension()
431
+ // A module with no `agents/` tree, standing in for a run with the
432
+ // enterprise agents flag off.
433
+ const otherBase = path.join(repoRoot, 'packages', 'core', 'src', 'modules', 'customers')
434
+ fs.mkdirSync(otherBase, { recursive: true })
435
+ extension.scanModule(createScanContext('customers', otherBase, otherBase))
436
+ extension.generateOutput()
437
+
438
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe('committed manifest')
439
+ expect(fs.readFileSync(dockerFile, 'utf8')).toBe('committed agent file')
440
+ expect(fs.readFileSync(skillFile, 'utf8')).toBe('committed skill file')
441
+ })
442
+
443
+ it('still prunes stale artifacts when the orchestrator is enabled without any agent', () => {
444
+ const fixture = makeRepoFixture()
445
+ repoRoot = fixture.repoRoot
446
+ const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
447
+ const skillDir = path.join(repoRoot, 'docker/opencode/skills/stage-playbook')
448
+ fs.mkdirSync(path.dirname(dockerFile), { recursive: true })
449
+ fs.mkdirSync(skillDir, { recursive: true })
450
+ fs.writeFileSync(dockerFile, 'stale agent file', 'utf8')
451
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'stale skill file', 'utf8')
452
+
453
+ const extension = createAgentFilesExtension()
454
+ const orchestratorBase = path.join(
455
+ repoRoot,
456
+ 'packages',
457
+ 'enterprise',
458
+ 'src',
459
+ 'modules',
460
+ 'agent_orchestrator',
461
+ )
462
+ fs.mkdirSync(orchestratorBase, { recursive: true })
463
+ extension.scanModule(createScanContext('agent_orchestrator', orchestratorBase, orchestratorBase))
464
+ extension.generateOutput()
465
+
466
+ expect(fs.existsSync(dockerFile)).toBe(false)
467
+ expect(fs.existsSync(skillDir)).toBe(false)
468
+ const manifest = fs.readFileSync(
469
+ path.join(
470
+ repoRoot,
471
+ 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
472
+ ),
473
+ 'utf8',
474
+ )
475
+ expect(manifest).toContain('export const fileAgentDescriptors: FileAgentDescriptor[] = []')
476
+ })
477
+ })
@@ -182,20 +182,37 @@ describe('module-facts BC resolve guard (T2)', () => {
182
182
  // facts, plus query-lifecycle, portal-page and strategy-registry hosts that
183
183
  // exist only in the v2 surface. Same linear, one-module growth the block
184
184
  // above describes -- the caps below are re-measured on the merged tree.
185
- expect(Buffer.byteLength(completeJson)).toBeLessThan(4_700_000)
185
+ //
186
+ // Re-measured once more on the agent_orchestrator union (#5718). The caps
187
+ // above were measured on `develop`'s tree, and `develop`'s tree does not
188
+ // contain `agent_orchestrator`, the expanded `workflows` surface or the
189
+ // `web-research*` packages — so three of the four sat below the union, which
190
+ // is the "neither side's cap covers the union" case the note above names.
191
+ //
192
+ // The growth is linear and it is checkable: all four measures grew by very
193
+ // nearly the same proportion (+17.1% / +17.6% / +16.8% / +16.2%), which is
194
+ // what a union of two additive module sets produces. A real blow-up is
195
+ // multiplicative and would move one measure far out of step with the others,
196
+ // so uniform growth across all four is the evidence that this is repo size,
197
+ // not a duplicated payload. The headroom ratio is kept at the ~1.15 the
198
+ // previous raise used, so the guard keeps the same detection power it had.
199
+ expect(Buffer.byteLength(completeJson)).toBeLessThan(5_550_000)
186
200
  // Measured on the merged tree (`yarn jest module-facts.bc-guard`):
187
- // completeJson 4,120,374 · legacy delta 1,795,214 · markdown 1,558,590 ·
188
- // directory markdown 1,709,511.
189
- // The delta cap is raised alongside the JSON one for the same reason: at
190
- // 1,800,000 it sat 0.27% above the measurement, which is not a guard.
191
- expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(2_050_000)
201
+ // completeJson 4,823,226 · legacy delta 2,111,784 · markdown 1,820,787 ·
202
+ // directory markdown 1,985,779.
203
+ // (Previous, on develop's tree: 4,120,374 · 1,795,214 · 1,558,590 · 1,709,511.)
204
+ expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(2_430_000)
192
205
  // Markdown cap raised with the source-link contract: entities, events, ACL
193
206
  // features, DI tokens, search entities, notifications, UMES hosts and UMES
194
207
  // contributions all render a resolved Source cell, and contribution
195
208
  // resolutions render as their own source-linked section — plus the additive
196
209
  // `channel_discord` module's own render, per the growth note above.
197
- expect(markdownBytes).toBeLessThan(1_800_000)
198
- expect(directoryMarkdownBytes).toBeLessThan(2_050_000)
210
+ expect(markdownBytes).toBeLessThan(2_100_000)
211
+ // Raised even though it was still passing: at 2,050,000 it sat 3.2% above
212
+ // the union measurement, and a cap that close has stopped detecting blow-ups
213
+ // and started rejecting the next additive module — the failure mode the note
214
+ // above warns about, one merge away.
215
+ expect(directoryMarkdownBytes).toBeLessThan(2_290_000)
199
216
  })
200
217
 
201
218
  it('keeps every shipped directory section resumable and every advertised subsection anchor exact', () => {
@@ -0,0 +1,155 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import type { PackageResolver, ModuleEntry } from '../../resolver'
5
+ import { generateModuleRegistry } from '../module-registry'
6
+ import { detectBackendRouteCollisions } from '../route-collisions'
7
+
8
+ let tmpDir: string
9
+
10
+ const PAGE_CONTENT = `export default function Page() { return null }
11
+ `
12
+
13
+ function createTmpDir(): string {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'route-collisions-test-'))
15
+ }
16
+
17
+ function touchFile(filePath: string, content = ''): void {
18
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
19
+ fs.writeFileSync(filePath, content)
20
+ }
21
+
22
+ function scaffoldBackendPage(tmpDir: string, modId: string, location: 'pkg' | 'app', pageSubPath: string): void {
23
+ const base =
24
+ location === 'pkg'
25
+ ? path.join(tmpDir, 'packages', 'core', 'src', 'modules', modId)
26
+ : path.join(tmpDir, 'app', 'src', 'modules', modId)
27
+ touchFile(path.join(base, 'backend', pageSubPath), PAGE_CONTENT)
28
+ }
29
+
30
+ function createMockResolver(tmpDir: string, enabled: ModuleEntry[]): PackageResolver {
31
+ const outputDir = path.join(tmpDir, 'output', 'generated')
32
+ fs.mkdirSync(outputDir, { recursive: true })
33
+
34
+ return {
35
+ isMonorepo: () => true,
36
+ getRootDir: () => tmpDir,
37
+ getAppDir: () => path.join(tmpDir, 'app'),
38
+ getOutputDir: () => outputDir,
39
+ getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
40
+ discoverPackages: () => [],
41
+ loadEnabledModules: () => enabled,
42
+ getModulePaths: (entry: ModuleEntry) => ({
43
+ appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
44
+ pkgBase: path.join(tmpDir, 'packages', 'core', 'src', 'modules', entry.id),
45
+ }),
46
+ getModuleImportBase: (entry: ModuleEntry) => ({
47
+ appBase: `@/modules/${entry.id}`,
48
+ pkgBase: `@open-mercato/core/modules/${entry.id}`,
49
+ }),
50
+ getPackageOutputDir: () => outputDir,
51
+ getPackageRoot: () => path.join(tmpDir, 'packages', 'core'),
52
+ }
53
+ }
54
+
55
+ beforeEach(() => {
56
+ tmpDir = createTmpDir()
57
+ })
58
+
59
+ afterEach(() => {
60
+ fs.rmSync(tmpDir, { recursive: true, force: true })
61
+ })
62
+
63
+ describe('detectBackendRouteCollisions', () => {
64
+ it('errors when two package modules register the same backend path, naming both and the path', () => {
65
+ const result = detectBackendRouteCollisions([
66
+ { routePath: '/backend/tasks', moduleId: 'workflows', packageSourced: true },
67
+ { routePath: '/backend/tasks', moduleId: 'agent_orchestrator', packageSourced: true },
68
+ ])
69
+ expect(result.errors).toHaveLength(1)
70
+ expect(result.errors[0]).toContain('/backend/tasks')
71
+ expect(result.errors[0]).toContain('"workflows"')
72
+ expect(result.errors[0]).toContain('"agent_orchestrator"')
73
+ expect(result.notes).toHaveLength(0)
74
+ })
75
+
76
+ it('emits an informational note (no error) when an app module shadows a package module path', () => {
77
+ const result = detectBackendRouteCollisions([
78
+ { routePath: '/backend/orders', moduleId: 'sales', packageSourced: true },
79
+ { routePath: '/backend/orders', moduleId: 'my_overrides', packageSourced: false },
80
+ ])
81
+ expect(result.errors).toHaveLength(0)
82
+ expect(result.notes).toHaveLength(1)
83
+ expect(result.notes[0]).toContain('/backend/orders')
84
+ expect(result.notes[0]).toContain('"sales"')
85
+ expect(result.notes[0]).toContain('"my_overrides"')
86
+ })
87
+
88
+ it('ignores duplicate registrations from a single module (app override of its own package page)', () => {
89
+ const result = detectBackendRouteCollisions([
90
+ { routePath: '/backend/orders', moduleId: 'sales', packageSourced: true },
91
+ { routePath: '/backend/orders', moduleId: 'sales', packageSourced: false },
92
+ ])
93
+ expect(result.errors).toHaveLength(0)
94
+ expect(result.notes).toHaveLength(0)
95
+ })
96
+
97
+ it('stays silent when there is no collision', () => {
98
+ const result = detectBackendRouteCollisions([
99
+ { routePath: '/backend/tasks', moduleId: 'workflows', packageSourced: true },
100
+ { routePath: '/backend/agentic-tasks', moduleId: 'agent_orchestrator', packageSourced: true },
101
+ ])
102
+ expect(result.errors).toHaveLength(0)
103
+ expect(result.notes).toHaveLength(0)
104
+ })
105
+ })
106
+
107
+ describe('generateModuleRegistry backend route collision guard', () => {
108
+ it('fails generation when two package modules register the same backend path', async () => {
109
+ scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
110
+ scaffoldBackendPage(tmpDir, 'launcher_like', 'pkg', 'tasks/page.tsx')
111
+ const resolver = createMockResolver(tmpDir, [
112
+ { id: 'workflows_like', from: '@open-mercato/core' },
113
+ { id: 'launcher_like', from: '@open-mercato/core' },
114
+ ])
115
+
116
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
117
+ try {
118
+ // Two package modules are refused by whichever guard sees the pattern
119
+ // first: the per-pattern assertion during generation, or the aggregate
120
+ // sweep that runs after it. Accept either message so the test asserts the
121
+ // refusal rather than which guard won the race.
122
+ await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
123
+ /Backend route collision|Duplicate backend route pattern/,
124
+ )
125
+ const printed = consoleErrorSpy.mock.calls.map((call) => call.join(' ')).join('\n')
126
+ expect(printed).toContain('/backend/tasks')
127
+ expect(printed).toContain('"workflows_like"')
128
+ expect(printed).toContain('"launcher_like"')
129
+ } finally {
130
+ consoleErrorSpy.mockRestore()
131
+ }
132
+ })
133
+
134
+ it('allows an app module to shadow a package module page', async () => {
135
+ scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
136
+ scaffoldBackendPage(tmpDir, 'app_overrides', 'app', 'tasks/page.tsx')
137
+ const resolver = createMockResolver(tmpDir, [
138
+ { id: 'workflows_like', from: '@open-mercato/core' },
139
+ { id: 'app_overrides', from: '@app' },
140
+ ])
141
+
142
+ await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
143
+ })
144
+
145
+ it('passes when package modules register distinct backend paths', async () => {
146
+ scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
147
+ scaffoldBackendPage(tmpDir, 'launcher_like', 'pkg', 'agentic-tasks/page.tsx')
148
+ const resolver = createMockResolver(tmpDir, [
149
+ { id: 'workflows_like', from: '@open-mercato/core' },
150
+ { id: 'launcher_like', from: '@open-mercato/core' },
151
+ ])
152
+
153
+ await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
154
+ })
155
+ })