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.
@@ -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
  };
@@ -69,7 +69,8 @@ const COST_MODES = [
69
69
  ];
70
70
 
71
71
  export async function interview(detected, args, docHints = {}) {
72
- if (args.yes) return defaults(detected, docHints);
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 rest = await prompts(
150
- [
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
- type: 'select',
165
- name: 'costMode',
166
- message: 'Agent cost mode',
167
- choices: COST_MODES,
168
- initial: 0,
169
- },
170
- {
171
- type: 'confirm',
172
- name: 'snark',
173
- message: 'Include the snarky persona block in copilot-instructions.md?',
174
- initial: true,
175
- },
176
- ],
177
- { onCancel },
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
+ }
@@ -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 AGENT_FILES = [
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
- const MODEL_MAP = {
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
- // Agents (with model swap per cost mode)
46
- for (const file of AGENT_FILES) {
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, MODEL_MAP[answers.costMode]);
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 (templated)
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 ready for /agent-customization
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
- // Project root memory primitives (GSD-inspired)
69
- for (const tmpl of [
70
- 'AGENTS.md.tmpl',
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
- // Per-agent histories
82
- for (const file of HISTORY_FILES) {
83
- written.push(writeFile(join(cwd, 'histories', file), readTemplate('histories', file), args));
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 m = /^name:\s*(.+?)\s*$/.exec(lines[i]);
117
- if (m) {
118
- agentName = m[1];
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) {
@@ -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
- const out = {};
100
- for (const rawLine of block.split('\n')) {
101
- const line = rawLine.trim();
102
- if (!line) continue;
103
- const match = /^([A-Za-z-]+):\s*(.+)$/.exec(line);
104
- if (!match) continue;
105
- const [, key, rawValue] = match;
106
- out[key] = parseFrontmatterValue(rawValue.trim());
107
- }
108
- return out;
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
+ }