@plainconceptsplatform/agent-harness 3.8.2 → 3.9.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/LICENSE +21 -21
- package/README.md +551 -549
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +48 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -160
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +81 -89
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +12 -10
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/package.json +2 -1
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers-core.js +337 -0
- package/harness/.opencode/plugins/pc-subagent-tiers.js +34 -345
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/harness-doctor.mjs +52 -0
- package/harness/.opencode/scripts/harness-doctor.test.mjs +58 -0
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/.opencode/scripts/sync-agents.mjs +29 -0
- package/harness/AGENTS.md +55 -49
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +12 -14
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +72 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +64 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// pc-subagent-tiers-core: pure Node agent generation. The plugin and the
|
|
2
|
+
// standalone sync script both call this, so a CI pre-start step can write the
|
|
3
|
+
// agents before OpenCode builds its task enum. No @opencode/plugin import here.
|
|
4
|
+
//
|
|
5
|
+
// Agent topology:
|
|
6
|
+
// build.md / plan.md mode: primary the only agents a human selects.
|
|
7
|
+
// planner.md mode: all fullstack body on the plan tier;
|
|
8
|
+
// commands run on it, it cannot spawn.
|
|
9
|
+
// planner-sub.md mode: subagent spawnable plan-tier alias for the
|
|
10
|
+
// task tool, same body, cannot spawn.
|
|
11
|
+
// fullstack-engineer.md mode: subagent shared body and fallback worker.
|
|
12
|
+
// *-engineer.md mode: subagent specialists.
|
|
13
|
+
// *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
export const TIERS = ["build", "fast", "plan"];
|
|
19
|
+
export const MANIFEST_FILE = "harness-models.json";
|
|
20
|
+
export const FULLSTACK_NAME = "fullstack-engineer";
|
|
21
|
+
export const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`;
|
|
22
|
+
|
|
23
|
+
export const PRIMARIES = {
|
|
24
|
+
build: {
|
|
25
|
+
tier: "build",
|
|
26
|
+
color: "primary",
|
|
27
|
+
description:
|
|
28
|
+
"Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
|
|
29
|
+
permissions: null,
|
|
30
|
+
},
|
|
31
|
+
plan: {
|
|
32
|
+
tier: "plan",
|
|
33
|
+
color: "warning",
|
|
34
|
+
description:
|
|
35
|
+
"Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
|
|
36
|
+
permissions: [
|
|
37
|
+
{ action: "edit", resource: "*", effect: "deny" },
|
|
38
|
+
{ action: "subagent", resource: "*", effect: "deny" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
planner: {
|
|
42
|
+
tier: "plan",
|
|
43
|
+
mode: "all",
|
|
44
|
+
color: "info",
|
|
45
|
+
description:
|
|
46
|
+
"Write plans, proposals, stories, reviews and archives on the plan model. Edits the tree, never spawns workers.",
|
|
47
|
+
permissions: [{ action: "subagent", resource: "*", effect: "deny" }],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const THEME_COLORS = new Set(["primary", "secondary", "accent", "success", "warning", "error", "info"]);
|
|
52
|
+
|
|
53
|
+
const LEGACY_FIELDS = [
|
|
54
|
+
"model",
|
|
55
|
+
"permission",
|
|
56
|
+
"prompt",
|
|
57
|
+
"disable",
|
|
58
|
+
"disabled",
|
|
59
|
+
"temperature",
|
|
60
|
+
"top_p",
|
|
61
|
+
"maxSteps",
|
|
62
|
+
"tools",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function agentColor(name) {
|
|
66
|
+
let h = 0x811c9dc5;
|
|
67
|
+
for (let i = 0; i < name.length; i++) {
|
|
68
|
+
h ^= name.charCodeAt(i);
|
|
69
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
70
|
+
}
|
|
71
|
+
const hue = (h >>> 0) % 360;
|
|
72
|
+
const sat = 0.68;
|
|
73
|
+
const light = 0.52;
|
|
74
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat;
|
|
75
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
76
|
+
const m = light - c / 2;
|
|
77
|
+
const [r, g, b] =
|
|
78
|
+
hue < 60
|
|
79
|
+
? [c, x, 0]
|
|
80
|
+
: hue < 120
|
|
81
|
+
? [x, c, 0]
|
|
82
|
+
: hue < 180
|
|
83
|
+
? [0, c, x]
|
|
84
|
+
: hue < 240
|
|
85
|
+
? [0, x, c]
|
|
86
|
+
: hue < 300
|
|
87
|
+
? [x, 0, c]
|
|
88
|
+
: [c, 0, x];
|
|
89
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
90
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function shouldDeriveColor(current) {
|
|
94
|
+
if (!current) return true;
|
|
95
|
+
return THEME_COLORS.has(current.trim().toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function yamlColor(value) {
|
|
99
|
+
return value.startsWith("#") ? `"${value}"` : value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unquoteColor(raw) {
|
|
103
|
+
return (raw ?? "").trim().replace(/^["']/, "").replace(/["']$/, "");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function splitFrontmatter(content) {
|
|
107
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
108
|
+
if (!match) return { frontmatter: null, body: content };
|
|
109
|
+
return { frontmatter: match[1], body: content.slice(match[0].length) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stripLegacyFields(frontmatter) {
|
|
113
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
114
|
+
const kept = [];
|
|
115
|
+
for (let i = 0; i < lines.length; i++) {
|
|
116
|
+
const key = lines[i].match(/^([A-Za-z0-9_-]+):/)?.[1];
|
|
117
|
+
if (key && LEGACY_FIELDS.includes(key)) {
|
|
118
|
+
i++;
|
|
119
|
+
while (i < lines.length && /^\s+\S/.test(lines[i])) i++;
|
|
120
|
+
i--;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
kept.push(lines[i]);
|
|
124
|
+
}
|
|
125
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setScalar(frontmatter, key, value) {
|
|
129
|
+
const line = `${key}: ${value}`;
|
|
130
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
131
|
+
const index = lines.findIndex((current) => current.startsWith(`${key}:`));
|
|
132
|
+
if (index === -1) return frontmatter ? `${line}\n${frontmatter}` : line;
|
|
133
|
+
lines[index] = line;
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function permissionsYaml(permissions) {
|
|
138
|
+
return [
|
|
139
|
+
"permissions:",
|
|
140
|
+
...permissions.flatMap((p) => [
|
|
141
|
+
` - action: ${p.action}`,
|
|
142
|
+
` resource: ${p.resource}`,
|
|
143
|
+
` effect: ${p.effect}`,
|
|
144
|
+
]),
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function colorLineFor(name, rawCurrent) {
|
|
149
|
+
const current = unquoteColor(rawCurrent);
|
|
150
|
+
const target = shouldDeriveColor(current) ? agentColor(name) : current;
|
|
151
|
+
const rendered = yamlColor(target);
|
|
152
|
+
return (rawCurrent ?? "").trim() === rendered ? null : `color: ${rendered}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeTemplate(content, name) {
|
|
156
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
157
|
+
if (frontmatter === null) {
|
|
158
|
+
return `---\nmode: subagent\ncolor: ${yamlColor(agentColor(name))}\n---\n\n${content}`;
|
|
159
|
+
}
|
|
160
|
+
let fm = stripLegacyFields(frontmatter);
|
|
161
|
+
fm = setScalar(fm, "mode", "subagent");
|
|
162
|
+
const colorLine = colorLineFor(name, fm.match(/^color:\s*(.+)$/m)?.[1]);
|
|
163
|
+
if (colorLine) fm = setScalar(fm, "color", colorLine.slice("color: ".length));
|
|
164
|
+
return `---\n${fm}\n---\n${body}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildVariant(templateContent, model) {
|
|
168
|
+
const { frontmatter, body } = splitFrontmatter(templateContent);
|
|
169
|
+
let fm = stripLegacyFields(frontmatter ?? "");
|
|
170
|
+
fm = setScalar(fm, "mode", "subagent");
|
|
171
|
+
fm = setScalar(fm, "model", model);
|
|
172
|
+
fm = setScalar(fm, "hidden", "true");
|
|
173
|
+
if (!/^color:/m.test(fm)) {
|
|
174
|
+
fm = setScalar(fm, "color", yamlColor(agentColor("engineer")));
|
|
175
|
+
}
|
|
176
|
+
return `---\n${fm}\n---\n${body}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildPrimary(spec, fullstackContent, model) {
|
|
180
|
+
const { body } = splitFrontmatter(fullstackContent);
|
|
181
|
+
const lines = ["---", `description: ${spec.description}`, `mode: ${spec.mode ?? "primary"}`];
|
|
182
|
+
if (model) lines.push(`model: ${model}`);
|
|
183
|
+
lines.push(`color: ${yamlColor(spec.color)}`);
|
|
184
|
+
if (spec.permissions) lines.push(permissionsYaml(spec.permissions));
|
|
185
|
+
lines.push("---");
|
|
186
|
+
return `${lines.join("\n")}\n\n${body.trim()}\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildPlannerSub(fullstackContent, model) {
|
|
190
|
+
const { body } = splitFrontmatter(fullstackContent);
|
|
191
|
+
const lines = [
|
|
192
|
+
"---",
|
|
193
|
+
"description: Spawnable planner alias for the task tool. Uses the plan model and cannot spawn workers.",
|
|
194
|
+
"mode: subagent",
|
|
195
|
+
];
|
|
196
|
+
if (model) lines.push(`model: ${model}`);
|
|
197
|
+
lines.push(`color: ${yamlColor(agentColor("planner-sub"))}`);
|
|
198
|
+
lines.push("hidden: true");
|
|
199
|
+
lines.push(permissionsYaml([{ action: "subagent", resource: "*", effect: "deny" }]));
|
|
200
|
+
lines.push("---");
|
|
201
|
+
return `${lines.join("\n")}\n\n${body.trim()}\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readJson(filePath) {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(await fs.readFile(filePath, "utf-8"));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function scanEngineers(agentsDir) {
|
|
213
|
+
try {
|
|
214
|
+
const entries = await fs.readdir(agentsDir);
|
|
215
|
+
return {
|
|
216
|
+
templates: entries
|
|
217
|
+
.filter((f) => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE)
|
|
218
|
+
.map((f) => f.replace(/\.md$/, "")),
|
|
219
|
+
variantFiles: entries.filter((f) => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
220
|
+
hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return { templates: [], variantFiles: [], hasFullstack: false };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function writeIfChanged(filePath, content) {
|
|
228
|
+
try {
|
|
229
|
+
if ((await fs.readFile(filePath, "utf-8")) === content) return false;
|
|
230
|
+
} catch {
|
|
231
|
+
// file missing, write it
|
|
232
|
+
}
|
|
233
|
+
const tmpPath = `${filePath}.tmp`;
|
|
234
|
+
await fs.writeFile(tmpPath, content, "utf-8");
|
|
235
|
+
await fs.rename(tmpPath, filePath);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isModelId(value) {
|
|
240
|
+
return typeof value === "string" && /^[^/\s]+\/[^/\s]+$/.test(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function generateAgents({ root, writeManifest = true } = {}) {
|
|
244
|
+
if (!root) throw new Error("generateAgents requires a root directory");
|
|
245
|
+
const agentsDir = path.join(root, ".opencode", "agents");
|
|
246
|
+
const [user, team] = await Promise.all([
|
|
247
|
+
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
248
|
+
readJson(path.join(root, ".opencode", "harness.json")),
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
const models = {};
|
|
252
|
+
const sources = {};
|
|
253
|
+
const warnings = [];
|
|
254
|
+
for (const tier of TIERS) {
|
|
255
|
+
const fromUser = user?.models?.[tier];
|
|
256
|
+
const fromTeam = team?.models?.[tier];
|
|
257
|
+
models[tier] = fromUser ?? fromTeam ?? null;
|
|
258
|
+
sources[tier] = fromUser ? "user" : fromTeam ? "team" : "missing";
|
|
259
|
+
if (!models[tier]) {
|
|
260
|
+
warnings.push(`tier ${tier} has no model; its agents inherit the session model`);
|
|
261
|
+
} else if (!isModelId(models[tier])) {
|
|
262
|
+
warnings.push(`tier ${tier} model "${models[tier]}" is not provider/model`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const available = TIERS.filter((tier) => models[tier]);
|
|
267
|
+
const { templates, variantFiles, hasFullstack } = await scanEngineers(agentsDir);
|
|
268
|
+
const manifest = {
|
|
269
|
+
generatedAt: new Date().toISOString(),
|
|
270
|
+
models,
|
|
271
|
+
source: sources,
|
|
272
|
+
agents: {},
|
|
273
|
+
warnings,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (hasFullstack) {
|
|
277
|
+
const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE);
|
|
278
|
+
const rawFullstack = await fs.readFile(fullstackPath, "utf-8");
|
|
279
|
+
const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME);
|
|
280
|
+
if (fullstack !== rawFullstack) await writeIfChanged(fullstackPath, fullstack);
|
|
281
|
+
|
|
282
|
+
for (const [name, spec] of Object.entries(PRIMARIES)) {
|
|
283
|
+
await writeIfChanged(path.join(agentsDir, `${name}.md`), buildPrimary(spec, fullstack, models[spec.tier]));
|
|
284
|
+
manifest.agents[name] = {
|
|
285
|
+
file: `${name}.md`,
|
|
286
|
+
mode: spec.mode ?? "primary",
|
|
287
|
+
model: models[spec.tier] ?? null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
await writeIfChanged(path.join(agentsDir, "planner-sub.md"), buildPlannerSub(fullstack, models.plan));
|
|
291
|
+
manifest.agents["planner-sub"] = {
|
|
292
|
+
file: "planner-sub.md",
|
|
293
|
+
mode: "subagent",
|
|
294
|
+
model: models.plan ?? null,
|
|
295
|
+
hidden: true,
|
|
296
|
+
};
|
|
297
|
+
} else {
|
|
298
|
+
warnings.push(`${FULLSTACK_TEMPLATE} missing: build, plan, planner and planner-sub not generated`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const keepSet = new Set();
|
|
302
|
+
const variants = [];
|
|
303
|
+
for (const name of templates) {
|
|
304
|
+
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8");
|
|
305
|
+
const content = normalizeTemplate(rawContent, name);
|
|
306
|
+
if (content !== rawContent) await writeIfChanged(path.join(agentsDir, `${name}.md`), content);
|
|
307
|
+
for (const tier of available) {
|
|
308
|
+
const file = `${name}.${tier}.md`;
|
|
309
|
+
variants.push({ file, path: path.join(agentsDir, file), content: buildVariant(content, models[tier]) });
|
|
310
|
+
keepSet.add(file);
|
|
311
|
+
manifest.agents[name] ??= {};
|
|
312
|
+
manifest.agents[name][tier] = { file, model: models[tier], hidden: true };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await Promise.all(variants.map((variant) => writeIfChanged(variant.path, variant.content)));
|
|
317
|
+
await Promise.all(
|
|
318
|
+
variantFiles.filter((file) => !keepSet.has(file)).map((file) => fs.unlink(path.join(agentsDir, file)).catch(() => {})),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (templates.length === 0) warnings.push("no *-engineer.md templates found; no tier variants generated");
|
|
322
|
+
|
|
323
|
+
if (writeManifest) {
|
|
324
|
+
await fs.mkdir(path.join(root, ".opencode"), { recursive: true });
|
|
325
|
+
await writeIfChanged(path.join(root, ".opencode", MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
models,
|
|
330
|
+
sources,
|
|
331
|
+
warnings,
|
|
332
|
+
manifest,
|
|
333
|
+
templates,
|
|
334
|
+
variants: variants.map((variant) => variant.file),
|
|
335
|
+
hasFullstack,
|
|
336
|
+
};
|
|
337
|
+
}
|