cli-five 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 idusortus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # cli-five
2
+
3
+ > **Code Like I'm Five** — scaffold a 5-agent VS Code Copilot team into any repo.
4
+
5
+ ```bash
6
+ npx cli-five init
7
+ ```
8
+
9
+ That's it. Run it in any folder. It interviews you, scaffolds `.github/agents/` + project memory files, searches skills.sh for relevant skills, and tells you what to do next.
10
+
11
+ ## What you get
12
+
13
+ ```
14
+ your-repo/
15
+ ├── .github/
16
+ │ ├── agents/ # 5 agents: Orchestrator, Planner, Coder, Designer, Reviewer
17
+ │ ├── copilot-instructions.md # Persona + project mandates from interview
18
+ │ ├── instructions/ # Empty — populate via /agent-customization in chat
19
+ │ └── skills/ # Empty — populate via skills.sh
20
+ ├── AGENTS.md # Tool-agnostic project context (agents.md standard)
21
+ ├── PROJECT.md # Long-form vision (rarely changes)
22
+ ├── STATE.md # Cross-session status (changes constantly)
23
+ ├── decisions.md # Architectural decision log
24
+ ├── agent-diary.md # Append-only session log
25
+ └── histories/ # Per-agent accumulated learnings
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ ```bash
31
+ npx cli-five init # interview + scaffold + skill discovery
32
+ npx cli-five doctor # validate an existing setup
33
+ npx cli-five list-stacks # show detectable tech stacks
34
+ npx cli-five help
35
+ ```
36
+
37
+ ### Flags
38
+
39
+ | Flag | Effect |
40
+ |---|---|
41
+ | `--yes`, `-y` | Accept interview defaults (overwrite gate still active) |
42
+ | `--force` | Overwrite without confirmation (use with `--yes`) |
43
+ | `--dry-run` | Print actions, write nothing |
44
+ | `--no-skills` | Skip skills.sh discovery step |
45
+ | `--cost-mode <m>` | Set cost mode: `premium`, `cheap`, or `mixed` |
46
+ | `--cwd <path>` | Run against a different directory |
47
+
48
+ ## What `init` does
49
+
50
+ 1. **Detect** — fingerprints stack (Node/TS, Python, .NET, Kotlin, Rust, Go, etc.). Brownfield-aware.
51
+ 2. **git init** — if needed. Asks first.
52
+ 3. **Overwrite gate** — double-confirms ("Proceed?" then "R U Sure?"). Only `--force --yes` bypasses.
53
+ 4. **Interview** — name, one-liner, stack, frameworks, goals, constraints, cost mode, persona toggle.
54
+ 5. **Scaffold** — writes 18 files. Substitutes answers into templates. Swaps `model:` per cost mode.
55
+ 6. **Skill discovery** — queries skills.sh via `npx skills find <term>` per detected stack. Recommends known-good skills, offers multiselect install, and launches interactive browser.
56
+ 7. **Next steps** — tells you to run `/agent-customization` for stack-specific instructions.
57
+
58
+ ## Cost modes
59
+
60
+ | Agent | `premium` (1x–3x) | `cheap` (0x) | `mixed` |
61
+ |---|---|---|---|
62
+ | Orchestrator | Claude Sonnet 4.6 | GPT-4.1 | GPT-4.1 |
63
+ | Planner | Claude Opus 4.6 | GPT-4o | GPT-4o |
64
+ | Coder | GPT-5.3-Codex | GPT-4.1 | GPT-5.3-Codex |
65
+ | Designer | Claude Opus 4.6 | GPT-4o | GPT-4o |
66
+ | Reviewer | Claude Opus 4.6 | GPT-5 mini | Claude Opus 4.6 |
67
+
68
+ Override from CLI: `npx cli-five init --cost-mode cheap`
69
+ Change anytime by editing the `model:` line in `.github/agents/*.agent.md`.
70
+
71
+ ## Required VS Code settings
72
+
73
+ ```jsonc
74
+ {
75
+ "chat.subagents.allowInvocationsFromSubagents": true
76
+ }
77
+ ```
78
+
79
+ ## What this is not
80
+
81
+ - **Not a runtime.** Once scaffolded, your repo doesn't depend on `cli-five`. You can uninstall the package and the agents still work.
82
+ - **Not a workflow engine.** No `/plan-phase`, no `/ship`, no 50 slash commands. Use `/agent-customization` in Copilot Chat to extend.
83
+ - **Not Ralph.** No issue-polling daemons, no autonomous loops, no crypto tokens.
84
+
85
+ ## Why the name
86
+
87
+ ELI5 → CLI5. Code Like I'm Five. Five agents. Get it? Yeah, it's a stretch. But the npm name was available.
88
+
89
+ ## Influences
90
+
91
+ - [bradygaster/squad](https://github.com/bradygaster/squad) — see [docs/squad-review.md](docs/squad-review.md)
92
+ - [burkeholland/ultralight](https://github.com/burkeholland/ultralight) — see [docs/ultralight-review.md](docs/ultralight-review.md)
93
+ - [gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done) — see [docs/gsd-review.md](docs/gsd-review.md)
94
+
95
+ ## Local development
96
+
97
+ ```bash
98
+ git clone https://github.com/idusortus/squad-mine
99
+ cd squad-mine
100
+ npm install
101
+ node bin/cli-five.mjs init --cwd /tmp/test-target
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // cli-five — Code Like I'm Five
3
+ import { run } from '../src/cli.mjs';
4
+
5
+ run(process.argv.slice(2)).catch((err) => {
6
+ console.error(err?.stack || err?.message || err);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "cli-five",
3
+ "version": "0.1.0",
4
+ "description": "Code Like I'm Five — scaffold a 5-agent VS Code Copilot team into any repo.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cli-five": "bin/cli-five.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "test": "node --test tests/",
18
+ "smoke": "node bin/cli-five.mjs --help"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "keywords": [
24
+ "copilot",
25
+ "vscode",
26
+ "agents",
27
+ "scaffold",
28
+ "ai"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "kleur": "^4.1.5",
33
+ "prompts": "^2.4.2"
34
+ }
35
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import kleur from 'kleur';
2
+ import { init } from './commands/init.mjs';
3
+ import { doctor } from './commands/doctor.mjs';
4
+ import { listStacks } from './commands/list-stacks.mjs';
5
+ import { STACK_SIGNATURES } from './steps/detect.mjs';
6
+
7
+ const HELP = `${kleur.bold('cli-five')} ${kleur.gray('— Code Like I\'m Five')}
8
+ Scaffold a 5-agent Copilot team into any repo.
9
+
10
+ ${kleur.bold('Usage')}
11
+ npx cli-five <command> [options]
12
+
13
+ ${kleur.bold('Commands')}
14
+ init Interview + scaffold .github/* and project memory files
15
+ doctor Validate an existing cli-five setup
16
+ list-stacks Show detectable tech stacks
17
+ help Show this message
18
+
19
+ ${kleur.bold('Flags')}
20
+ --yes, -y Accept defaults and skip confirmations (still gated on overwrite)
21
+ --force Overwrite without confirmation. Dangerous. Use with --yes.
22
+ --dry-run Print actions without writing files
23
+ --no-skills Skip the skills.sh discovery step
24
+ --cost-mode <m> Override cost mode (premium, cheap, mixed) — skips interview question
25
+ --cwd <path> Run against a directory other than the current one
26
+ `;
27
+
28
+ export async function run(argv) {
29
+ const args = parse(argv);
30
+ const cmd = args._[0] || 'help';
31
+
32
+ switch (cmd) {
33
+ case 'init':
34
+ return init(args);
35
+ case 'doctor':
36
+ return doctor(args);
37
+ case 'list-stacks':
38
+ return listStacks();
39
+ case 'help':
40
+ case '--help':
41
+ case '-h':
42
+ process.stdout.write(HELP);
43
+ return;
44
+ default:
45
+ process.stderr.write(kleur.red(`Unknown command: ${cmd}\n\n`));
46
+ process.stdout.write(HELP);
47
+ process.exit(2);
48
+ }
49
+ }
50
+
51
+ function parse(argv) {
52
+ const out = { _: [], yes: false, force: false, dryRun: false, skills: true, costMode: null, cwd: process.cwd() };
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const a = argv[i];
55
+ if (a === '--yes' || a === '-y') out.yes = true;
56
+ else if (a === '--force') out.force = true;
57
+ else if (a === '--dry-run') out.dryRun = true;
58
+ else if (a === '--no-skills') out.skills = false;
59
+ else if (a === '--cost-mode') out.costMode = argv[++i];
60
+ else if (a === '--cwd') out.cwd = argv[++i];
61
+ else if (a === '--help' || a === '-h') out._.push('help');
62
+ else out._.push(a);
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,54 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import kleur from 'kleur';
4
+ import { log } from '../util/log.mjs';
5
+
6
+ const REQUIRED = [
7
+ '.github/copilot-instructions.md',
8
+ '.github/agents/orchestrator.agent.md',
9
+ '.github/agents/planner.agent.md',
10
+ '.github/agents/coder.agent.md',
11
+ '.github/agents/designer.agent.md',
12
+ '.github/agents/reviewer.agent.md',
13
+ 'PROJECT.md',
14
+ 'STATE.md',
15
+ 'decisions.md',
16
+ 'agent-diary.md',
17
+ ];
18
+
19
+ const OPTIONAL = [
20
+ '.github/instructions',
21
+ '.github/skills',
22
+ 'AGENTS.md',
23
+ 'histories/orchestrator.md',
24
+ ];
25
+
26
+ export async function doctor(args) {
27
+ const cwd = args.cwd;
28
+ log.raw(kleur.bold().magenta('\ncli-five doctor') + kleur.gray(` ${cwd}`));
29
+ let fail = 0;
30
+
31
+ log.step('Required');
32
+ for (const p of REQUIRED) {
33
+ const ok = existsSync(join(cwd, p));
34
+ if (ok) log.ok(p);
35
+ else {
36
+ log.err(p);
37
+ fail++;
38
+ }
39
+ }
40
+
41
+ log.step('Optional');
42
+ for (const p of OPTIONAL) {
43
+ if (existsSync(join(cwd, p))) log.ok(p);
44
+ else log.warn(`missing (ok): ${p}`);
45
+ }
46
+
47
+ log.raw('');
48
+ if (fail === 0) {
49
+ log.ok('All required files present.');
50
+ } else {
51
+ log.err(`${fail} required file(s) missing. Run \`npx cli-five init\`.`);
52
+ process.exit(1);
53
+ }
54
+ }
@@ -0,0 +1,87 @@
1
+ import kleur from 'kleur';
2
+ import prompts from 'prompts';
3
+ import { log } from '../util/log.mjs';
4
+ import { detect } from '../steps/detect.mjs';
5
+ import { confirmOverwriteIfNeeded } from '../steps/confirm.mjs';
6
+ import { interview } from '../steps/interview.mjs';
7
+ import { scaffold, summarize } from '../steps/scaffold.mjs';
8
+ import { skillDiscovery } from '../steps/skills.mjs';
9
+ import { isGitRepo, gitInit } from '../util/git.mjs';
10
+
11
+ export async function init(args) {
12
+ const cwd = args.cwd;
13
+ log.raw(kleur.bold().magenta('\ncli-five init') + kleur.gray(` ${cwd}`));
14
+
15
+ // 1. Detect
16
+ log.step('1/5 Detect workspace');
17
+ const detected = detect(cwd);
18
+ log.info(`Project: ${kleur.bold(detected.projectName)}`);
19
+ log.info(`Mode: ${detected.isBrownfield ? kleur.yellow('brownfield') : kleur.green('greenfield')}`);
20
+ if (detected.stacks.length) log.info(`Stack: ${detected.stacks.map((s) => s.label).join(', ')}`);
21
+ if (!detected.hasGit) log.warn('Not a git repository.');
22
+
23
+ // 2. git init if needed
24
+ if (!detected.hasGit) {
25
+ if (args.yes || (await ask('Run `git init`?', true))) {
26
+ gitInit(cwd);
27
+ log.ok('Initialised git repo.');
28
+ } else {
29
+ log.warn('Continuing without git. You will regret this.');
30
+ }
31
+ }
32
+
33
+ // 3. Overwrite gate
34
+ log.step('2/5 Confirm overwrites');
35
+ const ok = await confirmOverwriteIfNeeded(detected, args);
36
+ if (!ok) {
37
+ log.warn('Aborted. Nothing written.');
38
+ return;
39
+ }
40
+ if (!detected.hasAgents && !detected.hasCopilotInstructions) log.dim('No collisions.');
41
+
42
+ // 4. Interview
43
+ log.step('3/5 Interview');
44
+ const answers = await interview(detected, args);
45
+
46
+ // CLI --cost-mode override
47
+ if (args.costMode && ['premium', 'cheap', 'mixed'].includes(args.costMode)) {
48
+ answers.costMode = args.costMode;
49
+ }
50
+
51
+ // 5. Scaffold
52
+ log.step('4/5 Scaffold');
53
+ const written = scaffold({ cwd, answers, args });
54
+ if (args.dryRun) log.warn('--dry-run: no files written. Plan:');
55
+ log.raw(summarize(written, cwd));
56
+ if (!args.dryRun) log.ok(`Wrote ${written.length} files.`);
57
+
58
+ // 6. Skill discovery
59
+ log.step('5/5 Skill discovery');
60
+ await skillDiscovery({ cwd, answers, args });
61
+
62
+ // 7. Next steps
63
+ printNextSteps(answers);
64
+ }
65
+
66
+ async function ask(message, initial = false) {
67
+ const { v } = await prompts({ type: 'confirm', name: 'v', message, initial });
68
+ return Boolean(v);
69
+ }
70
+
71
+ function printNextSteps(answers) {
72
+ log.raw('');
73
+ log.raw(kleur.bold().green('Done. Next steps:'));
74
+ log.raw('');
75
+ log.raw(` 1. Open this folder in VS Code Insiders.`);
76
+ log.raw(` 2. Enable Copilot subagents (settings.json):`);
77
+ log.raw(kleur.gray(` "chat.subagents.allowInvocationsFromSubagents": true`));
78
+ log.raw(` 3. Open Copilot Chat and verify the 5 agents appear (@ menu).`);
79
+ log.raw(` 4. Ask the Orchestrator to do something:`);
80
+ log.raw(kleur.gray(` @Orchestrator read PROJECT.md and propose Phase 1.`));
81
+ log.raw(` 5. Generate stack-specific instructions:`);
82
+ log.raw(kleur.gray(` Run /agent-customization in chat — it will read PROJECT.md`));
83
+ log.raw(kleur.gray(` and produce .github/instructions/*.instructions.md files.`));
84
+ log.raw('');
85
+ log.raw(kleur.dim('Edit cost mode anytime by changing `model:` in .github/agents/*.agent.md.'));
86
+ log.raw('');
87
+ }
@@ -0,0 +1,13 @@
1
+ import kleur from 'kleur';
2
+ import { STACK_SIGNATURES } from '../steps/detect.mjs';
3
+
4
+ export function listStacks() {
5
+ process.stdout.write(kleur.bold('\nDetectable stacks\n\n'));
6
+ process.stdout.write(` ${kleur.gray('ID'.padEnd(18))} ${kleur.gray('Label'.padEnd(22))} ${kleur.gray('Detected by')}\n`);
7
+ process.stdout.write(` ${'─'.repeat(18)} ${'─'.repeat(22)} ${'─'.repeat(40)}\n`);
8
+ for (const s of STACK_SIGNATURES) {
9
+ const trigger = s.files ? s.files.join(', ') : `glob: ${s.glob}`;
10
+ process.stdout.write(` ${s.id.padEnd(18)} ${s.label.padEnd(22)} ${trigger}\n`);
11
+ }
12
+ process.stdout.write('\n');
13
+ }
@@ -0,0 +1,38 @@
1
+ import prompts from 'prompts';
2
+ import { log } from '../util/log.mjs';
3
+
4
+ export async function confirmOverwriteIfNeeded(detected, args) {
5
+ const collisions = collideList(detected);
6
+ if (collisions.length === 0) return true;
7
+
8
+ log.warn('Existing cli-five artifacts detected:');
9
+ for (const c of collisions) log.dim(` - ${c}`);
10
+
11
+ if (args.force && args.yes) {
12
+ log.warn('--force --yes set. Overwriting without prompts. Hope you have git.');
13
+ return true;
14
+ }
15
+
16
+ const first = await prompts({
17
+ type: 'confirm',
18
+ name: 'ok',
19
+ message: 'This will OVERWRITE the files above. Proceed?',
20
+ initial: false,
21
+ });
22
+ if (!first.ok) return false;
23
+
24
+ const second = await prompts({
25
+ type: 'confirm',
26
+ name: 'sure',
27
+ message: 'R U Sure? Last chance.',
28
+ initial: false,
29
+ });
30
+ return Boolean(second.sure);
31
+ }
32
+
33
+ function collideList(detected) {
34
+ const out = [];
35
+ if (detected.hasAgents) out.push('.github/agents/');
36
+ if (detected.hasCopilotInstructions) out.push('.github/copilot-instructions.md');
37
+ return out;
38
+ }
@@ -0,0 +1,64 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+
4
+ export const STACK_SIGNATURES = [
5
+ { id: 'node-ts', label: 'Node + TypeScript', files: ['tsconfig.json', 'package.json'] },
6
+ { id: 'node', label: 'Node', files: ['package.json'] },
7
+ { id: 'python', label: 'Python', files: ['pyproject.toml', 'requirements.txt', 'setup.py'] },
8
+ { id: 'dotnet', label: '.NET', glob: /\.(csproj|fsproj|sln)$/i },
9
+ { id: 'rust', label: 'Rust', files: ['Cargo.toml'] },
10
+ { id: 'go', label: 'Go', files: ['go.mod'] },
11
+ { id: 'kotlin-android', label: 'Kotlin / Android', files: ['build.gradle.kts', 'settings.gradle.kts'] },
12
+ { id: 'gradle', label: 'Gradle (JVM)', files: ['build.gradle', 'settings.gradle'] },
13
+ { id: 'maven', label: 'Maven (JVM)', files: ['pom.xml'] },
14
+ { id: 'ruby', label: 'Ruby', files: ['Gemfile'] },
15
+ { id: 'php', label: 'PHP', files: ['composer.json'] },
16
+ ];
17
+
18
+ export function detect(cwd) {
19
+ const entries = safeRead(cwd);
20
+ const names = new Set(entries);
21
+ const stacks = [];
22
+
23
+ for (const sig of STACK_SIGNATURES) {
24
+ if (sig.files && sig.files.some((f) => names.has(f))) stacks.push(sig);
25
+ else if (sig.glob && entries.some((n) => sig.glob.test(n))) stacks.push(sig);
26
+ }
27
+
28
+ const hasGit = names.has('.git');
29
+ const hasGithub = names.has('.github');
30
+ const hasAgents = existsSync(join(cwd, '.github', 'agents'));
31
+ const hasCopilotInstructions = existsSync(join(cwd, '.github', 'copilot-instructions.md'));
32
+ const projectName = guessName(cwd, names);
33
+
34
+ return {
35
+ cwd,
36
+ projectName,
37
+ hasGit,
38
+ hasGithub,
39
+ hasAgents,
40
+ hasCopilotInstructions,
41
+ isBrownfield: stacks.length > 0,
42
+ stacks,
43
+ };
44
+ }
45
+
46
+ function safeRead(cwd) {
47
+ try {
48
+ return readdirSync(cwd);
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function guessName(cwd, names) {
55
+ if (names.has('package.json')) {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
58
+ if (pkg?.name) return pkg.name;
59
+ } catch {
60
+ /* ignore */
61
+ }
62
+ }
63
+ return basename(cwd);
64
+ }
@@ -0,0 +1,98 @@
1
+ import prompts from 'prompts';
2
+
3
+ const COST_MODES = [
4
+ { title: 'Premium (default)', value: 'premium', description: 'Sonnet/Opus/Codex. Best quality, real cost.' },
5
+ { title: 'Cheap (0x)', value: 'cheap', description: 'GPT-4.1 / GPT-4o / GPT-5 mini. Free tier-friendly.' },
6
+ { title: 'Mixed', value: 'mixed', description: 'Premium Coder + Reviewer, cheap everything else.' },
7
+ ];
8
+
9
+ export async function interview(detected, args) {
10
+ if (args.yes) return defaults(detected);
11
+
12
+ const onCancel = () => {
13
+ throw new Error('Interview cancelled. Nothing was written.');
14
+ };
15
+
16
+ const answers = await prompts(
17
+ [
18
+ {
19
+ type: 'text',
20
+ name: 'projectName',
21
+ message: 'Project name',
22
+ initial: detected.projectName,
23
+ },
24
+ {
25
+ type: 'text',
26
+ name: 'oneLiner',
27
+ message: 'One-line description (becomes PROJECT.md vision)',
28
+ initial: '',
29
+ },
30
+ {
31
+ type: 'list',
32
+ name: 'stack',
33
+ message: 'Tech stack (comma separated)',
34
+ initial: detected.stacks.map((s) => s.label).join(', '),
35
+ separator: ',',
36
+ },
37
+ {
38
+ type: 'list',
39
+ name: 'frameworks',
40
+ message: 'Frameworks / key libraries (comma separated, optional)',
41
+ initial: '',
42
+ separator: ',',
43
+ },
44
+ {
45
+ type: 'text',
46
+ name: 'goals',
47
+ message: 'Primary goal of this project (one sentence)',
48
+ },
49
+ {
50
+ type: 'text',
51
+ name: 'constraints',
52
+ message: 'Hard constraints (perf, deps, deploy, compliance — one sentence, optional)',
53
+ },
54
+ {
55
+ type: 'select',
56
+ name: 'costMode',
57
+ message: 'Agent cost mode',
58
+ choices: COST_MODES,
59
+ initial: 0,
60
+ },
61
+ {
62
+ type: 'confirm',
63
+ name: 'snark',
64
+ message: 'Include the snarky persona block in copilot-instructions.md?',
65
+ initial: true,
66
+ },
67
+ ],
68
+ { onCancel },
69
+ );
70
+
71
+ return normalize({ ...defaults(detected), ...answers });
72
+ }
73
+
74
+ function defaults(detected) {
75
+ return {
76
+ projectName: detected.projectName,
77
+ oneLiner: '',
78
+ stack: detected.stacks.map((s) => s.label),
79
+ frameworks: [],
80
+ goals: '',
81
+ constraints: '',
82
+ costMode: 'premium',
83
+ snark: true,
84
+ };
85
+ }
86
+
87
+ function normalize(a) {
88
+ const trim = (s) => (typeof s === 'string' ? s.trim() : s);
89
+ return {
90
+ ...a,
91
+ projectName: trim(a.projectName),
92
+ oneLiner: trim(a.oneLiner),
93
+ goals: trim(a.goals),
94
+ constraints: trim(a.constraints),
95
+ stack: (a.stack || []).map(trim).filter(Boolean),
96
+ frameworks: (a.frameworks || []).map(trim).filter(Boolean),
97
+ };
98
+ }