@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.
- package/README.md +48 -63
- package/assets/agents/architect/body.ko.md +177 -0
- package/{agents → assets/agents}/architect/body.md +16 -0
- package/assets/agents/designer/body.ko.md +125 -0
- package/{agents → assets/agents}/designer/body.md +16 -0
- package/assets/agents/engineer/body.ko.md +106 -0
- package/{agents → assets/agents}/engineer/body.md +14 -0
- package/assets/agents/lead/body.ko.md +70 -0
- package/assets/agents/lead/body.md +70 -0
- package/assets/agents/postdoc/body.ko.md +122 -0
- package/{agents → assets/agents}/postdoc/body.md +16 -0
- package/assets/agents/researcher/body.ko.md +137 -0
- package/{agents → assets/agents}/researcher/body.md +15 -0
- package/assets/agents/reviewer/body.ko.md +138 -0
- package/{agents → assets/agents}/reviewer/body.md +15 -0
- package/assets/agents/strategist/body.ko.md +116 -0
- package/{agents → assets/agents}/strategist/body.md +16 -0
- package/assets/agents/tester/body.ko.md +195 -0
- package/{agents → assets/agents}/tester/body.md +15 -0
- package/assets/agents/writer/body.ko.md +122 -0
- package/{agents → assets/agents}/writer/body.md +14 -0
- package/assets/capability-matrix.yml +198 -0
- package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
- package/assets/hooks/agent-bootstrap/handler.ts +119 -0
- package/assets/hooks/agent-bootstrap/meta.yml +10 -0
- package/assets/hooks/agent-finalize/handler.test.ts +368 -0
- package/assets/hooks/agent-finalize/handler.ts +76 -0
- package/assets/hooks/agent-finalize/meta.yml +10 -0
- package/assets/hooks/capability-matrix.yml +313 -0
- package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
- package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
- package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
- package/assets/hooks/prompt-router/handler.test.ts +801 -0
- package/assets/hooks/prompt-router/handler.ts +261 -0
- package/assets/hooks/prompt-router/meta.yml +11 -0
- package/assets/hooks/session-init/handler.test.ts +274 -0
- package/assets/hooks/session-init/handler.ts +30 -0
- package/assets/hooks/session-init/meta.yml +9 -0
- package/assets/lsp-servers.json +55 -0
- package/assets/schema/lsp-servers.schema.json +67 -0
- package/assets/skills/nx-init/body.ko.md +197 -0
- package/{skills → assets/skills}/nx-init/body.md +11 -0
- package/assets/skills/nx-plan/body.ko.md +361 -0
- package/{skills → assets/skills}/nx-plan/body.md +13 -0
- package/assets/skills/nx-run/body.ko.md +161 -0
- package/{skills → assets/skills}/nx-run/body.md +11 -0
- package/assets/skills/nx-sync/body.ko.md +92 -0
- package/{skills → assets/skills}/nx-sync/body.md +10 -0
- package/assets/tools/tool-name-map.yml +353 -0
- package/dist/hooks/opencode-mount.d.ts +35 -0
- package/dist/hooks/opencode-mount.d.ts.map +1 -0
- package/dist/hooks/opencode-mount.js +332 -0
- package/dist/hooks/opencode-mount.js.map +1 -0
- package/dist/hooks/runtime.d.ts +37 -0
- package/dist/hooks/runtime.d.ts.map +1 -0
- package/dist/hooks/runtime.js +274 -0
- package/dist/hooks/runtime.js.map +1 -0
- package/dist/hooks/types.d.ts +196 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +85 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/lsp/cache.d.ts +9 -0
- package/dist/lsp/cache.d.ts.map +1 -0
- package/dist/lsp/cache.js +216 -0
- package/dist/lsp/cache.js.map +1 -0
- package/dist/lsp/client.d.ts +24 -0
- package/dist/lsp/client.d.ts.map +1 -0
- package/dist/lsp/client.js +166 -0
- package/dist/lsp/client.js.map +1 -0
- package/dist/lsp/detect.d.ts +77 -0
- package/dist/lsp/detect.d.ts.map +1 -0
- package/dist/lsp/detect.js +116 -0
- package/dist/lsp/detect.js.map +1 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/artifact.d.ts +4 -0
- package/dist/mcp/tools/artifact.d.ts.map +1 -0
- package/dist/mcp/tools/artifact.js +36 -0
- package/dist/mcp/tools/artifact.js.map +1 -0
- package/dist/mcp/tools/history.d.ts +3 -0
- package/dist/mcp/tools/history.d.ts.map +1 -0
- package/dist/mcp/tools/history.js +29 -0
- package/dist/mcp/tools/history.js.map +1 -0
- package/dist/mcp/tools/lsp.d.ts +13 -0
- package/dist/mcp/tools/lsp.d.ts.map +1 -0
- package/dist/mcp/tools/lsp.js +225 -0
- package/dist/mcp/tools/lsp.js.map +1 -0
- package/dist/mcp/tools/plan.d.ts +3 -0
- package/dist/mcp/tools/plan.d.ts.map +1 -0
- package/dist/mcp/tools/plan.js +317 -0
- package/dist/mcp/tools/plan.js.map +1 -0
- package/dist/mcp/tools/task.d.ts +3 -0
- package/dist/mcp/tools/task.d.ts.map +1 -0
- package/dist/mcp/tools/task.js +252 -0
- package/dist/mcp/tools/task.js.map +1 -0
- package/dist/shared/invocations.d.ts +74 -0
- package/dist/shared/invocations.d.ts.map +1 -0
- package/dist/shared/invocations.js +247 -0
- package/dist/shared/invocations.js.map +1 -0
- package/dist/shared/json-store.d.ts +37 -0
- package/dist/shared/json-store.d.ts.map +1 -0
- package/dist/shared/json-store.js +163 -0
- package/dist/shared/json-store.js.map +1 -0
- package/dist/shared/mcp-utils.d.ts +3 -0
- package/dist/shared/mcp-utils.d.ts.map +1 -0
- package/dist/shared/mcp-utils.js +6 -0
- package/dist/shared/mcp-utils.js.map +1 -0
- package/dist/shared/paths.d.ts +21 -0
- package/dist/shared/paths.d.ts.map +1 -0
- package/dist/shared/paths.js +81 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/tool-log.d.ts +8 -0
- package/dist/shared/tool-log.d.ts.map +1 -0
- package/dist/shared/tool-log.js +22 -0
- package/dist/shared/tool-log.js.map +1 -0
- package/dist/types/state.d.ts +862 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +66 -0
- package/dist/types/state.js.map +1 -0
- package/docs/consuming/codex-lead-merge.md +106 -0
- package/docs/plugin-guide.md +360 -0
- package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
- package/docs/plugin-template/claude/README.md +110 -0
- package/docs/plugin-template/claude/package.json +16 -0
- package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
- package/docs/plugin-template/codex/README.md +147 -0
- package/docs/plugin-template/codex/package.json +17 -0
- package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
- package/docs/plugin-template/opencode/README.md +121 -0
- package/docs/plugin-template/opencode/package.json +25 -0
- package/package.json +21 -21
- package/scripts/build-agents.test.ts +1279 -0
- package/scripts/build-agents.ts +978 -0
- package/scripts/build-hooks.test.ts +1385 -0
- package/scripts/build-hooks.ts +584 -0
- package/scripts/cli.test.ts +367 -0
- package/scripts/cli.ts +547 -0
- package/agents/architect/meta.yml +0 -13
- package/agents/designer/meta.yml +0 -13
- package/agents/engineer/meta.yml +0 -11
- package/agents/postdoc/meta.yml +0 -13
- package/agents/researcher/meta.yml +0 -12
- package/agents/reviewer/meta.yml +0 -12
- package/agents/strategist/meta.yml +0 -13
- package/agents/tester/meta.yml +0 -12
- package/agents/writer/meta.yml +0 -11
- package/conformance/README.md +0 -311
- package/conformance/examples/plan.extension.schema.example.json +0 -25
- package/conformance/lifecycle/README.md +0 -48
- package/conformance/lifecycle/agent-complete.json +0 -44
- package/conformance/lifecycle/agent-resume.json +0 -43
- package/conformance/lifecycle/agent-spawn.json +0 -36
- package/conformance/lifecycle/memory-access-record.json +0 -27
- package/conformance/lifecycle/session-end.json +0 -48
- package/conformance/scenarios/full-plan-cycle.json +0 -147
- package/conformance/scenarios/task-deps-ordering.json +0 -95
- package/conformance/schema/fixture.schema.json +0 -354
- package/conformance/state-schemas/agent-tracker.schema.json +0 -63
- package/conformance/state-schemas/history.schema.json +0 -134
- package/conformance/state-schemas/memory-access.schema.json +0 -36
- package/conformance/state-schemas/plan.schema.json +0 -77
- package/conformance/state-schemas/tasks.schema.json +0 -98
- package/conformance/tools/artifact-write.json +0 -97
- package/conformance/tools/context.json +0 -172
- package/conformance/tools/history-search.json +0 -219
- package/conformance/tools/plan-decide.json +0 -139
- package/conformance/tools/plan-start.json +0 -81
- package/conformance/tools/plan-status.json +0 -127
- package/conformance/tools/plan-update.json +0 -341
- package/conformance/tools/task-add.json +0 -156
- package/conformance/tools/task-close.json +0 -161
- package/conformance/tools/task-list.json +0 -177
- package/conformance/tools/task-update.json +0 -167
- package/docs/behavioral-contracts.md +0 -145
- package/docs/consumer-implementation-guide.md +0 -840
- package/docs/memory-lifecycle-contract.md +0 -119
- package/docs/nexus-layout.md +0 -224
- package/docs/nexus-outputs-contract.md +0 -344
- package/docs/nexus-state-overview.md +0 -170
- package/docs/nexus-tools-contract.md +0 -438
- package/manifest.json +0 -448
- package/schema/README.md +0 -69
- package/schema/agent.schema.json +0 -23
- package/schema/common.schema.json +0 -17
- package/schema/manifest.schema.json +0 -78
- package/schema/memory-policy.schema.json +0 -98
- package/schema/skill.schema.json +0 -54
- package/schema/task-exceptions.schema.json +0 -40
- package/schema/vocabulary.schema.json +0 -167
- package/scripts/.gitkeep +0 -0
- package/scripts/conformance-coverage.ts +0 -466
- package/scripts/import-from-claude-nexus.ts +0 -403
- package/scripts/lib/frontmatter.ts +0 -71
- package/scripts/lib/lint.ts +0 -348
- package/scripts/lib/structure.ts +0 -159
- package/scripts/lib/validate.ts +0 -796
- package/scripts/validate.ts +0 -90
- package/skills/nx-init/meta.yml +0 -8
- package/skills/nx-plan/meta.yml +0 -10
- package/skills/nx-run/meta.yml +0 -8
- package/skills/nx-sync/meta.yml +0 -7
- package/vocabulary/capabilities.yml +0 -65
- package/vocabulary/categories.yml +0 -11
- package/vocabulary/invocations.yml +0 -147
- package/vocabulary/memory_policy.yml +0 -88
- package/vocabulary/resume-tiers.yml +0 -11
- package/vocabulary/tags.yml +0 -60
- 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
|
+
}
|