@moreih29/nexus-core 0.12.0 → 0.13.0

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 (210) hide show
  1. package/README.md +48 -63
  2. package/assets/agents/architect/body.ko.md +177 -0
  3. package/{agents → assets/agents}/architect/body.md +16 -0
  4. package/assets/agents/designer/body.ko.md +125 -0
  5. package/{agents → assets/agents}/designer/body.md +16 -0
  6. package/assets/agents/engineer/body.ko.md +106 -0
  7. package/{agents → assets/agents}/engineer/body.md +14 -0
  8. package/assets/agents/lead/body.ko.md +70 -0
  9. package/assets/agents/lead/body.md +70 -0
  10. package/assets/agents/postdoc/body.ko.md +122 -0
  11. package/{agents → assets/agents}/postdoc/body.md +16 -0
  12. package/assets/agents/researcher/body.ko.md +137 -0
  13. package/{agents → assets/agents}/researcher/body.md +15 -0
  14. package/assets/agents/reviewer/body.ko.md +138 -0
  15. package/{agents → assets/agents}/reviewer/body.md +15 -0
  16. package/assets/agents/strategist/body.ko.md +116 -0
  17. package/{agents → assets/agents}/strategist/body.md +16 -0
  18. package/assets/agents/tester/body.ko.md +195 -0
  19. package/{agents → assets/agents}/tester/body.md +15 -0
  20. package/assets/agents/writer/body.ko.md +122 -0
  21. package/{agents → assets/agents}/writer/body.md +14 -0
  22. package/assets/capability-matrix.yml +198 -0
  23. package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
  24. package/assets/hooks/agent-bootstrap/handler.ts +119 -0
  25. package/assets/hooks/agent-bootstrap/meta.yml +10 -0
  26. package/assets/hooks/agent-finalize/handler.test.ts +368 -0
  27. package/assets/hooks/agent-finalize/handler.ts +76 -0
  28. package/assets/hooks/agent-finalize/meta.yml +10 -0
  29. package/assets/hooks/capability-matrix.yml +313 -0
  30. package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
  31. package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
  32. package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
  33. package/assets/hooks/prompt-router/handler.test.ts +801 -0
  34. package/assets/hooks/prompt-router/handler.ts +261 -0
  35. package/assets/hooks/prompt-router/meta.yml +11 -0
  36. package/assets/hooks/session-init/handler.test.ts +274 -0
  37. package/assets/hooks/session-init/handler.ts +30 -0
  38. package/assets/hooks/session-init/meta.yml +9 -0
  39. package/assets/lsp-servers.json +55 -0
  40. package/assets/schema/lsp-servers.schema.json +67 -0
  41. package/assets/skills/nx-init/body.ko.md +197 -0
  42. package/{skills → assets/skills}/nx-init/body.md +11 -0
  43. package/assets/skills/nx-plan/body.ko.md +361 -0
  44. package/{skills → assets/skills}/nx-plan/body.md +13 -0
  45. package/assets/skills/nx-run/body.ko.md +161 -0
  46. package/{skills → assets/skills}/nx-run/body.md +11 -0
  47. package/assets/skills/nx-sync/body.ko.md +92 -0
  48. package/{skills → assets/skills}/nx-sync/body.md +10 -0
  49. package/assets/tools/tool-name-map.yml +353 -0
  50. package/dist/hooks/opencode-mount.d.ts +35 -0
  51. package/dist/hooks/opencode-mount.d.ts.map +1 -0
  52. package/dist/hooks/opencode-mount.js +332 -0
  53. package/dist/hooks/opencode-mount.js.map +1 -0
  54. package/dist/hooks/runtime.d.ts +37 -0
  55. package/dist/hooks/runtime.d.ts.map +1 -0
  56. package/dist/hooks/runtime.js +274 -0
  57. package/dist/hooks/runtime.js.map +1 -0
  58. package/dist/hooks/types.d.ts +196 -0
  59. package/dist/hooks/types.d.ts.map +1 -0
  60. package/dist/hooks/types.js +85 -0
  61. package/dist/hooks/types.js.map +1 -0
  62. package/dist/lsp/cache.d.ts +9 -0
  63. package/dist/lsp/cache.d.ts.map +1 -0
  64. package/dist/lsp/cache.js +216 -0
  65. package/dist/lsp/cache.js.map +1 -0
  66. package/dist/lsp/client.d.ts +24 -0
  67. package/dist/lsp/client.d.ts.map +1 -0
  68. package/dist/lsp/client.js +166 -0
  69. package/dist/lsp/client.js.map +1 -0
  70. package/dist/lsp/detect.d.ts +77 -0
  71. package/dist/lsp/detect.d.ts.map +1 -0
  72. package/dist/lsp/detect.js +116 -0
  73. package/dist/lsp/detect.js.map +1 -0
  74. package/dist/mcp/server.d.ts +5 -0
  75. package/dist/mcp/server.d.ts.map +1 -0
  76. package/dist/mcp/server.js +34 -0
  77. package/dist/mcp/server.js.map +1 -0
  78. package/dist/mcp/tools/artifact.d.ts +4 -0
  79. package/dist/mcp/tools/artifact.d.ts.map +1 -0
  80. package/dist/mcp/tools/artifact.js +36 -0
  81. package/dist/mcp/tools/artifact.js.map +1 -0
  82. package/dist/mcp/tools/history.d.ts +3 -0
  83. package/dist/mcp/tools/history.d.ts.map +1 -0
  84. package/dist/mcp/tools/history.js +29 -0
  85. package/dist/mcp/tools/history.js.map +1 -0
  86. package/dist/mcp/tools/lsp.d.ts +13 -0
  87. package/dist/mcp/tools/lsp.d.ts.map +1 -0
  88. package/dist/mcp/tools/lsp.js +225 -0
  89. package/dist/mcp/tools/lsp.js.map +1 -0
  90. package/dist/mcp/tools/plan.d.ts +3 -0
  91. package/dist/mcp/tools/plan.d.ts.map +1 -0
  92. package/dist/mcp/tools/plan.js +317 -0
  93. package/dist/mcp/tools/plan.js.map +1 -0
  94. package/dist/mcp/tools/task.d.ts +3 -0
  95. package/dist/mcp/tools/task.d.ts.map +1 -0
  96. package/dist/mcp/tools/task.js +252 -0
  97. package/dist/mcp/tools/task.js.map +1 -0
  98. package/dist/shared/invocations.d.ts +74 -0
  99. package/dist/shared/invocations.d.ts.map +1 -0
  100. package/dist/shared/invocations.js +247 -0
  101. package/dist/shared/invocations.js.map +1 -0
  102. package/dist/shared/json-store.d.ts +37 -0
  103. package/dist/shared/json-store.d.ts.map +1 -0
  104. package/dist/shared/json-store.js +163 -0
  105. package/dist/shared/json-store.js.map +1 -0
  106. package/dist/shared/mcp-utils.d.ts +3 -0
  107. package/dist/shared/mcp-utils.d.ts.map +1 -0
  108. package/dist/shared/mcp-utils.js +6 -0
  109. package/dist/shared/mcp-utils.js.map +1 -0
  110. package/dist/shared/paths.d.ts +21 -0
  111. package/dist/shared/paths.d.ts.map +1 -0
  112. package/dist/shared/paths.js +81 -0
  113. package/dist/shared/paths.js.map +1 -0
  114. package/dist/shared/tool-log.d.ts +8 -0
  115. package/dist/shared/tool-log.d.ts.map +1 -0
  116. package/dist/shared/tool-log.js +22 -0
  117. package/dist/shared/tool-log.js.map +1 -0
  118. package/dist/types/state.d.ts +862 -0
  119. package/dist/types/state.d.ts.map +1 -0
  120. package/dist/types/state.js +66 -0
  121. package/dist/types/state.js.map +1 -0
  122. package/docs/consuming/codex-lead-merge.md +106 -0
  123. package/docs/plugin-guide.md +360 -0
  124. package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
  125. package/docs/plugin-template/claude/README.md +110 -0
  126. package/docs/plugin-template/claude/package.json +16 -0
  127. package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
  128. package/docs/plugin-template/codex/README.md +147 -0
  129. package/docs/plugin-template/codex/package.json +17 -0
  130. package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
  131. package/docs/plugin-template/opencode/README.md +121 -0
  132. package/docs/plugin-template/opencode/package.json +25 -0
  133. package/package.json +21 -21
  134. package/scripts/build-agents.test.ts +1279 -0
  135. package/scripts/build-agents.ts +978 -0
  136. package/scripts/build-hooks.test.ts +1385 -0
  137. package/scripts/build-hooks.ts +584 -0
  138. package/scripts/cli.test.ts +367 -0
  139. package/scripts/cli.ts +547 -0
  140. package/agents/architect/meta.yml +0 -13
  141. package/agents/designer/meta.yml +0 -13
  142. package/agents/engineer/meta.yml +0 -11
  143. package/agents/postdoc/meta.yml +0 -13
  144. package/agents/researcher/meta.yml +0 -12
  145. package/agents/reviewer/meta.yml +0 -12
  146. package/agents/strategist/meta.yml +0 -13
  147. package/agents/tester/meta.yml +0 -12
  148. package/agents/writer/meta.yml +0 -11
  149. package/conformance/README.md +0 -311
  150. package/conformance/examples/plan.extension.schema.example.json +0 -25
  151. package/conformance/lifecycle/README.md +0 -48
  152. package/conformance/lifecycle/agent-complete.json +0 -44
  153. package/conformance/lifecycle/agent-resume.json +0 -43
  154. package/conformance/lifecycle/agent-spawn.json +0 -36
  155. package/conformance/lifecycle/memory-access-record.json +0 -27
  156. package/conformance/lifecycle/session-end.json +0 -48
  157. package/conformance/scenarios/full-plan-cycle.json +0 -147
  158. package/conformance/scenarios/task-deps-ordering.json +0 -95
  159. package/conformance/schema/fixture.schema.json +0 -354
  160. package/conformance/state-schemas/agent-tracker.schema.json +0 -63
  161. package/conformance/state-schemas/history.schema.json +0 -134
  162. package/conformance/state-schemas/memory-access.schema.json +0 -36
  163. package/conformance/state-schemas/plan.schema.json +0 -77
  164. package/conformance/state-schemas/tasks.schema.json +0 -98
  165. package/conformance/tools/artifact-write.json +0 -97
  166. package/conformance/tools/context.json +0 -172
  167. package/conformance/tools/history-search.json +0 -219
  168. package/conformance/tools/plan-decide.json +0 -139
  169. package/conformance/tools/plan-start.json +0 -81
  170. package/conformance/tools/plan-status.json +0 -127
  171. package/conformance/tools/plan-update.json +0 -341
  172. package/conformance/tools/task-add.json +0 -156
  173. package/conformance/tools/task-close.json +0 -161
  174. package/conformance/tools/task-list.json +0 -177
  175. package/conformance/tools/task-update.json +0 -167
  176. package/docs/behavioral-contracts.md +0 -145
  177. package/docs/consumer-implementation-guide.md +0 -840
  178. package/docs/memory-lifecycle-contract.md +0 -119
  179. package/docs/nexus-layout.md +0 -224
  180. package/docs/nexus-outputs-contract.md +0 -344
  181. package/docs/nexus-state-overview.md +0 -170
  182. package/docs/nexus-tools-contract.md +0 -438
  183. package/manifest.json +0 -448
  184. package/schema/README.md +0 -69
  185. package/schema/agent.schema.json +0 -23
  186. package/schema/common.schema.json +0 -17
  187. package/schema/manifest.schema.json +0 -78
  188. package/schema/memory-policy.schema.json +0 -98
  189. package/schema/skill.schema.json +0 -54
  190. package/schema/task-exceptions.schema.json +0 -40
  191. package/schema/vocabulary.schema.json +0 -167
  192. package/scripts/.gitkeep +0 -0
  193. package/scripts/conformance-coverage.ts +0 -466
  194. package/scripts/import-from-claude-nexus.ts +0 -403
  195. package/scripts/lib/frontmatter.ts +0 -71
  196. package/scripts/lib/lint.ts +0 -348
  197. package/scripts/lib/structure.ts +0 -159
  198. package/scripts/lib/validate.ts +0 -796
  199. package/scripts/validate.ts +0 -90
  200. package/skills/nx-init/meta.yml +0 -8
  201. package/skills/nx-plan/meta.yml +0 -10
  202. package/skills/nx-run/meta.yml +0 -8
  203. package/skills/nx-sync/meta.yml +0 -7
  204. package/vocabulary/capabilities.yml +0 -65
  205. package/vocabulary/categories.yml +0 -11
  206. package/vocabulary/invocations.yml +0 -147
  207. package/vocabulary/memory_policy.yml +0 -88
  208. package/vocabulary/resume-tiers.yml +0 -11
  209. package/vocabulary/tags.yml +0 -60
  210. package/vocabulary/task-exceptions.yml +0 -29
