cli-five 0.2.12 → 0.2.15
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 +101 -25
- package/package.json +14 -3
- package/plugin-agents/designer.agent.md +2 -2
- package/plugin-agents/planner.agent.md +1 -1
- package/src/cli.mjs +21 -3
- package/src/commands/doctor.mjs +82 -15
- package/src/commands/init.mjs +73 -24
- package/src/steps/confirm.mjs +2 -0
- package/src/steps/detect.mjs +4 -0
- package/src/steps/interview.mjs +40 -34
- package/src/steps/platform.mjs +196 -0
- package/src/steps/scaffold.mjs +135 -34
- package/src/util/agents.mjs +63 -11
- package/src/util/models.mjs +148 -0
- package/src/util/platforms.mjs +25 -0
- package/templates/AGENTS.md.tmpl +2 -0
- package/templates/opencode/agents/coder.md +74 -0
- package/templates/opencode/agents/designer.md +65 -0
- package/templates/opencode/agents/orchestrator.md +92 -0
- package/templates/opencode/agents/planner.md +76 -0
- package/templates/opencode/agents/reviewer.md +94 -0
package/src/steps/detect.mjs
CHANGED
|
@@ -29,6 +29,8 @@ export function detect(cwd) {
|
|
|
29
29
|
const hasGithub = names.has('.github');
|
|
30
30
|
const hasAgents = existsSync(join(cwd, '.github', 'agents'));
|
|
31
31
|
const hasCopilotInstructions = existsSync(join(cwd, '.github', 'copilot-instructions.md'));
|
|
32
|
+
const hasOpencodeAgents = existsSync(join(cwd, '.opencode', 'agents'));
|
|
33
|
+
const hasOpencodeConfig = existsSync(join(cwd, 'opencode.json'));
|
|
32
34
|
const projectName = guessName(cwd, names);
|
|
33
35
|
|
|
34
36
|
return {
|
|
@@ -38,6 +40,8 @@ export function detect(cwd) {
|
|
|
38
40
|
hasGithub,
|
|
39
41
|
hasAgents,
|
|
40
42
|
hasCopilotInstructions,
|
|
43
|
+
hasOpencodeAgents,
|
|
44
|
+
hasOpencodeConfig,
|
|
41
45
|
isBrownfield: stacks.length > 0,
|
|
42
46
|
stacks,
|
|
43
47
|
};
|
package/src/steps/interview.mjs
CHANGED
|
@@ -69,7 +69,8 @@ const COST_MODES = [
|
|
|
69
69
|
];
|
|
70
70
|
|
|
71
71
|
export async function interview(detected, args, docHints = {}) {
|
|
72
|
-
|
|
72
|
+
const platform = args.__platform || 'copilot';
|
|
73
|
+
if (args.yes) return defaults(detected, docHints, platform);
|
|
73
74
|
|
|
74
75
|
const onCancel = () => {
|
|
75
76
|
throw new Error('Interview cancelled. Nothing was written.');
|
|
@@ -146,39 +147,44 @@ export async function interview(detected, args, docHints = {}) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
// ── Remaining questions ───────────────────────────────────────────
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
150
|
+
const remainingQuestions = [
|
|
151
|
+
{
|
|
152
|
+
type: 'text',
|
|
153
|
+
name: 'goals',
|
|
154
|
+
message: `Primary goal of this project (one sentence)${skipHint}`,
|
|
155
|
+
initial: docHints.goals || '',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'text',
|
|
159
|
+
name: 'constraints',
|
|
160
|
+
message: `Hard constraints (perf, deps, deploy, compliance — one sentence, optional)${skipHint}`,
|
|
161
|
+
initial: docHints.constraints || '',
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
if (platform === 'copilot') {
|
|
166
|
+
remainingQuestions.push({
|
|
167
|
+
type: 'select',
|
|
168
|
+
name: 'costMode',
|
|
169
|
+
message: 'Agent cost mode',
|
|
170
|
+
choices: COST_MODES,
|
|
171
|
+
initial: 0,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
remainingQuestions.push({
|
|
176
|
+
type: 'confirm',
|
|
177
|
+
name: 'snark',
|
|
178
|
+
message: platform === 'copilot'
|
|
179
|
+
? 'Include the snarky persona block in copilot-instructions.md?'
|
|
180
|
+
: 'Include the snarky persona block in AGENTS.md?',
|
|
181
|
+
initial: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const rest = await prompts(remainingQuestions, { onCancel });
|
|
179
185
|
|
|
180
186
|
return normalize({
|
|
181
|
-
...defaults(detected, docHints),
|
|
187
|
+
...defaults(detected, docHints, platform),
|
|
182
188
|
...basic,
|
|
183
189
|
...stackAnswers,
|
|
184
190
|
...rest,
|
|
@@ -190,7 +196,7 @@ export async function interview(detected, args, docHints = {}) {
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
/** Default stack is first preset when nothing is detected and --yes is used. */
|
|
193
|
-
function defaults(detected, docHints = {}) {
|
|
199
|
+
function defaults(detected, docHints = {}, platform = 'copilot') {
|
|
194
200
|
const hasDetected = detected.stacks.length > 0;
|
|
195
201
|
const fallback = STACK_PRESETS[0];
|
|
196
202
|
return {
|
|
@@ -200,7 +206,7 @@ function defaults(detected, docHints = {}) {
|
|
|
200
206
|
frameworks: hasDetected ? [] : fallback.frameworks,
|
|
201
207
|
goals: docHints.goals || '',
|
|
202
208
|
constraints: docHints.constraints || '',
|
|
203
|
-
costMode: 'premium',
|
|
209
|
+
costMode: platform === 'copilot' ? 'premium' : 'none',
|
|
204
210
|
snark: true,
|
|
205
211
|
presetId: hasDetected ? 'custom' : fallback.value,
|
|
206
212
|
quickstart: hasDetected ? '' : fallback.quickstart,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import {
|
|
4
|
+
PLATFORM_COPILOT,
|
|
5
|
+
PLATFORM_OPENCODE,
|
|
6
|
+
PLATFORMS,
|
|
7
|
+
platformLabel,
|
|
8
|
+
} from '../util/platforms.mjs';
|
|
9
|
+
import { log } from '../util/log.mjs';
|
|
10
|
+
import {
|
|
11
|
+
agentNames,
|
|
12
|
+
getDefaultModelMap,
|
|
13
|
+
getModelCatalog,
|
|
14
|
+
normalizeProvider,
|
|
15
|
+
providerForPlatform,
|
|
16
|
+
providerLabel,
|
|
17
|
+
PROVIDER_COPILOT,
|
|
18
|
+
PROVIDER_ZEN,
|
|
19
|
+
PROVIDERS,
|
|
20
|
+
} from '../util/models.mjs';
|
|
21
|
+
|
|
22
|
+
const CUSTOM_SENTINEL = '__custom__';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ask the user to choose a target platform.
|
|
26
|
+
* If args.target is a valid platform, skip the prompt.
|
|
27
|
+
*/
|
|
28
|
+
export async function choosePlatform(args) {
|
|
29
|
+
if (args.target) {
|
|
30
|
+
const t = String(args.target).toLowerCase();
|
|
31
|
+
if (PLATFORMS.includes(t)) {
|
|
32
|
+
return { platform: t, codegraph: args.codegraph !== false };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (args.yes) {
|
|
37
|
+
return { platform: PLATFORM_COPILOT, codegraph: args.codegraph !== false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { platform } = await prompts({
|
|
41
|
+
type: 'select',
|
|
42
|
+
name: 'platform',
|
|
43
|
+
message: 'Target platform',
|
|
44
|
+
choices: [
|
|
45
|
+
{
|
|
46
|
+
title: 'GitHub Copilot',
|
|
47
|
+
value: PLATFORM_COPILOT,
|
|
48
|
+
description: 'VS Code / Copilot Chat agent customization files',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: 'OpenCode',
|
|
52
|
+
value: PLATFORM_OPENCODE,
|
|
53
|
+
description: 'OpenCode project agents + opencode.json',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
initial: 0,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!platform) {
|
|
60
|
+
throw new Error('Platform selection cancelled. Nothing was written.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { codegraph } = await prompts({
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'codegraph',
|
|
66
|
+
message: `Add CodeGraph MCP + instructions for ${platformLabel(platform)}?`,
|
|
67
|
+
initial: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { platform, codegraph: codegraph !== false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ask the user whether to customize models, pick a provider, and optionally
|
|
75
|
+
* override per-agent models.
|
|
76
|
+
*/
|
|
77
|
+
export async function chooseModels(platform, args) {
|
|
78
|
+
// Defaults for the platform.
|
|
79
|
+
const defaultProvider = providerForPlatform(platform, args.provider);
|
|
80
|
+
const defaults = getDefaultModelMap(defaultProvider);
|
|
81
|
+
|
|
82
|
+
if (args.yes) {
|
|
83
|
+
return {
|
|
84
|
+
provider: defaultProvider,
|
|
85
|
+
modelMap: defaults,
|
|
86
|
+
customized: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { customize } = await prompts({
|
|
91
|
+
type: 'confirm',
|
|
92
|
+
name: 'customize',
|
|
93
|
+
message: 'Customize agent models?',
|
|
94
|
+
initial: false,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!customize) {
|
|
98
|
+
return {
|
|
99
|
+
provider: defaultProvider,
|
|
100
|
+
modelMap: defaults,
|
|
101
|
+
customized: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const provider = await chooseProvider(platform, args.provider);
|
|
106
|
+
const baseMap = getDefaultModelMap(provider);
|
|
107
|
+
|
|
108
|
+
// Show defaults and ask which agents to override.
|
|
109
|
+
log.raw(kleur.dim('\n Default models:'));
|
|
110
|
+
for (const agent of agentNames()) {
|
|
111
|
+
log.raw(kleur.dim(` ${agent.padEnd(14)} → ${baseMap[agent]}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { overrideAgents } = await prompts({
|
|
115
|
+
type: 'multiselect',
|
|
116
|
+
name: 'overrideAgents',
|
|
117
|
+
message: 'Select agents whose model you want to change',
|
|
118
|
+
choices: agentNames().map((name) => ({ title: name, value: name })),
|
|
119
|
+
hint: 'Space to toggle, Enter to confirm',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const modelMap = { ...baseMap };
|
|
123
|
+
|
|
124
|
+
if (overrideAgents && overrideAgents.length > 0) {
|
|
125
|
+
const catalog = getModelCatalog(provider);
|
|
126
|
+
const choices = buildModelChoices(catalog);
|
|
127
|
+
|
|
128
|
+
for (const agent of overrideAgents) {
|
|
129
|
+
const { model } = await prompts({
|
|
130
|
+
type: 'select',
|
|
131
|
+
name: 'model',
|
|
132
|
+
message: `Model for ${agent}`,
|
|
133
|
+
choices,
|
|
134
|
+
initial: 0,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (model === CUSTOM_SENTINEL) {
|
|
138
|
+
const { custom } = await prompts({
|
|
139
|
+
type: 'text',
|
|
140
|
+
name: 'custom',
|
|
141
|
+
message: `Custom model for ${agent}`,
|
|
142
|
+
initial: baseMap[agent],
|
|
143
|
+
});
|
|
144
|
+
if (custom) modelMap[agent] = custom.trim();
|
|
145
|
+
} else if (model) {
|
|
146
|
+
modelMap[agent] = model;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { provider, modelMap, customized: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function chooseProvider(platform, cliProvider) {
|
|
155
|
+
if (cliProvider) {
|
|
156
|
+
const normalized = normalizeProvider(cliProvider);
|
|
157
|
+
if (platform === PLATFORM_OPENCODE || normalized === PROVIDER_COPILOT) {
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (platform === PLATFORM_COPILOT) {
|
|
163
|
+
return PROVIDER_COPILOT;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { provider } = await prompts({
|
|
167
|
+
type: 'select',
|
|
168
|
+
name: 'provider',
|
|
169
|
+
message: 'OpenCode model provider',
|
|
170
|
+
choices: [
|
|
171
|
+
{
|
|
172
|
+
title: providerLabel(PROVIDER_ZEN),
|
|
173
|
+
value: PROVIDER_ZEN,
|
|
174
|
+
description: 'Curated, tested models via OpenCode Zen',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: 'OpenCode Go',
|
|
178
|
+
value: 'opencode-go',
|
|
179
|
+
description: 'Low-cost open-coding model subscription',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
initial: 0,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!provider) {
|
|
186
|
+
throw new Error('Provider selection cancelled. Nothing was written.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return provider;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildModelChoices(catalog) {
|
|
193
|
+
const choices = catalog.map((id) => ({ title: id, value: id }));
|
|
194
|
+
choices.push({ title: kleur.dim('Type custom model...'), value: CUSTOM_SENTINEL });
|
|
195
|
+
return choices;
|
|
196
|
+
}
|
package/src/steps/scaffold.mjs
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { log } from '../util/log.mjs';
|
|
3
|
-
import { readTemplate, render, writeFile, listFilesRecursive, relTo } from '../util/fs.mjs';
|
|
4
|
-
import { templatePath } from '../util/fs.mjs';
|
|
3
|
+
import { readTemplate, render, writeFile, listFilesRecursive, relTo, templatePath } from '../util/fs.mjs';
|
|
5
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { PLATFORM_COPILOT, PLATFORM_OPENCODE, agentDirFor, agentFileFor } from '../util/platforms.mjs';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
'orchestrator.agent.md',
|
|
9
|
-
'planner.agent.md',
|
|
10
|
-
'coder.agent.md',
|
|
11
|
-
'designer.agent.md',
|
|
12
|
-
'reviewer.agent.md',
|
|
13
|
-
];
|
|
14
|
-
|
|
7
|
+
const AGENT_NAMES = ['orchestrator', 'planner', 'coder', 'designer', 'reviewer'];
|
|
15
8
|
const HISTORY_FILES = ['orchestrator.md', 'planner.md', 'coder.md', 'designer.md', 'reviewer.md'];
|
|
16
9
|
|
|
17
|
-
|
|
10
|
+
// Legacy cost-mode maps for GitHub Copilot (used when models are not customized).
|
|
11
|
+
const COPILOT_COST_MODE_MAP = {
|
|
18
12
|
premium: {
|
|
19
13
|
Orchestrator: 'Claude Sonnet 4.6 (copilot)',
|
|
20
14
|
Planner: 'Claude Opus 4.6 (copilot)',
|
|
@@ -39,21 +33,55 @@ const MODEL_MAP = {
|
|
|
39
33
|
};
|
|
40
34
|
|
|
41
35
|
export function scaffold({ cwd, answers, args }) {
|
|
36
|
+
const platform = answers.platform || PLATFORM_COPILOT;
|
|
42
37
|
const vars = buildVars(answers);
|
|
43
38
|
const written = [];
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (platform === PLATFORM_OPENCODE) {
|
|
41
|
+
written.push(...scaffoldOpenCode({ cwd, answers, args, vars }));
|
|
42
|
+
} else {
|
|
43
|
+
written.push(...scaffoldCopilot({ cwd, answers, args, vars }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Shared memory primitives
|
|
47
|
+
for (const tmpl of [
|
|
48
|
+
'AGENTS.md.tmpl',
|
|
49
|
+
'PROJECT.md.tmpl',
|
|
50
|
+
'STATE.md.tmpl',
|
|
51
|
+
'decisions.md.tmpl',
|
|
52
|
+
'agent-diary.md.tmpl',
|
|
53
|
+
]) {
|
|
54
|
+
const out = render(readTemplate(tmpl), vars);
|
|
55
|
+
const target = tmpl.replace(/\.tmpl$/, '');
|
|
56
|
+
written.push(writeFile(join(cwd, target), out, args));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Per-agent histories
|
|
60
|
+
for (const file of HISTORY_FILES) {
|
|
61
|
+
written.push(writeFile(join(cwd, 'histories', file), readTemplate('histories', file), args));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return written;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scaffoldCopilot({ cwd, answers, args, vars }) {
|
|
68
|
+
const written = [];
|
|
69
|
+
const modelByAgent = answers.customizedModels
|
|
70
|
+
? answers.modelMap
|
|
71
|
+
: COPILOT_COST_MODE_MAP[answers.costMode] || COPILOT_COST_MODE_MAP.premium;
|
|
72
|
+
|
|
73
|
+
// Agents
|
|
74
|
+
for (const file of AGENT_NAMES.map((n) => `${n}.agent.md`)) {
|
|
47
75
|
const src = readFileSync(templatePath('.github', 'agents', file), 'utf8');
|
|
48
|
-
const swapped = swapModel(src,
|
|
76
|
+
const swapped = swapModel(src, modelByAgent);
|
|
49
77
|
written.push(writeFile(join(cwd, '.github', 'agents', file), swapped, args));
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
// copilot-instructions.md
|
|
80
|
+
// copilot-instructions.md
|
|
53
81
|
const ci = render(readTemplate('.github', 'copilot-instructions.md.tmpl'), vars);
|
|
54
82
|
written.push(writeFile(join(cwd, '.github', 'copilot-instructions.md'), ci, args));
|
|
55
83
|
|
|
56
|
-
// Empty containers
|
|
84
|
+
// Empty containers
|
|
57
85
|
written.push(
|
|
58
86
|
writeFile(
|
|
59
87
|
join(cwd, '.github', 'instructions', 'README.md'),
|
|
@@ -65,28 +93,85 @@ export function scaffold({ cwd, answers, args }) {
|
|
|
65
93
|
writeFile(join(cwd, '.github', 'skills', 'README.md'), readTemplate('.github', 'skills', 'README.md'), args),
|
|
66
94
|
);
|
|
67
95
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
'PROJECT.md.tmpl',
|
|
72
|
-
'STATE.md.tmpl',
|
|
73
|
-
'decisions.md.tmpl',
|
|
74
|
-
'agent-diary.md.tmpl',
|
|
75
|
-
]) {
|
|
76
|
-
const out = render(readTemplate(tmpl), vars);
|
|
77
|
-
const target = tmpl.replace(/\.tmpl$/, '');
|
|
78
|
-
written.push(writeFile(join(cwd, target), out, args));
|
|
96
|
+
// CodeGraph MCP for VS Code Copilot
|
|
97
|
+
if (answers.codegraph) {
|
|
98
|
+
written.push(writeFile(join(cwd, '.vscode', 'mcp.json'), JSON.stringify(codegraphMcpJson(), null, 2), args));
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
return written;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scaffoldOpenCode({ cwd, answers, args, vars }) {
|
|
105
|
+
const written = [];
|
|
106
|
+
const modelByAgent = answers.modelMap || {};
|
|
107
|
+
const orchestratorModel = modelByAgent.Orchestrator || 'opencode/gpt-5.3-codex';
|
|
108
|
+
|
|
109
|
+
// Agents
|
|
110
|
+
for (const name of AGENT_NAMES) {
|
|
111
|
+
const src = readFileSync(templatePath('opencode', 'agents', `${name}.md`), 'utf8');
|
|
112
|
+
const swapped = swapModel(src, modelByAgent);
|
|
113
|
+
written.push(writeFile(join(cwd, '.opencode', 'agents', `${name}.md`), swapped, args));
|
|
84
114
|
}
|
|
85
115
|
|
|
116
|
+
// opencode.json
|
|
117
|
+
const opencodeConfig = buildOpencodeConfig({ answers, orchestratorModel });
|
|
118
|
+
written.push(writeFile(join(cwd, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2) + '\n', args));
|
|
119
|
+
|
|
120
|
+
// Empty containers (still useful for OpenCode agents)
|
|
121
|
+
written.push(
|
|
122
|
+
writeFile(
|
|
123
|
+
join(cwd, '.github', 'instructions', 'README.md'),
|
|
124
|
+
readTemplate('.github', 'instructions', 'README.md'),
|
|
125
|
+
args,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
written.push(
|
|
129
|
+
writeFile(join(cwd, '.github', 'skills', 'README.md'), readTemplate('.github', 'skills', 'README.md'), args),
|
|
130
|
+
);
|
|
131
|
+
|
|
86
132
|
return written;
|
|
87
133
|
}
|
|
88
134
|
|
|
135
|
+
function buildOpencodeConfig({ answers, orchestratorModel }) {
|
|
136
|
+
const smallModel = answers.provider === 'opencode-go'
|
|
137
|
+
? 'opencode-go/qwen3.8-flash'
|
|
138
|
+
: 'opencode/gpt-5-nano';
|
|
139
|
+
|
|
140
|
+
const config = {
|
|
141
|
+
$schema: 'https://opencode.ai/config.json',
|
|
142
|
+
model: orchestratorModel,
|
|
143
|
+
small_model: smallModel,
|
|
144
|
+
subagent_depth: 2,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (answers.codegraph) {
|
|
148
|
+
config.mcp = {
|
|
149
|
+
codegraph: {
|
|
150
|
+
type: 'local',
|
|
151
|
+
command: ['codegraph', 'serve', '--mcp'],
|
|
152
|
+
enabled: true,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return config;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function codegraphMcpJson() {
|
|
161
|
+
return {
|
|
162
|
+
inputs: [],
|
|
163
|
+
servers: {
|
|
164
|
+
codegraph: {
|
|
165
|
+
command: 'codegraph',
|
|
166
|
+
args: ['serve', '--mcp'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
89
172
|
function buildVars(a) {
|
|
173
|
+
const codegraphBlock = a.codegraph ? CODEGRAPH_BLOCK : '';
|
|
174
|
+
|
|
90
175
|
return {
|
|
91
176
|
PROJECT_NAME: a.projectName,
|
|
92
177
|
ONE_LINER: a.oneLiner || 'TODO — write a one-line vision statement.',
|
|
@@ -105,17 +190,17 @@ ${a.docs}
|
|
|
105
190
|
` : '',
|
|
106
191
|
DATE: new Date().toISOString().slice(0, 10),
|
|
107
192
|
PERSONA_BLOCK: a.snark ? PERSONA_BLOCK : '',
|
|
193
|
+
CODEGRAPH_BLOCK: codegraphBlock,
|
|
108
194
|
};
|
|
109
195
|
}
|
|
110
196
|
|
|
111
197
|
function swapModel(src, modelByAgent) {
|
|
112
|
-
// Replace the YAML `model:` line based on the `name:` immediately above/around it.
|
|
113
198
|
const lines = src.split('\n');
|
|
114
199
|
let agentName = null;
|
|
115
200
|
for (let i = 0; i < lines.length; i++) {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
agentName =
|
|
201
|
+
const nameMatch = /^name:\s*(.+?)\s*$/.exec(lines[i]);
|
|
202
|
+
if (nameMatch) {
|
|
203
|
+
agentName = nameMatch[1];
|
|
119
204
|
continue;
|
|
120
205
|
}
|
|
121
206
|
if (agentName && /^model:\s*/.test(lines[i])) {
|
|
@@ -136,6 +221,22 @@ const PERSONA_BLOCK = `# Persona
|
|
|
136
221
|
|
|
137
222
|
`;
|
|
138
223
|
|
|
224
|
+
const CODEGRAPH_BLOCK = `
|
|
225
|
+
<!-- CODEGRAPH_START -->
|
|
226
|
+
## CodeGraph
|
|
227
|
+
|
|
228
|
+
This project is configured to use [CodeGraph](https://codegraph.ru) for graph-backed codebase context.
|
|
229
|
+
When you need to understand relationships, call paths, or impacts, use:
|
|
230
|
+
|
|
231
|
+
\`\`\`
|
|
232
|
+
codegraph explore "<your question>"
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
The CodeGraph MCP server is registered in the project config. Run \`codegraph init\` in this directory
|
|
236
|
+
if the project has not been indexed yet.
|
|
237
|
+
<!-- CODEGRAPH_END -->
|
|
238
|
+
`;
|
|
239
|
+
|
|
139
240
|
export function summarize(written, cwd) {
|
|
140
241
|
const lines = [];
|
|
141
242
|
for (const w of written) {
|
package/src/util/agents.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import YAML from 'yaml';
|
|
2
3
|
|
|
3
4
|
export const AGENT_FILES = [
|
|
4
5
|
'orchestrator.agent.md',
|
|
@@ -8,6 +9,14 @@ export const AGENT_FILES = [
|
|
|
8
9
|
'reviewer.agent.md',
|
|
9
10
|
];
|
|
10
11
|
|
|
12
|
+
export const OPENCODE_AGENT_FILES = [
|
|
13
|
+
'orchestrator.md',
|
|
14
|
+
'planner.md',
|
|
15
|
+
'coder.md',
|
|
16
|
+
'designer.md',
|
|
17
|
+
'reviewer.md',
|
|
18
|
+
];
|
|
19
|
+
|
|
11
20
|
export const EXPECTED_AGENT_NAMES = {
|
|
12
21
|
'orchestrator.agent.md': 'Orchestrator',
|
|
13
22
|
'planner.agent.md': 'Planner',
|
|
@@ -89,6 +98,44 @@ export function validateAgentSource(source, fileName, { requiredMarkers = [] } =
|
|
|
89
98
|
return errors;
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
export function validateOpenCodeAgentSource(source, fileName) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
const { frontmatter, body } = parseAgentSource(source);
|
|
104
|
+
|
|
105
|
+
if (!frontmatter) {
|
|
106
|
+
return ['missing YAML frontmatter'];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const key of ['description', 'mode', 'model']) {
|
|
110
|
+
if (!(key in frontmatter)) {
|
|
111
|
+
errors.push(`missing frontmatter key: ${key}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mode = String(frontmatter.mode || '').toLowerCase();
|
|
116
|
+
if (mode && !['primary', 'subagent', 'all'].includes(mode)) {
|
|
117
|
+
errors.push(`invalid mode: ${frontmatter.mode}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const expectedMode = fileName === 'orchestrator.md' ? 'primary' : 'subagent';
|
|
121
|
+
if (mode && mode !== expectedMode && mode !== 'all') {
|
|
122
|
+
errors.push(`expected mode ${expectedMode}, found ${frontmatter.mode}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (fileName === 'orchestrator.md') {
|
|
126
|
+
const task = frontmatter.permission?.task || frontmatter.permissions?.task;
|
|
127
|
+
if (!task) {
|
|
128
|
+
errors.push('orchestrator must declare permission.task for subagents');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!body.trim()) {
|
|
133
|
+
errors.push('agent body must not be empty');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
138
|
+
|
|
92
139
|
function requiredFrontmatterKeys(fileName) {
|
|
93
140
|
return fileName === 'orchestrator.agent.md'
|
|
94
141
|
? ['name', 'description', 'model', 'tools', 'agents']
|
|
@@ -96,16 +143,21 @@ function requiredFrontmatterKeys(fileName) {
|
|
|
96
143
|
}
|
|
97
144
|
|
|
98
145
|
function parseFrontmatter(block) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
try {
|
|
147
|
+
return YAML.parse(block, { strict: false });
|
|
148
|
+
} catch {
|
|
149
|
+
// Fall back to the legacy line parser for simple Copilot frontmatter.
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const rawLine of block.split('\n')) {
|
|
152
|
+
const line = rawLine.trim();
|
|
153
|
+
if (!line) continue;
|
|
154
|
+
const match = /^([A-Za-z-]+):\s*(.+)$/.exec(line);
|
|
155
|
+
if (!match) continue;
|
|
156
|
+
const [, key, rawValue] = match;
|
|
157
|
+
out[key] = parseFrontmatterValue(rawValue.trim());
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
109
161
|
}
|
|
110
162
|
|
|
111
163
|
function parseFrontmatterValue(rawValue) {
|
|
@@ -134,4 +186,4 @@ function parseArrayValue(rawValue) {
|
|
|
134
186
|
}
|
|
135
187
|
return part;
|
|
136
188
|
});
|
|
137
|
-
}
|
|
189
|
+
}
|