@open-mercato/cli 0.7.1-develop.7193.1.910a5b0a1e → 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.
- package/.turbo/turbo-build.log +6 -2
- package/dist/agentic/github-copilot/copilot-instructions.md.template +37 -0
- package/dist/agentic/github-copilot/instructions/entity-guard.instructions.md +27 -0
- package/dist/agentic/github-copilot/instructions/generated-guard.instructions.md +10 -0
- package/dist/agentic/github-copilot/mcp.json.example +13 -0
- package/dist/agentic/guides/module-facts.json +11527 -283
- package/dist/agentic/guides/module-facts.v2.json +11527 -283
- package/dist/agentic/guides/modules/agent_orchestrator/acl-features.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-agents.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-tools-mcp-capabilities.md +13 -0
- package/dist/agentic/guides/modules/agent_orchestrator/backend-pages.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/agent_orchestrator/contribution-resolutions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-registrations-rich.md +42 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-service-tokens.md +24 -0
- package/dist/agentic/guides/modules/agent_orchestrator/domain-commands.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/encryption.md +20 -0
- package/dist/agentic/guides/modules/agent_orchestrator/entities.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/events.md +38 -0
- package/dist/agentic/guides/modules/agent_orchestrator/exact-override-targets.md +122 -0
- package/dist/agentic/guides/modules/agent_orchestrator/host-extension-points.md +10 -0
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/setup.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-contributions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-hosts.md +61 -0
- package/dist/agentic/guides/modules/agent_orchestrator/workers.md +14 -0
- package/dist/agentic/guides/modules/ai_assistant/di-registrations-rich.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/di-service-tokens.md +2 -1
- package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/cli-commands.md +11 -0
- package/dist/agentic/guides/modules/customer_accounts/exact-override-targets.md +5 -4
- package/dist/agentic/guides/modules/customer_accounts/index.md +3 -2
- package/dist/agentic/guides/modules/customer_accounts/setup.md +1 -1
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
- package/dist/agentic/guides/modules/customers/exact-override-targets.md +1 -0
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +3 -0
- package/dist/agentic/guides/modules/customers/index.md +2 -2
- package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/seeds/exact-override-targets.md +14 -0
- package/dist/agentic/guides/modules/seeds/index.md +14 -0
- package/dist/agentic/guides/modules/seeds/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/acl-features.md +11 -1
- package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +4 -1
- package/dist/agentic/guides/modules/workflows/ai-tools-mcp-capabilities.md +17 -0
- package/dist/agentic/guides/modules/workflows/backend-pages.md +3 -0
- package/dist/agentic/guides/modules/workflows/contribution-resolutions.md +24 -0
- package/dist/agentic/guides/modules/workflows/di-registrations-rich.md +11 -7
- package/dist/agentic/guides/modules/workflows/di-service-tokens.md +11 -7
- package/dist/agentic/guides/modules/workflows/domain-commands.md +11 -0
- package/dist/agentic/guides/modules/workflows/entities.md +2 -0
- package/dist/agentic/guides/modules/workflows/events.md +14 -8
- package/dist/agentic/guides/modules/workflows/exact-override-targets.md +45 -10
- package/dist/agentic/guides/modules/workflows/frontend-pages.md +2 -0
- package/dist/agentic/guides/modules/workflows/incoming-installed-contributions.md +17 -0
- package/dist/agentic/guides/modules/workflows/index.md +15 -12
- package/dist/agentic/guides/modules/workflows/notifications.md +2 -0
- package/dist/agentic/guides/modules/workflows/setup.md +1 -1
- package/dist/agentic/guides/modules/workflows/umes-contributions.md +24 -0
- package/dist/agentic/guides/modules/workflows/umes-hosts.md +14 -6
- package/dist/agentic/guides/modules/workflows/workers.md +4 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/AGENTS.md +3 -1
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/design-system-inventory.json +118 -106
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +8 -8
- package/dist/lib/agentic-init.js +4 -0
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +19 -5
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/agent-files.js +913 -0
- package/dist/lib/generators/extensions/agent-files.js.map +7 -0
- package/dist/lib/generators/extensions/index.js +2 -0
- package/dist/lib/generators/extensions/index.js.map +2 -2
- package/dist/lib/generators/index.js +3 -1
- package/dist/lib/generators/index.js.map +2 -2
- package/dist/lib/generators/module-registry.js +40 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-collisions.js +35 -0
- package/dist/lib/generators/route-collisions.js.map +7 -0
- package/dist/lib/generators/web-research-adapters.js +95 -0
- package/dist/lib/generators/web-research-adapters.js.map +7 -0
- package/dist/mercato.js +5 -2
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/agent-files.test.ts +199 -0
- package/src/__tests__/mercato.test.ts +14 -0
- package/src/lib/__tests__/agentic-init.test.ts +11 -0
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +5 -0
- package/src/lib/agentic-init.ts +4 -0
- package/src/lib/agentic-setup.ts +30 -5
- package/src/lib/generators/__tests__/agent-files-extension.test.ts +477 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +25 -8
- package/src/lib/generators/__tests__/route-collisions.test.ts +155 -0
- package/src/lib/generators/extensions/agent-files.ts +1332 -0
- package/src/lib/generators/extensions/index.ts +2 -0
- package/src/lib/generators/index.ts +1 -0
- package/src/lib/generators/module-registry.ts +66 -17
- package/src/lib/generators/route-collisions.ts +57 -0
- package/src/lib/generators/web-research-adapters.ts +133 -0
- package/src/mercato.ts +4 -1
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { countTokens } from '@open-mercato/shared/lib/ai/token-count'
|
|
4
|
+
import type { GeneratorExtension, ModuleScanContext } from '../extension'
|
|
5
|
+
import { resolveStandaloneSourceMirrorBase } from '../scanner'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generator extension for the `agents/<id>/` file-defined-agent convention
|
|
9
|
+
* (AGENT.md + OUTCOME.md). For every enabled module it scans the module's
|
|
10
|
+
* `agents/` tree, validates each agent dir, and emits two artifacts as a
|
|
11
|
+
* deterministic fs side effect from `generateOutput()`:
|
|
12
|
+
*
|
|
13
|
+
* 1. The committed, git-tracked registry manifest
|
|
14
|
+
* `packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts`
|
|
15
|
+
* (raw JSON-Schema, recompiled to Zod at load by `ensureAgentsLoaded`).
|
|
16
|
+
* 2. OpenCode agent `.md` files under `docker/opencode/agents/` (container
|
|
17
|
+
* delivery: dev bind-mount, prod Dockerfile COPY).
|
|
18
|
+
*
|
|
19
|
+
* Both targets live OUTSIDE the app `.mercato/generated/` dir, so they are
|
|
20
|
+
* written directly (not via the Map<filename,content> return, which targets the
|
|
21
|
+
* app generated dir). `generateOutput()` therefore returns an empty Map.
|
|
22
|
+
*
|
|
23
|
+
* Generation FAILS (throws) on any malformed AGENT.md/OUTCOME.md/SKILL.md,
|
|
24
|
+
* naming the offending dir (spec §9). The CLI does not depend on
|
|
25
|
+
* `@open-mercato/core`, so the small AGENT.md/OUTCOME.md/SKILL.md parsers are
|
|
26
|
+
* reimplemented here; they MUST stay in sync with
|
|
27
|
+
* `lib/sdk/{agentMarkdown,skillMarkdown,defineFileAgent}.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Phase 3 also emits NATIVE OpenCode skill files under `docker/opencode/skills/`
|
|
30
|
+
* (frontmatter `name` sanitized to `^[a-z0-9]+(-[a-z0-9]+)*$`, `description`
|
|
31
|
+
* required, body = skill instructions) and unions skill-contributed read-only
|
|
32
|
+
* tools into the agent allowlist (manifest + docker agent-file `tools` block).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** One sandboxed script carried as plain data (Phase 5). */
|
|
36
|
+
type DiscoveredScript = {
|
|
37
|
+
name: string
|
|
38
|
+
source: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type DiscoveredSkill = {
|
|
42
|
+
/** Skill id: frontmatter `id` or, when absent, the skill dir name. */
|
|
43
|
+
id: string
|
|
44
|
+
description: string
|
|
45
|
+
instructions: string
|
|
46
|
+
template?: string
|
|
47
|
+
examples: string[]
|
|
48
|
+
tools: string[]
|
|
49
|
+
/** Sandboxed helper scripts (`scripts/*.ts`), Phase 5. */
|
|
50
|
+
scripts: DiscoveredScript[]
|
|
51
|
+
/** OpenCode native skill name (sanitized to ^[a-z0-9]+(-[a-z0-9]+)*$). */
|
|
52
|
+
openCodeSkillName: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type DiscoveredAgent = {
|
|
56
|
+
moduleId: string
|
|
57
|
+
dir: string
|
|
58
|
+
id: string
|
|
59
|
+
label: string
|
|
60
|
+
description: string
|
|
61
|
+
instructions: string
|
|
62
|
+
resultKind: 'research' | 'proposal' | 'artifact'
|
|
63
|
+
/** Absent for `kind: artifact`, whose result envelope is fixed. */
|
|
64
|
+
outcomeSchema?: Record<string, unknown>
|
|
65
|
+
/** OUTCOME.md prose after the JSON-Schema fence — injected into the agent prompt. */
|
|
66
|
+
outcomeProse: string
|
|
67
|
+
/** Effective allowlist: AGENT.md tools ∪ skill-contributed read-only tools. */
|
|
68
|
+
tools: string[]
|
|
69
|
+
skills: string[]
|
|
70
|
+
subAgents: string[]
|
|
71
|
+
openCodeAgentName: string
|
|
72
|
+
/** Resolved agent-local skill content (Phase 3). */
|
|
73
|
+
skillsContent: DiscoveredSkill[]
|
|
74
|
+
/**
|
|
75
|
+
* Resolved sub-agents under `sub-agents/<subid>/` (Phase 4). Each is a full
|
|
76
|
+
* file agent, constrained to research + non-delegating. Empty for a
|
|
77
|
+
* sub-agent itself (depth cap = 1) and for primaries with no sub-agents.
|
|
78
|
+
*/
|
|
79
|
+
subAgentsContent: DiscoveredAgent[]
|
|
80
|
+
/** `'primary'` (default) or `'subagent'` for a discovered sub-agent. */
|
|
81
|
+
mode: 'primary' | 'subagent'
|
|
82
|
+
maxSteps?: number
|
|
83
|
+
provider?: string
|
|
84
|
+
model?: string
|
|
85
|
+
/** Optional `SAMPLE.json` example input for the Playground "Insert sample" button. */
|
|
86
|
+
sampleInput?: unknown
|
|
87
|
+
/** Optional `FACTS.json` declarations driving the Caseload facts panel. */
|
|
88
|
+
facts?: DiscoveredFact[]
|
|
89
|
+
/** Baked token-usage breakdown of the agent's construction files (Phase: token accounting). */
|
|
90
|
+
tokenUsage: FileAgentTokenUsage
|
|
91
|
+
/** Baked raw content of every construction file, for the read-only Files tab (#12 files). */
|
|
92
|
+
sourceFiles: FileAgentSourceFile[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Raw definition file baked into the manifest so the Files tab reads agent source
|
|
97
|
+
* without runtime fs access. MIRRORS `FileAgentFile` in
|
|
98
|
+
* `packages/enterprise/.../lib/tokens/types.ts` — the CLI cannot import the
|
|
99
|
+
* enterprise package, so the shape is reimplemented here and MUST stay in sync.
|
|
100
|
+
*/
|
|
101
|
+
type FileAgentSourceFile = {
|
|
102
|
+
path: string
|
|
103
|
+
content: string
|
|
104
|
+
tokens: number
|
|
105
|
+
inContext: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Token-usage shape MIRRORED from
|
|
110
|
+
* `packages/enterprise/src/modules/agent_orchestrator/lib/tokens/types.ts`. The
|
|
111
|
+
* CLI cannot import `@open-mercato/enterprise`, so both the type and the walker
|
|
112
|
+
* below are reimplemented here and MUST stay in sync — a parity test in the
|
|
113
|
+
* enterprise module (`agent-token-usage.test.ts`) guards the numbers.
|
|
114
|
+
*/
|
|
115
|
+
type TokenizedFile = { path: string; tokens: number }
|
|
116
|
+
type SkillTokenUsage = { id: string; tokens: number; files: TokenizedFile[] }
|
|
117
|
+
type ToolTokenUsage = { name: string; path: string; tokens: number }
|
|
118
|
+
type SubAgentTokenUsage = { id: string; tokens: number }
|
|
119
|
+
type FileAgentTokenUsage = {
|
|
120
|
+
total: number
|
|
121
|
+
self: number
|
|
122
|
+
agent: number
|
|
123
|
+
outcome: number
|
|
124
|
+
skills: SkillTokenUsage[]
|
|
125
|
+
tools: ToolTokenUsage[]
|
|
126
|
+
subAgents: SubAgentTokenUsage[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type DiscoveredFact = {
|
|
130
|
+
label: string
|
|
131
|
+
source: 'input' | 'payload' | 'output'
|
|
132
|
+
path: string
|
|
133
|
+
format?: 'text' | 'number' | 'boolean' | 'percent'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/
|
|
137
|
+
const JSON_FENCE_RE = /```json\s*\n([\s\S]*?)\n```/
|
|
138
|
+
const LIST_KEYS = ['tools', 'skills', 'subAgents'] as const
|
|
139
|
+
|
|
140
|
+
function stripQuotes(value: string): string {
|
|
141
|
+
return value.trim().replace(/^['"]/, '').replace(/['"]$/, '').trim()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseInlineList(rawValue: string): string[] {
|
|
145
|
+
return rawValue
|
|
146
|
+
.replace(/^\[/, '')
|
|
147
|
+
.replace(/\]$/, '')
|
|
148
|
+
.split(',')
|
|
149
|
+
.map((entry) => stripQuotes(entry))
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type AgentFrontmatter = {
|
|
154
|
+
id?: string
|
|
155
|
+
label?: string
|
|
156
|
+
description?: string
|
|
157
|
+
provider?: string
|
|
158
|
+
model?: string
|
|
159
|
+
tools: string[]
|
|
160
|
+
skills: string[]
|
|
161
|
+
subAgents: string[]
|
|
162
|
+
maxSteps?: number
|
|
163
|
+
instructions: string
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseAgentMarkdown(raw: string): AgentFrontmatter | null {
|
|
167
|
+
const match = FRONTMATTER_RE.exec(raw)
|
|
168
|
+
if (!match) return null
|
|
169
|
+
const [, frontmatterBlock, body] = match
|
|
170
|
+
const meta: AgentFrontmatter = { tools: [], skills: [], subAgents: [], instructions: body.trim() }
|
|
171
|
+
const lines = frontmatterBlock.split('\n')
|
|
172
|
+
let index = 0
|
|
173
|
+
while (index < lines.length) {
|
|
174
|
+
const line = lines[index]
|
|
175
|
+
if (!line.trim()) {
|
|
176
|
+
index += 1
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
const lineMatch = /^([A-Za-z0-9_]+):\s*(.*)$/.exec(line)
|
|
180
|
+
if (!lineMatch) {
|
|
181
|
+
index += 1
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
const key = lineMatch[1]
|
|
185
|
+
const rawValue = lineMatch[2].trim()
|
|
186
|
+
if ((LIST_KEYS as readonly string[]).includes(key)) {
|
|
187
|
+
const listKey = key as (typeof LIST_KEYS)[number]
|
|
188
|
+
if (rawValue.startsWith('[')) {
|
|
189
|
+
meta[listKey] = parseInlineList(rawValue)
|
|
190
|
+
index += 1
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const items: string[] = []
|
|
194
|
+
index += 1
|
|
195
|
+
while (index < lines.length && /^\s*-\s+/.test(lines[index])) {
|
|
196
|
+
items.push(stripQuotes(lines[index].replace(/^\s*-\s+/, '')))
|
|
197
|
+
index += 1
|
|
198
|
+
}
|
|
199
|
+
meta[listKey] = items.filter(Boolean)
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (key === 'maxSteps') {
|
|
203
|
+
const parsed = Number.parseInt(stripQuotes(rawValue), 10)
|
|
204
|
+
if (!Number.isNaN(parsed)) meta.maxSteps = parsed
|
|
205
|
+
index += 1
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (key === 'id' || key === 'label' || key === 'description' || key === 'provider' || key === 'model') {
|
|
209
|
+
meta[key] = stripQuotes(rawValue)
|
|
210
|
+
}
|
|
211
|
+
index += 1
|
|
212
|
+
}
|
|
213
|
+
if (!meta.id || !meta.label || !meta.description) return null
|
|
214
|
+
return meta
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseOutcomeKind(frontmatterBlock: string): 'research' | 'proposal' | 'artifact' | null {
|
|
218
|
+
for (const line of frontmatterBlock.split('\n')) {
|
|
219
|
+
const match = /^kind:\s*(.*)$/.exec(line.trim())
|
|
220
|
+
if (!match) continue
|
|
221
|
+
const value = stripQuotes(match[1])
|
|
222
|
+
if (value === 'research' || value === 'proposal' || value === 'artifact') return value
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseOutcomeMarkdown(
|
|
229
|
+
raw: string,
|
|
230
|
+
): {
|
|
231
|
+
kind: 'research' | 'proposal' | 'artifact'
|
|
232
|
+
schema?: Record<string, unknown>
|
|
233
|
+
prose: string
|
|
234
|
+
} | null {
|
|
235
|
+
const frontmatterMatch = FRONTMATTER_RE.exec(raw)
|
|
236
|
+
if (!frontmatterMatch) return null
|
|
237
|
+
const [, frontmatterBlock, body] = frontmatterMatch
|
|
238
|
+
const kind = parseOutcomeKind(frontmatterBlock)
|
|
239
|
+
if (!kind) return null
|
|
240
|
+
const fenceMatch = JSON_FENCE_RE.exec(body)
|
|
241
|
+
// An artifact agent declares no schema — its envelope is the fixed file list —
|
|
242
|
+
// so its whole OUTCOME.md is guidance.
|
|
243
|
+
if (!fenceMatch) {
|
|
244
|
+
return kind === 'artifact' ? { kind, prose: body.trim() } : null
|
|
245
|
+
}
|
|
246
|
+
let parsed: unknown
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(fenceMatch[1])
|
|
249
|
+
} catch {
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null
|
|
253
|
+
const prose = body.slice(fenceMatch.index + fenceMatch[0].length).trim()
|
|
254
|
+
return { kind, schema: parsed as Record<string, unknown>, prose }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Keep in sync with `agent_orchestrator/lib/sdk/outcomeSchema.ts` (the CLI cannot
|
|
258
|
+
// import @open-mercato/core). Mirrors `jsonSchemaToZod`'s supported subset so a
|
|
259
|
+
// schema that parses as JSON but could NOT compile to Zod fails generation LOUDLY
|
|
260
|
+
// here, instead of parsing fine and being silently dropped at load time (M2).
|
|
261
|
+
const OUTCOME_UNSUPPORTED_KEYWORDS = [
|
|
262
|
+
'oneOf', 'anyOf', 'allOf', 'not', '$ref', 'format', 'patternProperties', 'pattern',
|
|
263
|
+
'additionalItems', 'propertyNames', 'if', 'then', 'else',
|
|
264
|
+
] as const
|
|
265
|
+
const OUTCOME_SUPPORTED_TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean']
|
|
266
|
+
|
|
267
|
+
/** Throw (failing `yarn generate`) when an OUTCOME schema node is outside the supported subset. */
|
|
268
|
+
function assertOutcomeSchemaSupported(node: unknown, where: string, path = '$'): void {
|
|
269
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) {
|
|
270
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${where}: schema node at ${path} must be an object`)
|
|
271
|
+
}
|
|
272
|
+
const schema = node as Record<string, unknown>
|
|
273
|
+
for (const keyword of OUTCOME_UNSUPPORTED_KEYWORDS) {
|
|
274
|
+
if (keyword in schema) {
|
|
275
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${where}: unsupported keyword "${keyword}" at ${path}`)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if ('const' in schema) return
|
|
279
|
+
const type = schema.type
|
|
280
|
+
if (typeof type !== 'string' || !OUTCOME_SUPPORTED_TYPES.includes(type)) {
|
|
281
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${where}: missing/unsupported "type" at ${path}`)
|
|
282
|
+
}
|
|
283
|
+
if (type === 'object' && schema.properties != null) {
|
|
284
|
+
if (typeof schema.properties !== 'object' || schema.properties === null || Array.isArray(schema.properties)) {
|
|
285
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${where}: "properties" at ${path} must be an object`)
|
|
286
|
+
}
|
|
287
|
+
for (const [key, child] of Object.entries(schema.properties as Record<string, unknown>)) {
|
|
288
|
+
assertOutcomeSchemaSupported(child, where, `${path}.${key}`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (type === 'array') {
|
|
292
|
+
if (schema.items == null) {
|
|
293
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${where}: array at ${path} requires "items"`)
|
|
294
|
+
}
|
|
295
|
+
assertOutcomeSchemaSupported(schema.items, where, `${path}[]`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sanitizeAgentName(id: string): string {
|
|
300
|
+
return id.replace(/[^a-z0-9_-]/gi, '_')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Read the optional `agents/<id>/SAMPLE.json` example input emitted into the
|
|
305
|
+
* manifest for the Playground "Insert sample" button. Pure JSON (no markdown),
|
|
306
|
+
* so a missing file is fine; a malformed one fails generation LOUDLY (naming the
|
|
307
|
+
* dir) rather than being silently dropped. Must stay in sync with
|
|
308
|
+
* `lib/sdk/defineFileAgent.ts` `loadSampleInput`.
|
|
309
|
+
*/
|
|
310
|
+
function discoverSampleInput(dir: string): unknown {
|
|
311
|
+
const samplePath = path.join(dir, 'SAMPLE.json')
|
|
312
|
+
if (!fs.existsSync(samplePath)) return undefined
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(fs.readFileSync(samplePath, 'utf8'))
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
317
|
+
throw new Error(`[internal] malformed SAMPLE.json at ${dir}: ${detail}`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const FACT_SOURCES = ['input', 'payload', 'output'] as const
|
|
322
|
+
const FACT_FORMATS = ['text', 'number', 'boolean', 'percent'] as const
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Read the optional `agents/<id>/FACTS.json` declarations that drive the
|
|
326
|
+
* Caseload decision panel's facts grid (label + dot-path into the run input,
|
|
327
|
+
* proposal payload, or run output). Accepts `{ "facts": [...] }` or a bare
|
|
328
|
+
* array. A missing file is fine; a malformed one fails generation LOUDLY
|
|
329
|
+
* (naming the dir). Must stay in sync with `lib/sdk/defineFileAgent.ts`
|
|
330
|
+
* `loadFacts`.
|
|
331
|
+
*/
|
|
332
|
+
function discoverFacts(dir: string): DiscoveredFact[] | undefined {
|
|
333
|
+
const factsPath = path.join(dir, 'FACTS.json')
|
|
334
|
+
if (!fs.existsSync(factsPath)) return undefined
|
|
335
|
+
let parsed: unknown
|
|
336
|
+
try {
|
|
337
|
+
parsed = JSON.parse(fs.readFileSync(factsPath, 'utf8'))
|
|
338
|
+
} catch (err) {
|
|
339
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
340
|
+
throw new Error(`[internal] malformed FACTS.json at ${dir}: ${detail}`)
|
|
341
|
+
}
|
|
342
|
+
const entries = Array.isArray(parsed)
|
|
343
|
+
? parsed
|
|
344
|
+
: parsed && typeof parsed === 'object' && Array.isArray((parsed as { facts?: unknown }).facts)
|
|
345
|
+
? ((parsed as { facts: unknown[] }).facts)
|
|
346
|
+
: null
|
|
347
|
+
if (!entries) {
|
|
348
|
+
throw new Error(`[internal] malformed FACTS.json at ${dir}: expected an array or { "facts": [...] }`)
|
|
349
|
+
}
|
|
350
|
+
return entries.map((entry, index) => {
|
|
351
|
+
const fact = entry as Partial<DiscoveredFact> | null
|
|
352
|
+
if (
|
|
353
|
+
!fact ||
|
|
354
|
+
typeof fact.label !== 'string' ||
|
|
355
|
+
!fact.label.trim() ||
|
|
356
|
+
typeof fact.path !== 'string' ||
|
|
357
|
+
!fact.path.trim() ||
|
|
358
|
+
!FACT_SOURCES.includes(fact.source as (typeof FACT_SOURCES)[number])
|
|
359
|
+
) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`[internal] malformed FACTS.json at ${dir}: entry ${index} needs label (string), source (${FACT_SOURCES.join('|')}), path (string)`,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
if (fact.format !== undefined && !FACT_FORMATS.includes(fact.format as (typeof FACT_FORMATS)[number])) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`[internal] malformed FACTS.json at ${dir}: entry ${index} format must be one of ${FACT_FORMATS.join('|')}`,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
label: fact.label.trim(),
|
|
371
|
+
source: fact.source as DiscoveredFact['source'],
|
|
372
|
+
path: fact.path.trim(),
|
|
373
|
+
...(fact.format !== undefined ? { format: fact.format as DiscoveredFact['format'] } : {}),
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Sanitize a skill id into an OpenCode native skill `name`, which must match
|
|
380
|
+
* `^[a-z0-9]+(-[a-z0-9]+)*$`: lowercase, underscores/dots/spaces → hyphens,
|
|
381
|
+
* any other char → hyphen, collapse runs, trim leading/trailing hyphens. Must
|
|
382
|
+
* stay in sync with the loader's interpretation of skill ids.
|
|
383
|
+
*/
|
|
384
|
+
function sanitizeSkillName(id: string): string {
|
|
385
|
+
const name = id
|
|
386
|
+
.toLowerCase()
|
|
387
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
388
|
+
.replace(/-+/g, '-')
|
|
389
|
+
.replace(/^-+/, '')
|
|
390
|
+
.replace(/-+$/, '')
|
|
391
|
+
return name || 'skill'
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
type SkillFrontmatter = {
|
|
395
|
+
id?: string
|
|
396
|
+
label?: string
|
|
397
|
+
description?: string
|
|
398
|
+
tools: string[]
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Parse an agent-local SKILL.md frontmatter (in sync with
|
|
403
|
+
* `lib/sdk/skillMarkdown.ts`). Agent-local skills may omit `moduleId` and `id`
|
|
404
|
+
* (the dir name is then authoritative); only a parseable frontmatter block is
|
|
405
|
+
* required. Returns null when there is no frontmatter block.
|
|
406
|
+
*/
|
|
407
|
+
function parseSkillMarkdown(raw: string): { meta: SkillFrontmatter; body: string } | null {
|
|
408
|
+
const match = FRONTMATTER_RE.exec(raw)
|
|
409
|
+
if (!match) return null
|
|
410
|
+
const [, frontmatterBlock, body] = match
|
|
411
|
+
const meta: SkillFrontmatter = { tools: [] }
|
|
412
|
+
const lines = frontmatterBlock.split('\n')
|
|
413
|
+
let index = 0
|
|
414
|
+
while (index < lines.length) {
|
|
415
|
+
const line = lines[index]
|
|
416
|
+
if (!line.trim()) {
|
|
417
|
+
index += 1
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
const lineMatch = /^([A-Za-z0-9_]+):\s*(.*)$/.exec(line)
|
|
421
|
+
if (!lineMatch) {
|
|
422
|
+
index += 1
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
const key = lineMatch[1]
|
|
426
|
+
const rawValue = lineMatch[2].trim()
|
|
427
|
+
if (key === 'tools') {
|
|
428
|
+
if (rawValue.startsWith('[')) {
|
|
429
|
+
meta.tools = parseInlineList(rawValue)
|
|
430
|
+
index += 1
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
const items: string[] = []
|
|
434
|
+
index += 1
|
|
435
|
+
while (index < lines.length && /^\s*-\s+/.test(lines[index])) {
|
|
436
|
+
items.push(stripQuotes(lines[index].replace(/^\s*-\s+/, '')))
|
|
437
|
+
index += 1
|
|
438
|
+
}
|
|
439
|
+
meta.tools = items.filter(Boolean)
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
if (key === 'id' || key === 'label' || key === 'description') {
|
|
443
|
+
meta[key] = stripQuotes(rawValue)
|
|
444
|
+
}
|
|
445
|
+
index += 1
|
|
446
|
+
}
|
|
447
|
+
return { meta, body: body.trim() }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function listExampleBodies(skillDir: string): string[] {
|
|
451
|
+
const examplesDir = path.join(skillDir, 'examples')
|
|
452
|
+
if (!fs.existsSync(examplesDir) || !fs.statSync(examplesDir).isDirectory()) return []
|
|
453
|
+
return fs
|
|
454
|
+
.readdirSync(examplesDir)
|
|
455
|
+
.filter((name) => name.endsWith('.md'))
|
|
456
|
+
.sort((a, b) => a.localeCompare(b))
|
|
457
|
+
.map((name) => fs.readFileSync(path.join(examplesDir, name), 'utf8').trim())
|
|
458
|
+
.filter(Boolean)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Basic parse validation of a script source (Phase 5). The script runs in the
|
|
463
|
+
* `isolated-vm` sandbox server-side (never copied to the container), so we only
|
|
464
|
+
* cheaply assert it defines a `run` function — a missing `run` would fail at
|
|
465
|
+
* runtime, so we fail generation early naming the file. We do NOT execute it.
|
|
466
|
+
*/
|
|
467
|
+
function validateScriptSource(file: string, source: string): void {
|
|
468
|
+
const definesRun = /(^|\b)(async\s+)?function\s+run\b/.test(source) || /\brun\s*=/.test(source)
|
|
469
|
+
if (!definesRun) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`[internal] malformed agent script at ${file}: must define a \`run(args)\` function`,
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Read sandboxed scripts from a `scripts/` dir (`*.ts` / `*.js`), Phase 5. Each
|
|
478
|
+
* file's basename (no extension) is the script name; the raw source is carried
|
|
479
|
+
* as plain data. Validates each parses (basic). Ordered by filename.
|
|
480
|
+
*/
|
|
481
|
+
function listScripts(scriptsDir: string): DiscoveredScript[] {
|
|
482
|
+
if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) return []
|
|
483
|
+
const scripts: DiscoveredScript[] = []
|
|
484
|
+
for (const name of fs
|
|
485
|
+
.readdirSync(scriptsDir)
|
|
486
|
+
.filter((entry) => entry.endsWith('.ts') || entry.endsWith('.js'))
|
|
487
|
+
.sort((a, b) => a.localeCompare(b))) {
|
|
488
|
+
const file = path.join(scriptsDir, name)
|
|
489
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
490
|
+
validateScriptSource(file, source)
|
|
491
|
+
scripts.push({ name: name.replace(/\.(ts|js)$/, ''), source })
|
|
492
|
+
}
|
|
493
|
+
return scripts
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Synthetic skill id under which an agent's LOCAL sandboxed tool files register. */
|
|
497
|
+
const AGENT_TOOLS_SKILL_ID = '__agent_tools__'
|
|
498
|
+
const TOOL_REF_RE = /^\s*\/\/\s*@ref:?\s+(\S+)/
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Discover `agents/<id>/tools/*.ts` local tool files (Phase 5). Reference-form
|
|
502
|
+
* files (first line `// @ref <defineAiTool id>`) contribute the id to `refs`
|
|
503
|
+
* (unioned into the allowlist, flows through the central ACL + propose-only
|
|
504
|
+
* gate). Any other file is a LOCAL sandboxed tool: carried as a script run via
|
|
505
|
+
* `run_skill_script` under the synthetic `__agent_tools__` skill. Must stay in
|
|
506
|
+
* sync with `lib/sdk/defineFileAgent.ts` `loadToolFiles`.
|
|
507
|
+
*/
|
|
508
|
+
function discoverToolFiles(agentDir: string): { refs: string[]; scripts: DiscoveredScript[] } {
|
|
509
|
+
const toolsBase = path.join(agentDir, 'tools')
|
|
510
|
+
if (!fs.existsSync(toolsBase) || !fs.statSync(toolsBase).isDirectory()) {
|
|
511
|
+
return { refs: [], scripts: [] }
|
|
512
|
+
}
|
|
513
|
+
const refs: string[] = []
|
|
514
|
+
const scripts: DiscoveredScript[] = []
|
|
515
|
+
for (const name of fs
|
|
516
|
+
.readdirSync(toolsBase)
|
|
517
|
+
.filter((entry) => entry.endsWith('.ts') || entry.endsWith('.js'))
|
|
518
|
+
.sort((a, b) => a.localeCompare(b))) {
|
|
519
|
+
const file = path.join(toolsBase, name)
|
|
520
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
521
|
+
const firstLine = source.split('\n', 1)[0] ?? ''
|
|
522
|
+
const refMatch = TOOL_REF_RE.exec(firstLine)
|
|
523
|
+
if (refMatch) {
|
|
524
|
+
refs.push(refMatch[1])
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
validateScriptSource(file, source)
|
|
528
|
+
scripts.push({ name: name.replace(/\.(ts|js)$/, ''), source })
|
|
529
|
+
}
|
|
530
|
+
return { refs, scripts }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Discover the agent's referenced skills under `agents/<id>/skills/<skill_id>/`.
|
|
535
|
+
* For each id in AGENT.md `skills:` we look up the matching dir (by frontmatter
|
|
536
|
+
* id or dir name). FAILS generation when a referenced SKILL.md is malformed
|
|
537
|
+
* (present dir but unparseable frontmatter). A referenced id with no dir is
|
|
538
|
+
* skipped (the loader warns identically at runtime).
|
|
539
|
+
*/
|
|
540
|
+
function discoverAgentSkills(agentDir: string, skillIds: string[]): DiscoveredSkill[] {
|
|
541
|
+
if (skillIds.length === 0) return []
|
|
542
|
+
const skillsBase = path.join(agentDir, 'skills')
|
|
543
|
+
if (!fs.existsSync(skillsBase) || !fs.statSync(skillsBase).isDirectory()) return []
|
|
544
|
+
|
|
545
|
+
const bySkillId = new Map<string, DiscoveredSkill>()
|
|
546
|
+
for (const entry of fs.readdirSync(skillsBase, { withFileTypes: true })) {
|
|
547
|
+
if (!entry.isDirectory()) continue
|
|
548
|
+
const skillDir = path.join(skillsBase, entry.name)
|
|
549
|
+
const skillPath = path.join(skillDir, 'SKILL.md')
|
|
550
|
+
if (!fs.existsSync(skillPath)) continue
|
|
551
|
+
const parsed = parseSkillMarkdown(fs.readFileSync(skillPath, 'utf8'))
|
|
552
|
+
if (!parsed) {
|
|
553
|
+
throw new Error(`[internal] malformed SKILL.md at ${skillDir}: missing frontmatter block`)
|
|
554
|
+
}
|
|
555
|
+
const id = parsed.meta.id || entry.name
|
|
556
|
+
const templatePath = path.join(skillDir, 'TEMPLATE.md')
|
|
557
|
+
const template = fs.existsSync(templatePath)
|
|
558
|
+
? fs.readFileSync(templatePath, 'utf8').trim() || undefined
|
|
559
|
+
: undefined
|
|
560
|
+
bySkillId.set(id, {
|
|
561
|
+
id,
|
|
562
|
+
description: parsed.meta.description ?? '',
|
|
563
|
+
instructions: parsed.body,
|
|
564
|
+
template,
|
|
565
|
+
examples: listExampleBodies(skillDir),
|
|
566
|
+
tools: parsed.meta.tools,
|
|
567
|
+
scripts: listScripts(path.join(skillDir, 'scripts')),
|
|
568
|
+
openCodeSkillName: sanitizeSkillName(id),
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const resolved: DiscoveredSkill[] = []
|
|
573
|
+
for (const skillId of skillIds) {
|
|
574
|
+
const skill = bySkillId.get(skillId)
|
|
575
|
+
if (skill) resolved.push(skill)
|
|
576
|
+
}
|
|
577
|
+
return resolved
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Discover and validate the sub-agents under `agents/<id>/sub-agents/<subid>/`
|
|
582
|
+
* (Phase 4). Each is a full file agent (AGENT.md + OUTCOME.md) constrained to:
|
|
583
|
+
* 1. OUTCOME `kind: research` (sub-agents inform; only the primary proposes);
|
|
584
|
+
* 2. NO `subAgents` of its own (depth cap = 1).
|
|
585
|
+
* FAILS generation (throws, naming the dir) on a malformed sub-agent OR a
|
|
586
|
+
* constraint violation — in sync with `lib/sdk/defineFileAgent.ts` `loadSubAgentDir`.
|
|
587
|
+
*/
|
|
588
|
+
function discoverSubAgents(agentDir: string): DiscoveredAgent[] {
|
|
589
|
+
const base = path.join(agentDir, 'sub-agents')
|
|
590
|
+
if (!fs.existsSync(base) || !fs.statSync(base).isDirectory()) return []
|
|
591
|
+
const subAgents: DiscoveredAgent[] = []
|
|
592
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
593
|
+
if (!entry.isDirectory() || entry.name === '__tests__') continue
|
|
594
|
+
const dir = path.join(base, entry.name)
|
|
595
|
+
const agentMdPath = path.join(dir, 'AGENT.md')
|
|
596
|
+
const outcomePath = path.join(dir, 'OUTCOME.md')
|
|
597
|
+
if (!fs.existsSync(agentMdPath) || !fs.existsSync(outcomePath)) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`[internal] malformed sub-agent at ${dir}: both AGENT.md and OUTCOME.md are required`,
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
const agent = parseAgentMarkdown(fs.readFileSync(agentMdPath, 'utf8'))
|
|
603
|
+
if (!agent) {
|
|
604
|
+
throw new Error(`[internal] malformed AGENT.md at ${dir}: missing id/label/description`)
|
|
605
|
+
}
|
|
606
|
+
const outcome = parseOutcomeMarkdown(fs.readFileSync(outcomePath, 'utf8'))
|
|
607
|
+
if (!outcome) {
|
|
608
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${dir}: missing kind or JSON-Schema block`)
|
|
609
|
+
}
|
|
610
|
+
if (outcome.schema) assertOutcomeSchemaSupported(outcome.schema, dir)
|
|
611
|
+
if (outcome.kind !== 'research') {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`[internal] sub-agent at ${dir} must be research (kind: research); only the primary proposes`,
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
if (agent.subAgents.length > 0) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`[internal] sub-agent at ${dir} may not declare subAgents (depth cap = 1); sub-agents may not delegate further`,
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
const skillsContent = discoverAgentSkills(dir, agent.skills)
|
|
622
|
+
const skillTools = skillsContent.flatMap((skill) => skill.tools)
|
|
623
|
+
const toolFiles = discoverToolFiles(dir)
|
|
624
|
+
const effectiveSkillsContent =
|
|
625
|
+
toolFiles.scripts.length > 0
|
|
626
|
+
? [
|
|
627
|
+
...skillsContent,
|
|
628
|
+
{
|
|
629
|
+
id: AGENT_TOOLS_SKILL_ID,
|
|
630
|
+
description: '',
|
|
631
|
+
instructions: '',
|
|
632
|
+
examples: [],
|
|
633
|
+
tools: [],
|
|
634
|
+
scripts: toolFiles.scripts,
|
|
635
|
+
openCodeSkillName: sanitizeSkillName(AGENT_TOOLS_SKILL_ID),
|
|
636
|
+
},
|
|
637
|
+
]
|
|
638
|
+
: skillsContent
|
|
639
|
+
const effectiveTools = Array.from(
|
|
640
|
+
new Set([...agent.tools, ...skillTools, ...toolFiles.refs]),
|
|
641
|
+
)
|
|
642
|
+
subAgents.push({
|
|
643
|
+
moduleId: '',
|
|
644
|
+
dir,
|
|
645
|
+
id: agent.id!,
|
|
646
|
+
label: agent.label!,
|
|
647
|
+
description: agent.description!,
|
|
648
|
+
instructions: agent.instructions,
|
|
649
|
+
resultKind: outcome.kind,
|
|
650
|
+
outcomeSchema: outcome.schema,
|
|
651
|
+
outcomeProse: outcome.prose,
|
|
652
|
+
tools: effectiveTools,
|
|
653
|
+
skills: agent.skills,
|
|
654
|
+
subAgents: [],
|
|
655
|
+
openCodeAgentName: sanitizeAgentName(agent.id!),
|
|
656
|
+
skillsContent: effectiveSkillsContent,
|
|
657
|
+
subAgentsContent: [],
|
|
658
|
+
mode: 'subagent',
|
|
659
|
+
maxSteps: agent.maxSteps,
|
|
660
|
+
provider: agent.provider,
|
|
661
|
+
model: agent.model,
|
|
662
|
+
sampleInput: discoverSampleInput(dir),
|
|
663
|
+
facts: discoverFacts(dir),
|
|
664
|
+
tokenUsage: discoverAgentTokenUsage(dir),
|
|
665
|
+
sourceFiles: collectAgentFiles(dir),
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
return subAgents
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// --- Token accounting (MIRRORS lib/tokens/computeAgentTokenUsage.ts) ---
|
|
672
|
+
|
|
673
|
+
const TOKEN_TOOL_EXTENSIONS = ['.ts', '.js']
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* MIRRORS `toPosixRelativePath` in lib/tokens/computeAgentTokenUsage.ts.
|
|
677
|
+
*
|
|
678
|
+
* These relative paths are baked into `file-agents.generated.ts` and split into
|
|
679
|
+
* a folder tree in the browser, so they must be `/`-separated whatever machine
|
|
680
|
+
* ran `yarn generate`. `path.join` answers with the native separator, which on
|
|
681
|
+
* Windows baked backslashes into the artifact and flattened the Files tab.
|
|
682
|
+
*/
|
|
683
|
+
function toPosixRelativePath(relativePath: string): string {
|
|
684
|
+
return relativePath.split(path.sep).join('/')
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function tokenCountFile(agentDir: string, relativePath: string): TokenizedFile | null {
|
|
688
|
+
const absolute = path.join(agentDir, relativePath)
|
|
689
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) return null
|
|
690
|
+
return { path: toPosixRelativePath(relativePath), tokens: countTokens(fs.readFileSync(absolute, 'utf8')) }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function tokenListFiles(dir: string, extensions?: string[]): string[] {
|
|
694
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return []
|
|
695
|
+
return fs
|
|
696
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
697
|
+
.filter((entry) => entry.isFile())
|
|
698
|
+
.map((entry) => entry.name)
|
|
699
|
+
.filter((name) => !extensions || extensions.includes(path.extname(name)))
|
|
700
|
+
.sort((a, b) => a.localeCompare(b))
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function tokenListDirs(dir: string): string[] {
|
|
704
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return []
|
|
705
|
+
return fs
|
|
706
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
707
|
+
.filter((entry) => entry.isDirectory() && entry.name !== '__tests__')
|
|
708
|
+
.map((entry) => entry.name)
|
|
709
|
+
.sort((a, b) => a.localeCompare(b))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function tokenComputeSkills(agentDir: string): SkillTokenUsage[] {
|
|
713
|
+
return tokenListDirs(path.join(agentDir, 'skills')).map((skillId) => {
|
|
714
|
+
const skillRel = path.join('skills', skillId)
|
|
715
|
+
const files: TokenizedFile[] = []
|
|
716
|
+
for (const name of ['SKILL.md', 'TEMPLATE.md']) {
|
|
717
|
+
const file = tokenCountFile(agentDir, path.join(skillRel, name))
|
|
718
|
+
if (file) files.push(file)
|
|
719
|
+
}
|
|
720
|
+
for (const subdir of ['examples', 'scripts']) {
|
|
721
|
+
for (const name of tokenListFiles(path.join(agentDir, skillRel, subdir))) {
|
|
722
|
+
const file = tokenCountFile(agentDir, path.join(skillRel, subdir, name))
|
|
723
|
+
if (file) files.push(file)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { id: skillId, tokens: files.reduce((sum, f) => sum + f.tokens, 0), files }
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function tokenComputeTools(agentDir: string): ToolTokenUsage[] {
|
|
731
|
+
return tokenListFiles(path.join(agentDir, 'tools'), TOKEN_TOOL_EXTENSIONS).map((name) => {
|
|
732
|
+
const rel = path.join('tools', name)
|
|
733
|
+
return {
|
|
734
|
+
name: name.replace(/\.[^.]+$/, ''),
|
|
735
|
+
path: toPosixRelativePath(rel),
|
|
736
|
+
tokens: countTokens(fs.readFileSync(path.join(agentDir, rel), 'utf8')),
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function discoverAgentTokenUsage(agentDir: string, depth = 0): FileAgentTokenUsage {
|
|
742
|
+
const agent = tokenCountFile(agentDir, 'AGENT.md')?.tokens ?? 0
|
|
743
|
+
const outcome = tokenCountFile(agentDir, 'OUTCOME.md')?.tokens ?? 0
|
|
744
|
+
const skills = tokenComputeSkills(agentDir)
|
|
745
|
+
const tools = tokenComputeTools(agentDir)
|
|
746
|
+
|
|
747
|
+
const subAgents: SubAgentTokenUsage[] = []
|
|
748
|
+
if (depth === 0) {
|
|
749
|
+
for (const subId of tokenListDirs(path.join(agentDir, 'sub-agents'))) {
|
|
750
|
+
const nested = discoverAgentTokenUsage(path.join(agentDir, 'sub-agents', subId), depth + 1)
|
|
751
|
+
subAgents.push({ id: subId, tokens: nested.total })
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const self =
|
|
756
|
+
agent +
|
|
757
|
+
outcome +
|
|
758
|
+
skills.reduce((sum, s) => sum + s.tokens, 0) +
|
|
759
|
+
tools.reduce((sum, t) => sum + t.tokens, 0)
|
|
760
|
+
const total = self + subAgents.reduce((sum, s) => sum + s.tokens, 0)
|
|
761
|
+
|
|
762
|
+
return { total, self, agent, outcome, skills, tools, subAgents }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// --- Raw source-file collection (feeds the read-only Files tab, #12 files) ---
|
|
766
|
+
|
|
767
|
+
/** Files present at the agent root but NOT part of the constructed prompt. */
|
|
768
|
+
const AGENT_AUX_FILES = ['SAMPLE.json', 'FACTS.json']
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Walks every construction file of a file-defined agent and returns its raw
|
|
772
|
+
* content + `o200k_base` token count, paths relative to the agent root. Recurses
|
|
773
|
+
* `sub-agents/<id>/` once (depth cap = 1, matching `discoverAgentTokenUsage`),
|
|
774
|
+
* prefixing nested paths with `sub-agents/<id>/`. The `inContext` sum of a tree
|
|
775
|
+
* equals `discoverAgentTokenUsage(...).total`, so the Files tab and the baked
|
|
776
|
+
* token estimate stay consistent.
|
|
777
|
+
*/
|
|
778
|
+
function collectAgentFiles(agentDir: string, prefix = '', depth = 0): FileAgentSourceFile[] {
|
|
779
|
+
const files: FileAgentSourceFile[] = []
|
|
780
|
+
const add = (relativePath: string, inContext: boolean): void => {
|
|
781
|
+
const absolute = path.join(agentDir, relativePath)
|
|
782
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) return
|
|
783
|
+
const content = fs.readFileSync(absolute, 'utf8')
|
|
784
|
+
files.push({ path: toPosixRelativePath(path.join(prefix, relativePath)), content, tokens: countTokens(content), inContext })
|
|
785
|
+
}
|
|
786
|
+
add('AGENT.md', true)
|
|
787
|
+
add('OUTCOME.md', true)
|
|
788
|
+
for (const name of AGENT_AUX_FILES) add(name, false)
|
|
789
|
+
for (const skillId of tokenListDirs(path.join(agentDir, 'skills'))) {
|
|
790
|
+
const skillRel = path.join('skills', skillId)
|
|
791
|
+
for (const name of ['SKILL.md', 'TEMPLATE.md']) add(path.join(skillRel, name), true)
|
|
792
|
+
for (const subdir of ['examples', 'scripts']) {
|
|
793
|
+
for (const name of tokenListFiles(path.join(agentDir, skillRel, subdir))) {
|
|
794
|
+
add(path.join(skillRel, subdir, name), true)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (const name of tokenListFiles(path.join(agentDir, 'tools'), TOKEN_TOOL_EXTENSIONS)) {
|
|
799
|
+
add(path.join('tools', name), true)
|
|
800
|
+
}
|
|
801
|
+
if (depth === 0) {
|
|
802
|
+
for (const subId of tokenListDirs(path.join(agentDir, 'sub-agents'))) {
|
|
803
|
+
files.push(
|
|
804
|
+
...collectAgentFiles(
|
|
805
|
+
path.join(agentDir, 'sub-agents', subId),
|
|
806
|
+
path.join(prefix, 'sub-agents', subId),
|
|
807
|
+
depth + 1,
|
|
808
|
+
),
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return files
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderOpenCodeSkillFile(skill: DiscoveredSkill): string {
|
|
816
|
+
const frontmatter = [
|
|
817
|
+
'---',
|
|
818
|
+
`name: ${skill.openCodeSkillName}`,
|
|
819
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
820
|
+
'---',
|
|
821
|
+
]
|
|
822
|
+
return `${frontmatter.join('\n')}\n${skill.instructions.trim()}\n`
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Walk up from a known in-repo path until a dir containing both `docker` and `packages` is found. */
|
|
826
|
+
function findRepoRoot(start: string): string | null {
|
|
827
|
+
let current = path.resolve(start)
|
|
828
|
+
for (let depth = 0; depth < 40; depth += 1) {
|
|
829
|
+
if (
|
|
830
|
+
fs.existsSync(path.join(current, 'docker', 'opencode')) &&
|
|
831
|
+
fs.existsSync(path.join(current, 'packages'))
|
|
832
|
+
) {
|
|
833
|
+
return current
|
|
834
|
+
}
|
|
835
|
+
const parent = path.dirname(current)
|
|
836
|
+
if (parent === current) break
|
|
837
|
+
current = parent
|
|
838
|
+
}
|
|
839
|
+
return null
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function listAgentDirs(agentsBase: string): string[] {
|
|
843
|
+
if (!fs.existsSync(agentsBase)) return []
|
|
844
|
+
return fs
|
|
845
|
+
.readdirSync(agentsBase, { withFileTypes: true })
|
|
846
|
+
.filter((entry) => entry.isDirectory() && entry.name !== '__tests__')
|
|
847
|
+
.map((entry) => path.join(agentsBase, entry.name))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function renderToolPermissionLine(toolName: string): string {
|
|
851
|
+
return ` ${JSON.stringify(toolName)}: true`
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const ORCHESTRATOR_MODULE_ID = 'agent_orchestrator'
|
|
855
|
+
// OpenCode names every MCP tool `<serverKey>_<toolName with dots→underscores>`
|
|
856
|
+
// (verified against the running image). Keep in sync with
|
|
857
|
+
// `lib/sdk/defineFileAgent.ts`.
|
|
858
|
+
const OPENCODE_MCP_SERVER_KEY = 'open-mercato'
|
|
859
|
+
const CORE_FILE_AGENT_TOOL_IDS = [
|
|
860
|
+
'agent_orchestrator.submit_outcome',
|
|
861
|
+
'agent_orchestrator.load_skill',
|
|
862
|
+
'agent_orchestrator.run_skill_script',
|
|
863
|
+
]
|
|
864
|
+
/**
|
|
865
|
+
* MCP tool a file-agent orchestrator uses to run a sub-agent SERVER-SIDE. We
|
|
866
|
+
* route delegation through this tool instead of OpenCode's built-in `task` tool:
|
|
867
|
+
* `task` spawns each sub-agent in a fresh session that never receives the run's
|
|
868
|
+
* `_sessionToken`, so the sub-agent's MCP calls fail with `no_active_run`. The
|
|
869
|
+
* delegate tool runs the sub-agent via `agentRuntime.run`, which mints its own
|
|
870
|
+
* per-run session token + correlation, so sub-agent tool calls authenticate.
|
|
871
|
+
*/
|
|
872
|
+
const DELEGATE_AGENT_TOOL_ID = 'agent_orchestrator.delegate_agent'
|
|
873
|
+
function toOpenCodeMcpToolId(omToolId: string): string {
|
|
874
|
+
return `${OPENCODE_MCP_SERVER_KEY}_${omToolId.replace(/\./g, '_')}`
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Render an OpenCode agent .md file. Must stay in sync with
|
|
879
|
+
* `lib/sdk/defineFileAgent.ts` `renderOpenCodeAgentFile`. The built-in OpenCode
|
|
880
|
+
* `task` tool is NEVER granted (`permission.task: deny` for every agent): it
|
|
881
|
+
* spawns each sub-agent in a fresh session that does not inherit the run's
|
|
882
|
+
* `_sessionToken`, so the sub-agent's MCP calls fail with `no_active_run`.
|
|
883
|
+
* Instead, a primary that declares sub-agents is granted the
|
|
884
|
+
* `agent_orchestrator.delegate_agent` MCP tool (runs each sub-agent server-side
|
|
885
|
+
* under its own minted session token) and gains a "Sub-agents" prompt section
|
|
886
|
+
* nudging parallel fan-out through that tool. Sub-agent files (`mode: subagent`)
|
|
887
|
+
* get neither the delegate tool nor `task` (depth cap = 1).
|
|
888
|
+
*/
|
|
889
|
+
function renderOpenCodeAgentFile(agent: DiscoveredAgent): string {
|
|
890
|
+
const subAgentIds =
|
|
891
|
+
agent.mode === 'primary' ? agent.subAgentsContent.map((sub) => sub.id) : []
|
|
892
|
+
const hasSubAgents = subAgentIds.length > 0
|
|
893
|
+
const omMcpToolIds = Array.from(
|
|
894
|
+
new Set([
|
|
895
|
+
...agent.tools,
|
|
896
|
+
...CORE_FILE_AGENT_TOOL_IDS,
|
|
897
|
+
...(hasSubAgents ? [DELEGATE_AGENT_TOOL_ID] : []),
|
|
898
|
+
]),
|
|
899
|
+
)
|
|
900
|
+
const allowedTools = omMcpToolIds.map(toOpenCodeMcpToolId)
|
|
901
|
+
const modelLine =
|
|
902
|
+
agent.provider && agent.model
|
|
903
|
+
? `model: ${agent.provider}/${agent.model}`
|
|
904
|
+
: agent.model
|
|
905
|
+
? `model: ${agent.model}`
|
|
906
|
+
: null
|
|
907
|
+
const frontmatterLines = [
|
|
908
|
+
'---',
|
|
909
|
+
`description: ${JSON.stringify(agent.description)}`,
|
|
910
|
+
...(modelLine ? [modelLine] : []),
|
|
911
|
+
`mode: ${agent.mode}`,
|
|
912
|
+
'tools:',
|
|
913
|
+
' "*": false',
|
|
914
|
+
...allowedTools.map(renderToolPermissionLine),
|
|
915
|
+
'permission:',
|
|
916
|
+
' write: deny',
|
|
917
|
+
' edit: deny',
|
|
918
|
+
' bash: deny',
|
|
919
|
+
' task: deny',
|
|
920
|
+
'---',
|
|
921
|
+
]
|
|
922
|
+
const terminalInstruction =
|
|
923
|
+
`Finish by calling the \`${toOpenCodeMcpToolId('agent_orchestrator.submit_outcome')}\` tool with a value matching the outcome contract (pass it as the \`outcome\` argument). You MUST call the tool — do not answer in prose or emit the result as a code block.`
|
|
924
|
+
const delegateToolName = toOpenCodeMcpToolId(DELEGATE_AGENT_TOOL_ID)
|
|
925
|
+
const subAgentSection = hasSubAgents
|
|
926
|
+
? `## Sub-agents\nDelegate a sub-task by calling the \`${delegateToolName}\` tool with \`{ agentId: "<sub-agent id>", input: <sub-task input object> }\`. Issue multiple \`${delegateToolName}\` calls in the SAME step to fan out in parallel, then combine their results before submitting your outcome. Available sub-agents: ${subAgentIds.join(', ')}.`
|
|
927
|
+
: null
|
|
928
|
+
// Inject the OUTCOME contract so the agent SEES the exact shape it must submit
|
|
929
|
+
// (otherwise it guesses and learns the shape only from validation errors). Keep
|
|
930
|
+
// in sync with lib/sdk/defineFileAgent.ts renderOutcomeSection.
|
|
931
|
+
const outcomeTarget = agent.resultKind === 'proposal' ? 'the `proposal` object' : 'the `data` object'
|
|
932
|
+
const outcomeSection =
|
|
933
|
+
agent.resultKind === 'artifact'
|
|
934
|
+
? [
|
|
935
|
+
'## Outcome contract',
|
|
936
|
+
"Write the files you produce into the run's `out/` directory, then pass this shape as the `outcome` argument of the submit_outcome tool:",
|
|
937
|
+
'',
|
|
938
|
+
'```json',
|
|
939
|
+
JSON.stringify(
|
|
940
|
+
{
|
|
941
|
+
artifacts: [
|
|
942
|
+
{ fileName: 'report.pdf', mimeType: 'application/pdf', caption: 'What this file is' },
|
|
943
|
+
],
|
|
944
|
+
summary: 'One sentence about what you produced.',
|
|
945
|
+
},
|
|
946
|
+
null,
|
|
947
|
+
2,
|
|
948
|
+
),
|
|
949
|
+
'```',
|
|
950
|
+
...(agent.outcomeProse ? ['', agent.outcomeProse] : []),
|
|
951
|
+
].join('\n')
|
|
952
|
+
: [
|
|
953
|
+
'## Outcome contract',
|
|
954
|
+
`Your result MUST match this JSON Schema (${outcomeTarget}). Pass it as the \`outcome\` argument of the submit_outcome tool, as a JSON object (not a string):`,
|
|
955
|
+
'',
|
|
956
|
+
'```json',
|
|
957
|
+
JSON.stringify(agent.outcomeSchema, null, 2),
|
|
958
|
+
'```',
|
|
959
|
+
...(agent.outcomeProse ? ['', agent.outcomeProse] : []),
|
|
960
|
+
].join('\n')
|
|
961
|
+
const body = [
|
|
962
|
+
agent.instructions.trim(),
|
|
963
|
+
...(subAgentSection ? [subAgentSection] : []),
|
|
964
|
+
outcomeSection,
|
|
965
|
+
terminalInstruction,
|
|
966
|
+
]
|
|
967
|
+
.filter(Boolean)
|
|
968
|
+
.join('\n\n')
|
|
969
|
+
return `${frontmatterLines.join('\n')}\n${body}\n`
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Render one agent descriptor as a key-per-line object literal at `indent`.
|
|
974
|
+
* Used for both top-level agents and nested sub-agents (Phase 4). A primary that
|
|
975
|
+
* declares sub-agents emits them as a nested `subAgentDescriptors` array so
|
|
976
|
+
* `ensureAgentsLoaded` can register each sub-agent too (research, individually
|
|
977
|
+
* runnable file agents). A nested sub-agent carries no `subAgentDescriptors`
|
|
978
|
+
* (depth cap = 1).
|
|
979
|
+
*/
|
|
980
|
+
function renderDescriptor(agent: DiscoveredAgent, indent: string): string {
|
|
981
|
+
const optional: string[] = []
|
|
982
|
+
if (agent.subAgentsContent.length > 0) {
|
|
983
|
+
const nested = agent.subAgentsContent
|
|
984
|
+
.map((sub) => renderDescriptor(sub, `${indent} `))
|
|
985
|
+
.join('\n')
|
|
986
|
+
optional.push(`${indent} subAgentDescriptors: [\n${nested}\n${indent} ],`)
|
|
987
|
+
}
|
|
988
|
+
if (agent.maxSteps != null) optional.push(`${indent} maxSteps: ${agent.maxSteps},`)
|
|
989
|
+
if (agent.provider != null) optional.push(`${indent} provider: ${JSON.stringify(agent.provider)},`)
|
|
990
|
+
if (agent.model != null) optional.push(`${indent} model: ${JSON.stringify(agent.model)},`)
|
|
991
|
+
if (agent.sampleInput !== undefined) {
|
|
992
|
+
optional.push(`${indent} sampleInput: ${JSON.stringify(agent.sampleInput)},`)
|
|
993
|
+
}
|
|
994
|
+
if (agent.facts !== undefined) {
|
|
995
|
+
optional.push(`${indent} facts: ${JSON.stringify(agent.facts)},`)
|
|
996
|
+
}
|
|
997
|
+
const skillsContent = agent.skillsContent.map((skill) => ({
|
|
998
|
+
id: skill.id,
|
|
999
|
+
instructions: skill.instructions,
|
|
1000
|
+
...(skill.template != null ? { template: skill.template } : {}),
|
|
1001
|
+
examples: skill.examples,
|
|
1002
|
+
tools: skill.tools,
|
|
1003
|
+
...(skill.scripts.length > 0 ? { scripts: skill.scripts } : {}),
|
|
1004
|
+
}))
|
|
1005
|
+
return [
|
|
1006
|
+
`${indent}{`,
|
|
1007
|
+
`${indent} id: ${JSON.stringify(agent.id)},`,
|
|
1008
|
+
`${indent} moduleId: ${JSON.stringify(agent.moduleId)},`,
|
|
1009
|
+
`${indent} label: ${JSON.stringify(agent.label)},`,
|
|
1010
|
+
`${indent} description: ${JSON.stringify(agent.description)},`,
|
|
1011
|
+
`${indent} instructions: ${JSON.stringify(agent.instructions)},`,
|
|
1012
|
+
`${indent} resultKind: ${JSON.stringify(agent.resultKind)},`,
|
|
1013
|
+
...(agent.outcomeSchema ? [`${indent} outcomeSchema: ${JSON.stringify(agent.outcomeSchema)},`] : []),
|
|
1014
|
+
`${indent} tools: ${JSON.stringify(agent.tools)},`,
|
|
1015
|
+
`${indent} skills: ${JSON.stringify(agent.skills)},`,
|
|
1016
|
+
`${indent} subAgents: ${JSON.stringify(agent.subAgents)},`,
|
|
1017
|
+
`${indent} openCodeAgentName: ${JSON.stringify(agent.openCodeAgentName)},`,
|
|
1018
|
+
`${indent} skillsContent: ${JSON.stringify(skillsContent)},`,
|
|
1019
|
+
`${indent} tokenUsage: ${JSON.stringify(agent.tokenUsage)},`,
|
|
1020
|
+
`${indent} sourceFiles: ${JSON.stringify(agent.sourceFiles)},`,
|
|
1021
|
+
...optional,
|
|
1022
|
+
`${indent}},`,
|
|
1023
|
+
].join('\n')
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function renderManifest(agents: DiscoveredAgent[]): string {
|
|
1027
|
+
const descriptors = agents.map((agent) => renderDescriptor(agent, ' ')).join('\n')
|
|
1028
|
+
|
|
1029
|
+
return `// AUTO-GENERATED by mercato generate registry — DO NOT EDIT BY HAND.
|
|
1030
|
+
//
|
|
1031
|
+
// Committed, generator-owned manifest of file-defined (OpenCode) agents,
|
|
1032
|
+
// discovered from \`agents/<id>/\` directories across every enabled module. It
|
|
1033
|
+
// stores PLAIN data (raw JSON-Schema, not a Zod instance) so this file is pure
|
|
1034
|
+
// data and travels with the repo (survives \`yarn clean-generated\`).
|
|
1035
|
+
// \`ensureAgentsLoaded()\` recompiles each \`outcomeSchema\` to Zod via
|
|
1036
|
+
// \`compileOutcome\` at load time and registers it with \`runtime:'opencode'\`.
|
|
1037
|
+
//
|
|
1038
|
+
// Regenerate with \`yarn generate\`.
|
|
1039
|
+
import type { JsonSchemaNode, OutcomeKind } from '../lib/sdk/outcomeSchema'
|
|
1040
|
+
import type { AgentTokenUsage, FileAgentFile } from '../lib/tokens/types'
|
|
1041
|
+
|
|
1042
|
+
export type FileAgentScript = {
|
|
1043
|
+
name: string
|
|
1044
|
+
source: string
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export type FileAgentSkillContent = {
|
|
1048
|
+
id: string
|
|
1049
|
+
instructions: string
|
|
1050
|
+
template?: string
|
|
1051
|
+
examples: string[]
|
|
1052
|
+
tools: string[]
|
|
1053
|
+
/**
|
|
1054
|
+
* Sandboxed helper scripts (Phase 5). Carried as plain source; run server-side
|
|
1055
|
+
* in the Code Mode \`isolated-vm\` sandbox via the \`run_skill_script\` MCP tool.
|
|
1056
|
+
* Never copied to the OpenCode container. The synthetic skill id
|
|
1057
|
+
* \`__agent_tools__\` carries an agent's LOCAL \`tools/*.ts\` sources.
|
|
1058
|
+
*/
|
|
1059
|
+
scripts?: FileAgentScript[]
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export type FileAgentFact = {
|
|
1063
|
+
/** Human label shown in the Caseload facts grid. */
|
|
1064
|
+
label: string
|
|
1065
|
+
/** Where to resolve the value: run input, proposal payload, or run output. */
|
|
1066
|
+
source: 'input' | 'payload' | 'output'
|
|
1067
|
+
/** Dot-path into the source (array indexes allowed), e.g. "actions.0.payload.amount". */
|
|
1068
|
+
path: string
|
|
1069
|
+
format?: 'text' | 'number' | 'boolean' | 'percent'
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export type FileAgentDescriptor = {
|
|
1073
|
+
id: string
|
|
1074
|
+
moduleId: string
|
|
1075
|
+
label: string
|
|
1076
|
+
description: string
|
|
1077
|
+
instructions: string
|
|
1078
|
+
resultKind: OutcomeKind
|
|
1079
|
+
outcomeSchema?: JsonSchemaNode
|
|
1080
|
+
tools: string[]
|
|
1081
|
+
skills: string[]
|
|
1082
|
+
subAgents: string[]
|
|
1083
|
+
openCodeAgentName: string
|
|
1084
|
+
skillsContent?: FileAgentSkillContent[]
|
|
1085
|
+
/**
|
|
1086
|
+
* Baked token-usage estimate of the agent's construction files (AGENT.md,
|
|
1087
|
+
* OUTCOME.md, skills, tools, sub-agents), counted with the shared
|
|
1088
|
+
* \`o200k_base\` tokenizer. Surfaced on the Agent detail page and the
|
|
1089
|
+
* \`agent_orchestrator token-usage\` CLI. An estimate, not an exact count.
|
|
1090
|
+
*/
|
|
1091
|
+
tokenUsage?: AgentTokenUsage
|
|
1092
|
+
/**
|
|
1093
|
+
* Baked raw content of every construction file (AGENT.md, OUTCOME.md, skills,
|
|
1094
|
+
* tools, sub-agents, and auxiliary SAMPLE.json/FACTS.json), for the read-only
|
|
1095
|
+
* Files tab. Each carries its \`o200k_base\` token count; paths are relative to
|
|
1096
|
+
* the agent root (sub-agent files prefixed \`sub-agents/<id>/\`). Baked so the
|
|
1097
|
+
* runtime never reads agent dirs from disk.
|
|
1098
|
+
*/
|
|
1099
|
+
sourceFiles?: FileAgentFile[]
|
|
1100
|
+
/**
|
|
1101
|
+
* Nested descriptors for this agent's sub-agents (Phase 4). Each is an
|
|
1102
|
+
* research-kind, non-delegating file agent registered individually (depth cap =
|
|
1103
|
+
* 1). Absent for agents without sub-agents and for sub-agents themselves.
|
|
1104
|
+
*/
|
|
1105
|
+
subAgentDescriptors?: FileAgentDescriptor[]
|
|
1106
|
+
maxSteps?: number
|
|
1107
|
+
provider?: string
|
|
1108
|
+
model?: string
|
|
1109
|
+
/**
|
|
1110
|
+
* Optional example \`input\` for the Playground "Insert sample" button, read
|
|
1111
|
+
* from \`agents/<id>/SAMPLE.json\`. Any JSON value the agent accepts as input.
|
|
1112
|
+
*/
|
|
1113
|
+
sampleInput?: unknown
|
|
1114
|
+
/**
|
|
1115
|
+
* Optional fact declarations for the Caseload decision panel, read from
|
|
1116
|
+
* \`agents/<id>/FACTS.json\`. Each maps a labelled dot-path into the run
|
|
1117
|
+
* input / proposal payload / run output.
|
|
1118
|
+
*/
|
|
1119
|
+
facts?: FileAgentFact[]
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export const fileAgentDescriptors: FileAgentDescriptor[] = [${
|
|
1123
|
+
descriptors ? `\n${descriptors}\n` : ''
|
|
1124
|
+
}]
|
|
1125
|
+
`
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export function createAgentFilesExtension(): GeneratorExtension {
|
|
1129
|
+
const discovered: DiscoveredAgent[] = []
|
|
1130
|
+
const seenIds = new Set<string>()
|
|
1131
|
+
let repoRoot: string | null = null
|
|
1132
|
+
let sawOrchestratorModule = false
|
|
1133
|
+
|
|
1134
|
+
function scanAgentsTree(moduleId: string, baseDir: string): void {
|
|
1135
|
+
for (const dir of listAgentDirs(baseDir)) {
|
|
1136
|
+
const agentMdPath = path.join(dir, 'AGENT.md')
|
|
1137
|
+
const outcomePath = path.join(dir, 'OUTCOME.md')
|
|
1138
|
+
// Only treat a dir as an agent if at least one convention file exists.
|
|
1139
|
+
const hasAgentMd = fs.existsSync(agentMdPath)
|
|
1140
|
+
const hasOutcome = fs.existsSync(outcomePath)
|
|
1141
|
+
if (!hasAgentMd && !hasOutcome) continue
|
|
1142
|
+
if (!hasAgentMd || !hasOutcome) {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`[internal] malformed file agent at ${dir}: both AGENT.md and OUTCOME.md are required`,
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
const agent = parseAgentMarkdown(fs.readFileSync(agentMdPath, 'utf8'))
|
|
1148
|
+
if (!agent) {
|
|
1149
|
+
throw new Error(`[internal] malformed AGENT.md at ${dir}: missing id/label/description`)
|
|
1150
|
+
}
|
|
1151
|
+
const outcome = parseOutcomeMarkdown(fs.readFileSync(outcomePath, 'utf8'))
|
|
1152
|
+
if (!outcome) {
|
|
1153
|
+
throw new Error(`[internal] malformed OUTCOME.md at ${dir}: missing kind or JSON-Schema block`)
|
|
1154
|
+
}
|
|
1155
|
+
if (outcome.schema) assertOutcomeSchemaSupported(outcome.schema, dir)
|
|
1156
|
+
// The id is the registry/dup key AND the OpenCode `agent` message field —
|
|
1157
|
+
// constrain it to a safe charset (module.entity-style: lowercase alnum + . _ -).
|
|
1158
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(agent.id!)) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`[internal] invalid agent id "${agent.id}" at ${dir}: use lowercase [a-z0-9._-] (e.g. "module.agent")`,
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
if (seenIds.has(agent.id!)) {
|
|
1164
|
+
// Surface accidental collisions at generate time instead of silently
|
|
1165
|
+
// dropping the later agent (the registry would only skip it at load).
|
|
1166
|
+
console.warn(`[internal] duplicate file-agent id "${agent.id}" at ${dir}; keeping the first, skipping this one.`)
|
|
1167
|
+
continue
|
|
1168
|
+
}
|
|
1169
|
+
seenIds.add(agent.id!)
|
|
1170
|
+
// Phase 3: resolve agent-local skills and UNION their read-only tools into
|
|
1171
|
+
// the agent allowlist (deduped), matching the loader.
|
|
1172
|
+
const skillsContent = discoverAgentSkills(dir, agent.skills)
|
|
1173
|
+
const skillTools = skillsContent.flatMap((skill) => skill.tools)
|
|
1174
|
+
// Phase 5: local tool files — reference ids union into the allowlist; local
|
|
1175
|
+
// sandboxed tool sources register under the synthetic `__agent_tools__` skill.
|
|
1176
|
+
const toolFiles = discoverToolFiles(dir)
|
|
1177
|
+
const effectiveSkillsContent =
|
|
1178
|
+
toolFiles.scripts.length > 0
|
|
1179
|
+
? [
|
|
1180
|
+
...skillsContent,
|
|
1181
|
+
{
|
|
1182
|
+
id: AGENT_TOOLS_SKILL_ID,
|
|
1183
|
+
description: '',
|
|
1184
|
+
instructions: '',
|
|
1185
|
+
examples: [],
|
|
1186
|
+
tools: [],
|
|
1187
|
+
scripts: toolFiles.scripts,
|
|
1188
|
+
openCodeSkillName: sanitizeSkillName(AGENT_TOOLS_SKILL_ID),
|
|
1189
|
+
},
|
|
1190
|
+
]
|
|
1191
|
+
: skillsContent
|
|
1192
|
+
const effectiveTools = Array.from(
|
|
1193
|
+
new Set([...agent.tools, ...skillTools, ...toolFiles.refs]),
|
|
1194
|
+
)
|
|
1195
|
+
// Phase 4: discover + validate sub-agents (throws on a malformed/proposal/
|
|
1196
|
+
// self-delegating sub-agent).
|
|
1197
|
+
const subAgentsContent = discoverSubAgents(dir)
|
|
1198
|
+
discovered.push({
|
|
1199
|
+
moduleId,
|
|
1200
|
+
dir,
|
|
1201
|
+
id: agent.id!,
|
|
1202
|
+
label: agent.label!,
|
|
1203
|
+
description: agent.description!,
|
|
1204
|
+
instructions: agent.instructions,
|
|
1205
|
+
resultKind: outcome.kind,
|
|
1206
|
+
outcomeSchema: outcome.schema,
|
|
1207
|
+
outcomeProse: outcome.prose,
|
|
1208
|
+
tools: effectiveTools,
|
|
1209
|
+
skills: agent.skills,
|
|
1210
|
+
subAgents: agent.subAgents,
|
|
1211
|
+
openCodeAgentName: sanitizeAgentName(agent.id!),
|
|
1212
|
+
skillsContent: effectiveSkillsContent,
|
|
1213
|
+
subAgentsContent,
|
|
1214
|
+
mode: 'primary',
|
|
1215
|
+
maxSteps: agent.maxSteps,
|
|
1216
|
+
provider: agent.provider,
|
|
1217
|
+
model: agent.model,
|
|
1218
|
+
sampleInput: discoverSampleInput(dir),
|
|
1219
|
+
facts: discoverFacts(dir),
|
|
1220
|
+
tokenUsage: discoverAgentTokenUsage(dir),
|
|
1221
|
+
sourceFiles: collectAgentFiles(dir),
|
|
1222
|
+
})
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return {
|
|
1227
|
+
id: 'registry.agent-files',
|
|
1228
|
+
outputFiles: [],
|
|
1229
|
+
scanModule(ctx: ModuleScanContext) {
|
|
1230
|
+
if (ctx.moduleId === ORCHESTRATOR_MODULE_ID) sawOrchestratorModule = true
|
|
1231
|
+
if (!repoRoot) {
|
|
1232
|
+
repoRoot = findRepoRoot(ctx.roots.pkgBase) ?? findRepoRoot(ctx.roots.appBase)
|
|
1233
|
+
}
|
|
1234
|
+
const pkgScanBase = resolveStandaloneSourceMirrorBase(ctx.roots.pkgBase) ?? ctx.roots.pkgBase
|
|
1235
|
+
scanAgentsTree(ctx.moduleId, path.join(pkgScanBase, 'agents'))
|
|
1236
|
+
scanAgentsTree(ctx.moduleId, path.join(ctx.roots.appBase, 'agents'))
|
|
1237
|
+
},
|
|
1238
|
+
generateOutput() {
|
|
1239
|
+
const sorted = [...discovered].sort((a, b) => a.id.localeCompare(b.id))
|
|
1240
|
+
|
|
1241
|
+
if (!repoRoot) {
|
|
1242
|
+
// No module roots resolved to a repo (e.g. an isolated unit test). Skip
|
|
1243
|
+
// the fs side effect; the empty-Map return keeps the contract intact.
|
|
1244
|
+
return new Map<string, string>()
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (!sawOrchestratorModule && sorted.length === 0) {
|
|
1248
|
+
// Neither the orchestrator (the manifest's only consumer) nor any agent
|
|
1249
|
+
// module is part of this run — the enterprise agents flag is off. The
|
|
1250
|
+
// committed manifest and `docker/opencode/**` files belong to modules
|
|
1251
|
+
// that are merely switched off, so pruning them here would delete
|
|
1252
|
+
// tracked artifacts on every `yarn generate`. Leave the tree untouched.
|
|
1253
|
+
return new Map<string, string>()
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const manifestPath = path.join(
|
|
1257
|
+
repoRoot,
|
|
1258
|
+
'packages',
|
|
1259
|
+
'enterprise',
|
|
1260
|
+
'src',
|
|
1261
|
+
'modules',
|
|
1262
|
+
'agent_orchestrator',
|
|
1263
|
+
'generated',
|
|
1264
|
+
'file-agents.generated.ts',
|
|
1265
|
+
)
|
|
1266
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
1267
|
+
fs.writeFileSync(manifestPath, renderManifest(sorted), 'utf8')
|
|
1268
|
+
|
|
1269
|
+
const dockerAgentsDir = path.join(repoRoot, 'docker', 'opencode', 'agents')
|
|
1270
|
+
fs.mkdirSync(dockerAgentsDir, { recursive: true })
|
|
1271
|
+
const desiredFiles = new Map<string, string>()
|
|
1272
|
+
for (const agent of sorted) {
|
|
1273
|
+
desiredFiles.set(`${agent.openCodeAgentName}.md`, renderOpenCodeAgentFile(agent))
|
|
1274
|
+
// Phase 4: emit each sub-agent as its own `mode: subagent` file so
|
|
1275
|
+
// OpenCode can reach it via the primary's whitelisted `task` tool.
|
|
1276
|
+
for (const sub of agent.subAgentsContent) {
|
|
1277
|
+
desiredFiles.set(`${sub.openCodeAgentName}.md`, renderOpenCodeAgentFile(sub))
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Idempotent: remove stale generated agent files, write the current set.
|
|
1281
|
+
for (const existing of fs.existsSync(dockerAgentsDir) ? fs.readdirSync(dockerAgentsDir) : []) {
|
|
1282
|
+
if (existing.endsWith('.md') && !desiredFiles.has(existing)) {
|
|
1283
|
+
fs.rmSync(path.join(dockerAgentsDir, existing))
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
for (const [fileName, content] of desiredFiles) {
|
|
1287
|
+
fs.writeFileSync(path.join(dockerAgentsDir, fileName), content, 'utf8')
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Phase 3: emit NATIVE OpenCode skill files (one dir per skill name) under
|
|
1291
|
+
// `docker/opencode/skills/<sanitized-skill-name>/SKILL.md`. Idempotent:
|
|
1292
|
+
// remove stale skill dirs not in the current desired set. Sub-agents
|
|
1293
|
+
// (Phase 4) may carry their own skills too, so flatten them in.
|
|
1294
|
+
const allAgents = sorted.flatMap((agent) => [agent, ...agent.subAgentsContent])
|
|
1295
|
+
const dockerSkillsDir = path.join(repoRoot, 'docker', 'opencode', 'skills')
|
|
1296
|
+
// The synthetic `__agent_tools__` skill only carries an agent's local
|
|
1297
|
+
// `tools/*.ts` sources (run via `run_skill_script`); it has no instructions
|
|
1298
|
+
// and MUST NOT be emitted as a native OpenCode skill (OpenCode requires a
|
|
1299
|
+
// non-empty `description`, and it is not a progressive-disclosure skill).
|
|
1300
|
+
const isNativeSkill = (skill: { id: string }): boolean => skill.id !== AGENT_TOOLS_SKILL_ID
|
|
1301
|
+
const desiredSkillNames = new Set<string>()
|
|
1302
|
+
for (const agent of allAgents) {
|
|
1303
|
+
for (const skill of agent.skillsContent) {
|
|
1304
|
+
if (isNativeSkill(skill)) desiredSkillNames.add(skill.openCodeSkillName)
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (desiredSkillNames.size > 0) fs.mkdirSync(dockerSkillsDir, { recursive: true })
|
|
1308
|
+
if (fs.existsSync(dockerSkillsDir)) {
|
|
1309
|
+
for (const existing of fs.readdirSync(dockerSkillsDir, { withFileTypes: true })) {
|
|
1310
|
+
if (existing.isDirectory() && !desiredSkillNames.has(existing.name)) {
|
|
1311
|
+
fs.rmSync(path.join(dockerSkillsDir, existing.name), { recursive: true, force: true })
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const renderedSkillNames = new Set<string>()
|
|
1316
|
+
for (const agent of allAgents) {
|
|
1317
|
+
for (const skill of agent.skillsContent) {
|
|
1318
|
+
if (!isNativeSkill(skill)) continue
|
|
1319
|
+
// A skill name may be shared across agents; the first rendering wins
|
|
1320
|
+
// (content is keyed by name, deterministic by sorted agent order).
|
|
1321
|
+
if (renderedSkillNames.has(skill.openCodeSkillName)) continue
|
|
1322
|
+
renderedSkillNames.add(skill.openCodeSkillName)
|
|
1323
|
+
const skillDir = path.join(dockerSkillsDir, skill.openCodeSkillName)
|
|
1324
|
+
fs.mkdirSync(skillDir, { recursive: true })
|
|
1325
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), renderOpenCodeSkillFile(skill), 'utf8')
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return new Map<string, string>()
|
|
1330
|
+
},
|
|
1331
|
+
}
|
|
1332
|
+
}
|