@@ -0,0 +1,978 @@
1
+ /**
2
+ * scripts/build-agents.ts
3
+ *
4
+ * Build pipeline for nexus agents and skills.
5
+ *
6
+ * Inputs:
7
+ * assets/agents/<n>/body.md × 9 agents
8
+ * assets/skills/<n>/body.md × 4 skills
9
+ * assets/capability-matrix.yml
10
+ * assets/tools/tool-name-map.yml (invocations section)
11
+ *
12
+ * Outputs per harness:
13
+ * dist/claude/
14
+ * .claude-plugin/plugin.json (Template — skip if exists, --force to overwrite)
15
+ * .claude-plugin/marketplace.json (Template — skip if exists, --force to overwrite)
16
+ * agents/<n>.md × N
17
+ * skills/<n>/SKILL.md × 4
18
+ * settings.json (Managed — primary agent injection, omitted if no primary)
19
+ *
20
+ * dist/opencode/
21
+ * package.json (Template — skip if exists)
22
+ * opencode.json.fragment (Managed — always overwrite)
23
+ * src/index.ts (Managed — always overwrite)
24
+ * src/agents/<n>.ts × N (Managed — always overwrite, mode:primary gets mode field)
25
+ * .opencode/skills/<n>/SKILL.md × 4 (Managed)
26
+ *
27
+ * dist/codex/
28
+ * plugin/.codex-plugin/plugin.json (Managed)
29
+ * plugin/skills/<n>/SKILL.md × 4 (Managed)
30
+ * agents/<n>.toml × N (Managed)
31
+ * prompts/<n>.md × N (Managed)
32
+ * install/config.fragment.toml (Managed)
33
+ * install/AGENTS.fragment.md (Managed — primary agents only, omitted if none)
34
+ *
35
+ * Overwrite policy:
36
+ * Managed paths — always overwrite (unless --dry-run)
37
+ * Template paths — skip if file exists (overwrite only with --force)
38
+ *
39
+ * CLI flags:
40
+ * --harness=claude|opencode|codex (default: all)
41
+ * --target=<dir> (default: dist/)
42
+ * --dry-run print affected files, no writes
43
+ * --force force Template file overwrite
44
+ * --strict error if Managed output has untracked modifications
45
+ * --only=<agent|skill name> restrict to a single asset
46
+ */
47
+
48
+ import {
49
+ readFileSync,
50
+ readdirSync,
51
+ existsSync,
52
+ mkdirSync,
53
+ writeFileSync,
54
+ statSync,
55
+ } from "node:fs";
56
+ import { join, resolve, dirname, basename } from "node:path";
57
+ import { fileURLToPath } from "node:url";
58
+ import { execSync } from "node:child_process";
59
+ import { parse as parseYaml } from "yaml";
60
+ import { expandInvocations } from "../src/shared/invocations.js";
61
+ import type { InvocationsMap, Harness } from "../src/shared/invocations.js";
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Constants
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const __dirname = dirname(fileURLToPath(import.meta.url));
68
+ export const ROOT = resolve(__dirname, "..");
69
+ export const AGENTS_DIR = join(ROOT, "assets/agents");
70
+ export const SKILLS_DIR = join(ROOT, "assets/skills");
71
+ export const CAPABILITY_MATRIX_PATH = join(ROOT, "assets/capability-matrix.yml");
72
+ export const TOOL_NAME_MAP_PATH = join(ROOT, "assets/tools/tool-name-map.yml");
73
+
74
+ const HARNESSES = ["claude", "opencode", "codex"] as const;
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Data shapes
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export interface AgentFrontmatter {
81
+ name: string;
82
+ description: string;
83
+ task?: string;
84
+ alias_ko?: string;
85
+ category: "how" | "do" | "check" | "lead";
86
+ mode?: "primary" | "subagent" | "all";
87
+ resume_tier?: "persistent" | "bounded" | "ephemeral";
88
+ model_tier: "high" | "standard" | "low";
89
+ capabilities: string[];
90
+ id: string;
91
+ // skill-specific fields
92
+ summary?: string;
93
+ triggers?: string[];
94
+ manual_only?: boolean;
95
+ harness_docs_refs?: string[];
96
+ }
97
+
98
+ export interface AssetEntry {
99
+ type: "agent" | "skill";
100
+ name: string;
101
+ frontmatter: AgentFrontmatter;
102
+ body: string; // raw body without frontmatter
103
+ bodyPath: string;
104
+ }
105
+
106
+ export interface CapabilityMatrixEntry {
107
+ claude?: {
108
+ disallowedTools?: string[];
109
+ };
110
+ opencode?: {
111
+ permission?: Record<string, string>;
112
+ };
113
+ codex?: {
114
+ sandbox_mode?: string | null;
115
+ disabled_tools?: string[];
116
+ };
117
+ }
118
+
119
+ export interface CapabilityMatrix {
120
+ capabilities: Record<string, CapabilityMatrixEntry>;
121
+ model_tier: Record<string, { claude: string; codex: string; opencode: string | null }>;
122
+ }
123
+
124
+ export interface ToolNameMapInvocations {
125
+ invocations: InvocationsMap;
126
+ }
127
+
128
+ export interface BuildOptions {
129
+ harnesses: Harness[];
130
+ targetDir: string;
131
+ dryRun: boolean;
132
+ force: boolean;
133
+ strict: boolean;
134
+ only?: string;
135
+ }
136
+
137
+ // Track dry-run affected files
138
+ const dryRunFiles: string[] = [];
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Frontmatter parsing
142
+ // ---------------------------------------------------------------------------
143
+
144
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
145
+
146
+ /**
147
+ * Split a body.md file into frontmatter object and body text.
148
+ * Throws on YAML parse failure.
149
+ */
150
+ export function parseFrontmatter(raw: string, filePath: string): { fm: AgentFrontmatter; body: string } {
151
+ const match = FRONTMATTER_RE.exec(raw);
152
+ if (!match) {
153
+ throw new Error(`[build-agents] Missing or malformed frontmatter in: ${filePath}`);
154
+ }
155
+
156
+ let fm: AgentFrontmatter;
157
+ try {
158
+ fm = parseYaml(match[1]!) as AgentFrontmatter;
159
+ } catch (err) {
160
+ throw new Error(
161
+ `[build-agents] YAML parse failure in frontmatter of: ${filePath}\n ${String(err)}`,
162
+ );
163
+ }
164
+
165
+ if (!fm.id || !fm.name) {
166
+ throw new Error(
167
+ `[build-agents] Missing required frontmatter fields (id, name) in: ${filePath}`,
168
+ );
169
+ }
170
+
171
+ const VALID_MODES = ["primary", "subagent", "all"] as const;
172
+ if (fm.mode !== undefined && !(VALID_MODES as readonly string[]).includes(fm.mode)) {
173
+ throw new Error(
174
+ `[build-agents] Invalid mode "${fm.mode}" in: ${filePath}. Valid values: ${VALID_MODES.join(", ")}`,
175
+ );
176
+ }
177
+
178
+ return { fm, body: (match[2] ?? "").trimStart() };
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Stage 1: Load assets
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /**
186
+ * Load all agents and skills from assets/, whitelisting only body.md.
187
+ */
188
+ export function loadAssets(opts?: { only?: string }): AssetEntry[] {
189
+ const entries: AssetEntry[] = [];
190
+
191
+ // Load agents
192
+ if (existsSync(AGENTS_DIR)) {
193
+ for (const entry of readdirSync(AGENTS_DIR, { withFileTypes: true })) {
194
+ if (!entry.isDirectory()) continue;
195
+ if (opts?.only && entry.name !== opts.only) continue;
196
+
197
+ const bodyPath = join(AGENTS_DIR, entry.name, "body.md");
198
+ if (!existsSync(bodyPath)) {
199
+ throw new Error(`[build-agents] Missing body.md for agent: ${entry.name}`);
200
+ }
201
+
202
+ const raw = readFileSync(bodyPath, "utf-8");
203
+ const { fm, body } = parseFrontmatter(raw, bodyPath);
204
+
205
+ entries.push({ type: "agent", name: entry.name, frontmatter: fm, body, bodyPath });
206
+ }
207
+ }
208
+
209
+ // Load skills
210
+ if (existsSync(SKILLS_DIR)) {
211
+ for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
212
+ if (!entry.isDirectory()) continue;
213
+ if (opts?.only && entry.name !== opts.only) continue;
214
+
215
+ const bodyPath = join(SKILLS_DIR, entry.name, "body.md");
216
+ if (!existsSync(bodyPath)) {
217
+ throw new Error(`[build-agents] Missing body.md for skill: ${entry.name}`);
218
+ }
219
+
220
+ const raw = readFileSync(bodyPath, "utf-8");
221
+ const { fm, body } = parseFrontmatter(raw, bodyPath);
222
+
223
+ entries.push({ type: "skill", name: entry.name, frontmatter: fm, body, bodyPath });
224
+ }
225
+ }
226
+
227
+ return entries;
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Stage 2: Load capability matrix
232
+ // ---------------------------------------------------------------------------
233
+
234
+ export function loadCapabilityMatrix(): CapabilityMatrix {
235
+ if (!existsSync(CAPABILITY_MATRIX_PATH)) {
236
+ throw new Error(`[build-agents] capability-matrix.yml not found at: ${CAPABILITY_MATRIX_PATH}`);
237
+ }
238
+ const raw = readFileSync(CAPABILITY_MATRIX_PATH, "utf-8");
239
+ return parseYaml(raw) as CapabilityMatrix;
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Stage 3: Load invocations
244
+ // ---------------------------------------------------------------------------
245
+
246
+ export function loadInvocations(): InvocationsMap {
247
+ if (!existsSync(TOOL_NAME_MAP_PATH)) {
248
+ throw new Error(`[build-agents] tool-name-map.yml not found at: ${TOOL_NAME_MAP_PATH}`);
249
+ }
250
+ const raw = readFileSync(TOOL_NAME_MAP_PATH, "utf-8");
251
+ const parsed = parseYaml(raw) as ToolNameMapInvocations;
252
+ if (!parsed.invocations) {
253
+ throw new Error(`[build-agents] tool-name-map.yml missing 'invocations' section`);
254
+ }
255
+ return parsed.invocations;
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Capability resolution helpers
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Collect all Claude disallowedTools for an agent based on its capabilities[].
264
+ */
265
+ export function resolveClaudeDisallowedTools(
266
+ capabilities: string[],
267
+ capMatrix: CapabilityMatrix,
268
+ ): string[] {
269
+ const tools: string[] = [];
270
+ for (const cap of capabilities) {
271
+ const entry = capMatrix.capabilities[cap];
272
+ if (!entry) {
273
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
274
+ }
275
+ if (entry.claude?.disallowedTools) {
276
+ for (const t of entry.claude.disallowedTools) {
277
+ if (!tools.includes(t)) tools.push(t);
278
+ }
279
+ }
280
+ }
281
+ return tools;
282
+ }
283
+
284
+ /**
285
+ * Collect merged OpenCode permission block for an agent.
286
+ */
287
+ export function resolveOpencodePermissions(
288
+ capabilities: string[],
289
+ capMatrix: CapabilityMatrix,
290
+ ): Record<string, string> {
291
+ const perms: Record<string, string> = {};
292
+ for (const cap of capabilities) {
293
+ const entry = capMatrix.capabilities[cap];
294
+ if (!entry) {
295
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
296
+ }
297
+ if (entry.opencode?.permission) {
298
+ Object.assign(perms, entry.opencode.permission);
299
+ }
300
+ }
301
+ return perms;
302
+ }
303
+
304
+ /**
305
+ * Resolve codex sandbox_mode and disabled_tools for an agent.
306
+ * sandbox_mode: take the most restrictive non-null value ("read-only" wins).
307
+ */
308
+ export function resolveCodexConfig(
309
+ capabilities: string[],
310
+ capMatrix: CapabilityMatrix,
311
+ ): { sandbox_mode: string | null; disabled_tools: string[] } {
312
+ let sandboxMode: string | null = null;
313
+ const disabledTools: string[] = [];
314
+
315
+ for (const cap of capabilities) {
316
+ const entry = capMatrix.capabilities[cap];
317
+ if (!entry) {
318
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
319
+ }
320
+ if (entry.codex?.sandbox_mode) {
321
+ // "read-only" is the most restrictive
322
+ sandboxMode = entry.codex.sandbox_mode;
323
+ }
324
+ if (entry.codex?.disabled_tools) {
325
+ for (const t of entry.codex.disabled_tools) {
326
+ if (t && !disabledTools.includes(t)) disabledTools.push(t);
327
+ }
328
+ }
329
+ }
330
+
331
+ return { sandbox_mode: sandboxMode, disabled_tools: disabledTools };
332
+ }
333
+
334
+ /**
335
+ * Resolve model slug for a given model_tier and harness.
336
+ */
337
+ export function resolveModel(
338
+ modelTier: string,
339
+ harness: Harness,
340
+ capMatrix: CapabilityMatrix,
341
+ ): string | null {
342
+ const tierEntry = capMatrix.model_tier[modelTier];
343
+ if (!tierEntry) return null;
344
+ const val = tierEntry[harness];
345
+ return val === null || val === undefined ? null : val;
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Overwrite policy
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /**
353
+ * Write a file according to the managed/template overwrite policy.
354
+ *
355
+ * Managed: always overwrite (unless --dry-run)
356
+ * Template: skip if exists (overwrite only with --force)
357
+ * --dry-run: record path, no write
358
+ * --strict: error if Managed path has untracked git modifications
359
+ */
360
+ export function applyOverwritePolicy(
361
+ filePath: string,
362
+ content: string,
363
+ isManaged: boolean,
364
+ opts: BuildOptions,
365
+ ): void {
366
+ if (opts.dryRun) {
367
+ dryRunFiles.push(filePath);
368
+ return;
369
+ }
370
+
371
+ if (isManaged) {
372
+ if (opts.strict) {
373
+ // Check if the file is tracked by git and has local modifications
374
+ if (existsSync(filePath)) {
375
+ try {
376
+ const rel = filePath.startsWith(ROOT)
377
+ ? filePath.slice(ROOT.length + 1)
378
+ : filePath;
379
+ const result = execSync(`git status --short -- ${JSON.stringify(rel)}`, {
380
+ cwd: ROOT,
381
+ encoding: "utf-8",
382
+ stdio: ["pipe", "pipe", "pipe"],
383
+ }).trim();
384
+ if (result && !result.startsWith("?")) {
385
+ throw new Error(
386
+ `[build-agents] --strict: managed file has untracked modifications: ${filePath}`,
387
+ );
388
+ }
389
+ } catch (err) {
390
+ if (String(err).includes("--strict:")) throw err;
391
+ // git not available or file not tracked — allow
392
+ }
393
+ }
394
+ }
395
+ mkdirSync(dirname(filePath), { recursive: true });
396
+ writeFileSync(filePath, content, "utf-8");
397
+ } else {
398
+ // Template: skip if exists unless --force
399
+ if (existsSync(filePath) && !opts.force) {
400
+ return;
401
+ }
402
+ mkdirSync(dirname(filePath), { recursive: true });
403
+ writeFileSync(filePath, content, "utf-8");
404
+ }
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Harness: Claude
409
+ // ---------------------------------------------------------------------------
410
+
411
+ function claudeAgentMarkdown(asset: AssetEntry, capMatrix: CapabilityMatrix, invocations: InvocationsMap): string {
412
+ const fm = asset.frontmatter;
413
+ const disallowed = resolveClaudeDisallowedTools(fm.capabilities ?? [], capMatrix);
414
+ const model = resolveModel(fm.model_tier, "claude", capMatrix);
415
+
416
+ const fmLines: string[] = ["---"];
417
+ if (fm.description) fmLines.push(`description: ${JSON.stringify(fm.description)}`);
418
+ if (model) fmLines.push(`model: ${model}`);
419
+ if (disallowed.length > 0) {
420
+ fmLines.push(`disallowedTools:`);
421
+ for (const t of disallowed) {
422
+ fmLines.push(` - ${t}`);
423
+ }
424
+ }
425
+ fmLines.push("---");
426
+ fmLines.push("");
427
+
428
+ const expandedBody = expandInvocations(asset.body, "claude", invocations);
429
+
430
+ return fmLines.join("\n") + expandedBody;
431
+ }
432
+
433
+ function claudeSkillMarkdown(asset: AssetEntry, invocations: InvocationsMap): string {
434
+ const fm = asset.frontmatter;
435
+
436
+ const fmLines: string[] = ["---"];
437
+ if (fm.description) fmLines.push(`description: ${JSON.stringify(fm.description)}`);
438
+ if (fm.triggers && fm.triggers.length > 0) {
439
+ fmLines.push(`triggers:`);
440
+ for (const t of fm.triggers) {
441
+ fmLines.push(` - ${t}`);
442
+ }
443
+ }
444
+ fmLines.push("---");
445
+ fmLines.push("");
446
+
447
+ const expandedBody = expandInvocations(asset.body, "claude", invocations);
448
+
449
+ return fmLines.join("\n") + expandedBody;
450
+ }
451
+
452
+ function buildPluginJson(agents: AssetEntry[]): string {
453
+ return JSON.stringify(
454
+ {
455
+ name: "claude-nexus",
456
+ version: "0.13.0",
457
+ description: "Nexus agent suite for Claude Code",
458
+ agents: agents.map((a) => ({
459
+ id: a.frontmatter.id,
460
+ name: a.frontmatter.name,
461
+ description: a.frontmatter.description,
462
+ file: `agents/${a.name}.md`,
463
+ })),
464
+ },
465
+ null,
466
+ 2,
467
+ ) + "\n";
468
+ }
469
+
470
+ function buildMarketplaceJson(agents: AssetEntry[]): string {
471
+ return JSON.stringify(
472
+ {
473
+ schema_version: "1.0",
474
+ agents: agents.map((a) => ({
475
+ id: a.frontmatter.id,
476
+ name: a.frontmatter.name,
477
+ description: a.frontmatter.description,
478
+ category: a.frontmatter.category,
479
+ model_tier: a.frontmatter.model_tier,
480
+ })),
481
+ },
482
+ null,
483
+ 2,
484
+ ) + "\n";
485
+ }
486
+
487
+ export function buildForClaude(
488
+ assets: AssetEntry[],
489
+ capMatrix: CapabilityMatrix,
490
+ invocations: InvocationsMap,
491
+ opts: BuildOptions,
492
+ ): void {
493
+ const baseDir = join(opts.targetDir, "claude");
494
+ const agentAssets = assets.filter((a) => a.type === "agent");
495
+ const skillAssets = assets.filter((a) => a.type === "skill");
496
+
497
+ // Template files: .claude-plugin/plugin.json and marketplace.json
498
+ const pluginJsonPath = join(baseDir, ".claude-plugin", "plugin.json");
499
+ const marketplacePath = join(baseDir, ".claude-plugin", "marketplace.json");
500
+
501
+ applyOverwritePolicy(pluginJsonPath, buildPluginJson(agentAssets), false, opts);
502
+ applyOverwritePolicy(marketplacePath, buildMarketplaceJson(agentAssets), false, opts);
503
+
504
+ // Managed: agents/<n>.md
505
+ for (const agent of agentAssets) {
506
+ const outPath = join(baseDir, "agents", `${agent.name}.md`);
507
+ const content = claudeAgentMarkdown(agent, capMatrix, invocations);
508
+ applyOverwritePolicy(outPath, content, true, opts);
509
+ }
510
+
511
+ // Managed: skills/<n>/SKILL.md
512
+ for (const skill of skillAssets) {
513
+ const outPath = join(baseDir, "skills", skill.name, "SKILL.md");
514
+ const content = claudeSkillMarkdown(skill, invocations);
515
+ applyOverwritePolicy(outPath, content, true, opts);
516
+ }
517
+
518
+ // Managed: settings.json (primary agent injection)
519
+ const primaryAgents = agentAssets.filter(
520
+ (a) => (a.frontmatter.mode ?? "subagent") === "primary",
521
+ );
522
+ if (primaryAgents.length > 0) {
523
+ if (primaryAgents.length > 1) {
524
+ console.warn(
525
+ `[build-agents] Warning: multiple primary agents found (${primaryAgents.map((a) => a.name).join(", ")}). Using first: ${primaryAgents[0]!.name}`,
526
+ );
527
+ }
528
+ const primaryAgent = primaryAgents[0]!;
529
+ const settingsPath = join(baseDir, "settings.json");
530
+ const settingsContent = JSON.stringify({ agent: primaryAgent.frontmatter.id }, null, 2) + "\n";
531
+ applyOverwritePolicy(settingsPath, settingsContent, true, opts);
532
+ }
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Harness: OpenCode
537
+ // ---------------------------------------------------------------------------
538
+
539
+ /**
540
+ * Generate OpenCode src/agents/<n>.ts content.
541
+ * Uses template literal inline. Backtick and ${ are escaped via string concatenation.
542
+ */
543
+ function opencodeAgentTs(asset: AssetEntry, capMatrix: CapabilityMatrix, invocations: InvocationsMap): string {
544
+ const fm = asset.frontmatter;
545
+ const perms = resolveOpencodePermissions(fm.capabilities ?? [], capMatrix);
546
+ const expandedBody = expandInvocations(asset.body, "opencode", invocations);
547
+
548
+ // Build permission block
549
+ const permEntries = Object.entries(perms);
550
+ const permBlock =
551
+ permEntries.length > 0
552
+ ? ` permission: {\n${permEntries.map(([k, v]) => ` ${k}: "${v}",`).join("\n")}\n },`
553
+ : "";
554
+
555
+ // Escape content for embedding in a template literal
556
+ // We use string concatenation to avoid issues with backtick and ${ in the template
557
+ const escapedBody = expandedBody.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
558
+ const escapedDesc = fm.description.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
559
+
560
+ const lines: string[] = [
561
+ `// Auto-generated by build-agents.ts — do not edit`,
562
+ `// Source: assets/agents/${asset.name}/body.md`,
563
+ `import type { AgentConfig } from "opencode";`,
564
+ ``,
565
+ `export const ${camelCase(asset.name)}: AgentConfig = {`,
566
+ ` id: ${JSON.stringify(fm.id)},`,
567
+ ` name: ${JSON.stringify(fm.name)},`,
568
+ ` description: \`${escapedDesc}\`,`,
569
+ ];
570
+
571
+ if (permBlock) lines.push(permBlock);
572
+
573
+ // Emit mode field only for primary agents (subagent is the OpenCode default)
574
+ if (fm.mode === "primary") {
575
+ lines.push(` mode: "primary",`);
576
+ }
577
+
578
+ lines.push(
579
+ ` system: \`${escapedBody}\`,`,
580
+ `};`,
581
+ ``,
582
+ );
583
+
584
+ return lines.join("\n");
585
+ }
586
+
587
+ function opencodeIndexTs(agents: AssetEntry[]): string {
588
+ const imports = agents
589
+ .map((a) => `import { ${camelCase(a.name)} } from "./agents/${a.name}.js";`)
590
+ .join("\n");
591
+ const exports = `export const agents = [\n${agents.map((a) => ` ${camelCase(a.name)},`).join("\n")}\n];`;
592
+
593
+ return [
594
+ `// Auto-generated by build-agents.ts — do not edit`,
595
+ ``,
596
+ imports,
597
+ ``,
598
+ exports,
599
+ ``,
600
+ ].join("\n");
601
+ }
602
+
603
+ function opencodePackageJson(agents: AssetEntry[]): string {
604
+ return (
605
+ JSON.stringify(
606
+ {
607
+ name: "opencode-nexus",
608
+ version: "0.13.0",
609
+ description: "Nexus agent suite for OpenCode",
610
+ type: "module",
611
+ main: "./src/index.ts",
612
+ exports: {
613
+ ".": "./src/index.ts",
614
+ },
615
+ peerDependencies: {
616
+ opencode: "*",
617
+ },
618
+ },
619
+ null,
620
+ 2,
621
+ ) + "\n"
622
+ );
623
+ }
624
+
625
+ function opencodeJsonFragment(agents: AssetEntry[]): string {
626
+ // Fragment to be merged into opencode.json
627
+ return (
628
+ JSON.stringify(
629
+ {
630
+ agents: agents.map((a) => ({
631
+ id: a.frontmatter.id,
632
+ module: `./src/agents/${a.name}.js`,
633
+ })),
634
+ },
635
+ null,
636
+ 2,
637
+ ) + "\n"
638
+ );
639
+ }
640
+
641
+ function opencodeSkillMarkdown(asset: AssetEntry, invocations: InvocationsMap): string {
642
+ const fm = asset.frontmatter;
643
+ const fmLines: string[] = ["---"];
644
+ if (fm.description) fmLines.push(`description: ${JSON.stringify(fm.description)}`);
645
+ if (fm.triggers && fm.triggers.length > 0) {
646
+ fmLines.push(`triggers:`);
647
+ for (const t of fm.triggers) {
648
+ fmLines.push(` - ${t}`);
649
+ }
650
+ }
651
+ fmLines.push("---");
652
+ fmLines.push("");
653
+
654
+ const expandedBody = expandInvocations(asset.body, "opencode", invocations);
655
+ return fmLines.join("\n") + expandedBody;
656
+ }
657
+
658
+ export function buildForOpencode(
659
+ assets: AssetEntry[],
660
+ capMatrix: CapabilityMatrix,
661
+ invocations: InvocationsMap,
662
+ opts: BuildOptions,
663
+ ): void {
664
+ const baseDir = join(opts.targetDir, "opencode");
665
+ const agentAssets = assets.filter((a) => a.type === "agent");
666
+ const skillAssets = assets.filter((a) => a.type === "skill");
667
+
668
+ // Template: package.json
669
+ const pkgPath = join(baseDir, "package.json");
670
+ applyOverwritePolicy(pkgPath, opencodePackageJson(agentAssets), false, opts);
671
+
672
+ // Managed: opencode.json.fragment
673
+ const fragmentPath = join(baseDir, "opencode.json.fragment");
674
+ applyOverwritePolicy(fragmentPath, opencodeJsonFragment(agentAssets), true, opts);
675
+
676
+ // Managed: src/index.ts
677
+ const indexPath = join(baseDir, "src", "index.ts");
678
+ applyOverwritePolicy(indexPath, opencodeIndexTs(agentAssets), true, opts);
679
+
680
+ // Managed: src/agents/<n>.ts
681
+ for (const agent of agentAssets) {
682
+ const outPath = join(baseDir, "src", "agents", `${agent.name}.ts`);
683
+ const content = opencodeAgentTs(agent, capMatrix, invocations);
684
+ applyOverwritePolicy(outPath, content, true, opts);
685
+ }
686
+
687
+ // Managed: .opencode/skills/<n>/SKILL.md
688
+ for (const skill of skillAssets) {
689
+ const outPath = join(baseDir, ".opencode", "skills", skill.name, "SKILL.md");
690
+ const content = opencodeSkillMarkdown(skill, invocations);
691
+ applyOverwritePolicy(outPath, content, true, opts);
692
+ }
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // Harness: Codex
697
+ // ---------------------------------------------------------------------------
698
+
699
+ /**
700
+ * Escape a string for TOML multi-line literal string (''' ''').
701
+ * Literal strings do not allow escapes, so we must not include '''.
702
+ * Fallback: use basic multi-line string with minimal escaping.
703
+ */
704
+ function tomlMultilineString(value: string): string {
705
+ // Use basic multi-line string: """ ... """
706
+ // Escape backslash and double-quote sequences
707
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
708
+ return `"""\n${escaped}\n"""`;
709
+ }
710
+
711
+ function codexAgentToml(asset: AssetEntry, capMatrix: CapabilityMatrix, invocations: InvocationsMap): string {
712
+ const fm = asset.frontmatter;
713
+ const { sandbox_mode, disabled_tools } = resolveCodexConfig(fm.capabilities ?? [], capMatrix);
714
+ const model = resolveModel(fm.model_tier, "codex", capMatrix);
715
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
716
+
717
+ const lines: string[] = [
718
+ `# Auto-generated by build-agents.ts — do not edit`,
719
+ `# Source: assets/agents/${asset.name}/body.md`,
720
+ ``,
721
+ `[agents.${fm.id}]`,
722
+ `description = ${JSON.stringify(fm.description)}`,
723
+ ];
724
+
725
+ if (model) lines.push(`model = ${JSON.stringify(model)}`);
726
+ if (sandbox_mode) lines.push(`sandbox_mode = ${JSON.stringify(sandbox_mode)}`);
727
+
728
+ if (disabled_tools.length > 0) {
729
+ lines.push(`disabled_tools = [${disabled_tools.map((t) => JSON.stringify(t)).join(", ")}]`);
730
+ }
731
+
732
+ lines.push(
733
+ ``,
734
+ `[agents.${fm.id}.system]`,
735
+ `content = ${tomlMultilineString(expandedBody)}`,
736
+ ``,
737
+ );
738
+
739
+ return lines.join("\n");
740
+ }
741
+
742
+ function codexPromptMarkdown(asset: AssetEntry, invocations: InvocationsMap): string {
743
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
744
+ const fm = asset.frontmatter;
745
+ return [
746
+ `---`,
747
+ `name: ${JSON.stringify(fm.name)}`,
748
+ `description: ${JSON.stringify(fm.description)}`,
749
+ `---`,
750
+ ``,
751
+ expandedBody,
752
+ ].join("\n");
753
+ }
754
+
755
+ function codexSkillMarkdown(asset: AssetEntry, invocations: InvocationsMap): string {
756
+ const fm = asset.frontmatter;
757
+ const fmLines: string[] = ["---"];
758
+ if (fm.description) fmLines.push(`description: ${JSON.stringify(fm.description)}`);
759
+ if (fm.triggers && fm.triggers.length > 0) {
760
+ fmLines.push(`triggers:`);
761
+ for (const t of fm.triggers) {
762
+ fmLines.push(` - ${t}`);
763
+ }
764
+ }
765
+ fmLines.push("---");
766
+ fmLines.push("");
767
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
768
+ return fmLines.join("\n") + expandedBody;
769
+ }
770
+
771
+ function codexPluginJson(agents: AssetEntry[]): string {
772
+ return (
773
+ JSON.stringify(
774
+ {
775
+ name: "codex-nexus",
776
+ version: "0.13.0",
777
+ description: "Nexus agent suite for Codex",
778
+ agents: agents.map((a) => ({
779
+ id: a.frontmatter.id,
780
+ config: `agents/${a.name}.toml`,
781
+ prompt: `prompts/${a.name}.md`,
782
+ })),
783
+ },
784
+ null,
785
+ 2,
786
+ ) + "\n"
787
+ );
788
+ }
789
+
790
+ function codexConfigFragment(agents: AssetEntry[]): string {
791
+ const lines: string[] = [
792
+ `# Auto-generated by build-agents.ts — do not edit`,
793
+ `# Merge this fragment into your codex config.toml`,
794
+ ``,
795
+ `[mcp_servers.nx]`,
796
+ `command = "nexus-mcp"`,
797
+ ``,
798
+ ];
799
+
800
+ return lines.join("\n");
801
+ }
802
+
803
+ export function buildForCodex(
804
+ assets: AssetEntry[],
805
+ capMatrix: CapabilityMatrix,
806
+ invocations: InvocationsMap,
807
+ opts: BuildOptions,
808
+ ): void {
809
+ const baseDir = join(opts.targetDir, "codex");
810
+ const agentAssets = assets.filter((a) => a.type === "agent");
811
+ const skillAssets = assets.filter((a) => a.type === "skill");
812
+
813
+ // Managed: plugin/.codex-plugin/plugin.json
814
+ const pluginJsonPath = join(baseDir, "plugin", ".codex-plugin", "plugin.json");
815
+ applyOverwritePolicy(pluginJsonPath, codexPluginJson(agentAssets), true, opts);
816
+
817
+ // Managed: plugin/skills/<n>/SKILL.md
818
+ for (const skill of skillAssets) {
819
+ const outPath = join(baseDir, "plugin", "skills", skill.name, "SKILL.md");
820
+ const content = codexSkillMarkdown(skill, invocations);
821
+ applyOverwritePolicy(outPath, content, true, opts);
822
+ }
823
+
824
+ // Managed: agents/<n>.toml
825
+ for (const agent of agentAssets) {
826
+ const outPath = join(baseDir, "agents", `${agent.name}.toml`);
827
+ const content = codexAgentToml(agent, capMatrix, invocations);
828
+ applyOverwritePolicy(outPath, content, true, opts);
829
+ }
830
+
831
+ // Managed: prompts/<n>.md
832
+ for (const agent of agentAssets) {
833
+ const outPath = join(baseDir, "prompts", `${agent.name}.md`);
834
+ const content = codexPromptMarkdown(agent, invocations);
835
+ applyOverwritePolicy(outPath, content, true, opts);
836
+ }
837
+
838
+ // Managed: install/config.fragment.toml
839
+ const fragmentPath = join(baseDir, "install", "config.fragment.toml");
840
+ applyOverwritePolicy(fragmentPath, codexConfigFragment(agentAssets), true, opts);
841
+
842
+ // Managed: install/AGENTS.fragment.md (primary agents only)
843
+ const primaryAgents = agentAssets.filter(
844
+ (a) => (a.frontmatter.mode ?? "subagent") === "primary",
845
+ );
846
+ if (primaryAgents.length > 0) {
847
+ const agentsFragmentPath = join(baseDir, "install", "AGENTS.fragment.md");
848
+ const blocks = primaryAgents.map((agent) => {
849
+ const expandedBody = expandInvocations(agent.body, "codex", invocations);
850
+ return [
851
+ `<!-- nexus-core:${agent.frontmatter.id}:start -->`,
852
+ `# ${agent.frontmatter.name}`,
853
+ ``,
854
+ expandedBody,
855
+ `<!-- nexus-core:${agent.frontmatter.id}:end -->`,
856
+ ].join("\n");
857
+ });
858
+ const agentsFragmentContent = blocks.join("\n\n") + "\n";
859
+ applyOverwritePolicy(agentsFragmentPath, agentsFragmentContent, true, opts);
860
+ }
861
+ }
862
+
863
+ // ---------------------------------------------------------------------------
864
+ // Utilities
865
+ // ---------------------------------------------------------------------------
866
+
867
+ function camelCase(str: string): string {
868
+ return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
869
+ }
870
+
871
+ // ---------------------------------------------------------------------------
872
+ // CLI arg parsing
873
+ // ---------------------------------------------------------------------------
874
+
875
+ export function parseArgs(argv: string[]): BuildOptions {
876
+ const args = argv.slice(2); // remove node and script path
877
+
878
+ let harnesses: Harness[] = [...HARNESSES];
879
+ let targetDir = join(ROOT, "dist");
880
+ let dryRun = false;
881
+ let force = false;
882
+ let strict = false;
883
+ let only: string | undefined;
884
+
885
+ for (const arg of args) {
886
+ if (arg.startsWith("--harness=")) {
887
+ const val = arg.slice("--harness=".length) as Harness;
888
+ if (!HARNESSES.includes(val)) {
889
+ throw new Error(`[build-agents] Unknown harness: ${val}. Valid: ${HARNESSES.join(", ")}`);
890
+ }
891
+ harnesses = [val];
892
+ } else if (arg.startsWith("--target=")) {
893
+ targetDir = resolve(arg.slice("--target=".length));
894
+ } else if (arg === "--dry-run") {
895
+ dryRun = true;
896
+ } else if (arg === "--force") {
897
+ force = true;
898
+ } else if (arg === "--strict") {
899
+ strict = true;
900
+ } else if (arg.startsWith("--only=")) {
901
+ only = arg.slice("--only=".length);
902
+ }
903
+ }
904
+
905
+ return { harnesses, targetDir, dryRun, force, strict, only };
906
+ }
907
+
908
+ // ---------------------------------------------------------------------------
909
+ // Main
910
+ // ---------------------------------------------------------------------------
911
+
912
+ export async function buildAgents(opts: BuildOptions): Promise<void> {
913
+ // Stage 1: Load assets
914
+ const assets = loadAssets({ only: opts.only });
915
+ const agentCount = assets.filter((a) => a.type === "agent").length;
916
+ const skillCount = assets.filter((a) => a.type === "skill").length;
917
+ console.log(`[build-agents] Loaded ${agentCount} agents, ${skillCount} skills`);
918
+
919
+ // Stage 2: Load capability matrix
920
+ const capMatrix = loadCapabilityMatrix();
921
+
922
+ // Validate all capability IDs referenced by assets
923
+ const knownCapIds = new Set(Object.keys(capMatrix.capabilities));
924
+ for (const asset of assets) {
925
+ for (const cap of asset.frontmatter.capabilities ?? []) {
926
+ if (!knownCapIds.has(cap)) {
927
+ throw new Error(
928
+ `[build-agents] "${asset.name}" references unknown capability: "${cap}". ` +
929
+ `Known: ${[...knownCapIds].join(", ")}`,
930
+ );
931
+ }
932
+ }
933
+ }
934
+
935
+ // Stage 3: Load invocations
936
+ const invocations = loadInvocations();
937
+
938
+ // Stage 4: Build per harness
939
+ if (opts.dryRun) {
940
+ console.log(`[build-agents] --dry-run mode: listing affected files only`);
941
+ }
942
+
943
+ for (const harness of opts.harnesses) {
944
+ console.log(`[build-agents] Building for harness: ${harness}`);
945
+ if (harness === "claude") {
946
+ buildForClaude(assets, capMatrix, invocations, opts);
947
+ } else if (harness === "opencode") {
948
+ buildForOpencode(assets, capMatrix, invocations, opts);
949
+ } else if (harness === "codex") {
950
+ buildForCodex(assets, capMatrix, invocations, opts);
951
+ }
952
+ }
953
+
954
+ if (opts.dryRun) {
955
+ console.log(`[build-agents] Affected files (${dryRunFiles.length}):`);
956
+ for (const f of dryRunFiles) {
957
+ console.log(` ${f}`);
958
+ }
959
+ // Clear for next run
960
+ dryRunFiles.length = 0;
961
+ return;
962
+ }
963
+
964
+ console.log(`[build-agents] Done`);
965
+ }
966
+
967
+ // Run when executed directly
968
+ if (
969
+ import.meta.url === `file://${process.argv[1]}` ||
970
+ process.argv[1]?.endsWith("build-agents.ts") ||
971
+ process.argv[1]?.endsWith("build-agents.js")
972
+ ) {
973
+ const opts = parseArgs(process.argv);
974
+ buildAgents(opts).catch((err: unknown) => {
975
+ process.stderr.write(`[build-agents] FATAL: ${String(err)}\n`);
976
+ process.exit(1);
977
+ });
978
+ }