cli-five 0.2.13 → 0.2.16

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.
@@ -0,0 +1,170 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, extname } from 'node:path';
3
+
4
+ const JSON_EXTENSIONS = new Set(['.json']);
5
+
6
+ /**
7
+ * Surgically merge a named block into an existing file **without touching the
8
+ * rest of it**. Unlike init's blunt overwrite gate (which replaces whole files),
9
+ * `mergeBlock` is designed for `cli-five add` against repos that are already
10
+ * scaffolded.
11
+ *
12
+ * Two strategies, chosen by file extension:
13
+ *
14
+ * JSON (.json)
15
+ * `content` (a plain object or a JSON string) is deep-merged into the file.
16
+ * Existing keys are preserved; overlapping scalar/array keys are replaced.
17
+ * Passing `fenceKey` nests the patch under that top-level key instead of
18
+ * merging at the root (e.g. `{ fenceKey: 'mcp' }` for `opencode.json`).
19
+ *
20
+ * Markdown / other text
21
+ * `content` is wrapped in HTML-comment fences derived from `markerFence`:
22
+ * <!-- NAME_START -->
23
+ * ...content...
24
+ * <!-- NAME_END -->
25
+ * If the fences already exist the body between them is replaced in place;
26
+ * otherwise the block is appended. Re-running is idempotent.
27
+ *
28
+ * @param {string} filePath Absolute path to the target file.
29
+ * @param {string|{name?:string,start?:string,end?:string}} markerFence
30
+ * Block name (e.g. "codegraph"), or explicit `{ start, end }` markers.
31
+ * @param {string|object} content Markdown body, or object / JSON string.
32
+ * @param {object} [options]
33
+ * @param {boolean} [options.dryRun] Compute but do not write.
34
+ * @param {string|null} [options.fenceKey] JSON only — nest the merge under this key.
35
+ * @param {boolean} [options.track] JSON only — record the block name under `$cliFive`.
36
+ * @param {string} [options.metaKey] JSON only — metadata key (default `$cliFive`).
37
+ * @returns {{path:string, block:string, action:'created'|'updated'|'unchanged', dryRun:boolean}}
38
+ */
39
+ export function mergeBlock(filePath, markerFence, content, options = {}) {
40
+ const { dryRun = false, fenceKey = null, track = false, metaKey = '$cliFive' } = options;
41
+ const block = fenceName(markerFence);
42
+
43
+ const ext = extname(filePath).toLowerCase();
44
+ const result = JSON_EXTENSIONS.has(ext)
45
+ ? mergeJson(filePath, block, content, { fenceKey, track, metaKey })
46
+ : mergeText(filePath, markerFence, content);
47
+
48
+ if (!dryRun && (result.action === 'created' || result.action === 'updated')) {
49
+ mkdirSync(dirname(filePath), { recursive: true });
50
+ writeFileSync(filePath, result.contents);
51
+ }
52
+
53
+ return { path: filePath, block, action: result.action, dryRun };
54
+ }
55
+
56
+ // ── Markdown / text ───────────────────────────────────────────────────
57
+
58
+ function mergeText(filePath, markerFence, content) {
59
+ const { start, end } = fenceMarkers(markerFence);
60
+ const body = String(content ?? '').replace(/\s+$/, '');
61
+ const core = `${start}\n${body}\n${end}`;
62
+
63
+ if (!existsSync(filePath)) {
64
+ return { contents: `${core}\n`, action: 'created' };
65
+ }
66
+
67
+ const existing = readFileSync(filePath, 'utf8');
68
+ if (existing.trim() === '') {
69
+ return { contents: `${core}\n`, action: 'created' };
70
+ }
71
+
72
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`);
73
+ if (pattern.test(existing)) {
74
+ const next = existing.replace(pattern, core);
75
+ return { contents: next, action: next === existing ? 'unchanged' : 'updated' };
76
+ }
77
+
78
+ const next = `${existing.replace(/\s+$/, '')}\n\n${core}\n`;
79
+ return { contents: next, action: 'updated' };
80
+ }
81
+
82
+ function fenceMarkers(markerFence) {
83
+ if (isPlainObject(markerFence) && markerFence.start && markerFence.end) {
84
+ return { start: markerFence.start, end: markerFence.end };
85
+ }
86
+ const name = fenceName(markerFence).toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
87
+ return { start: `<!-- ${name}_START -->`, end: `<!-- ${name}_END -->` };
88
+ }
89
+
90
+ // ── JSON ──────────────────────────────────────────────────────────────
91
+
92
+ function mergeJson(filePath, block, content, { fenceKey, track, metaKey }) {
93
+ let existing = {};
94
+ if (existsSync(filePath)) {
95
+ const raw = readFileSync(filePath, 'utf8').trim();
96
+ if (raw) {
97
+ try {
98
+ existing = JSON.parse(raw);
99
+ } catch (err) {
100
+ throw new Error(`mergeBlock: ${filePath} is not valid JSON: ${err.message}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ if (!isPlainObject(existing)) {
106
+ throw new Error(`mergeBlock: ${filePath} must contain a JSON object at the root`);
107
+ }
108
+
109
+ let patch = content;
110
+ if (typeof patch === 'string') {
111
+ try {
112
+ patch = JSON.parse(patch);
113
+ } catch (err) {
114
+ throw new Error(`mergeBlock: content for ${filePath} is not valid JSON: ${err.message}`);
115
+ }
116
+ }
117
+ if (!isPlainObject(patch)) {
118
+ throw new Error(`mergeBlock: content for ${filePath} must be a JSON object`);
119
+ }
120
+
121
+ const before = JSON.stringify(existing);
122
+
123
+ const target = fenceKey
124
+ ? (isPlainObject(existing[fenceKey]) ? existing[fenceKey] : (existing[fenceKey] = {}))
125
+ : existing;
126
+ deepMerge(target, patch);
127
+
128
+ if (track) {
129
+ const meta = isPlainObject(existing[metaKey]) ? existing[metaKey] : (existing[metaKey] = {});
130
+ const blocks = Array.isArray(meta.blocks) ? meta.blocks : (meta.blocks = []);
131
+ if (!blocks.includes(block)) blocks.push(block);
132
+ }
133
+
134
+ const contents = `${JSON.stringify(existing, null, 2)}\n`;
135
+ const action = before === JSON.stringify(existing) && existsSync(filePath) ? 'unchanged' : (existsSync(filePath) ? 'updated' : 'created');
136
+ return { contents, action };
137
+ }
138
+
139
+ function deepMerge(target, patch) {
140
+ for (const [key, value] of Object.entries(patch)) {
141
+ if (isPlainObject(value) && isPlainObject(target[key])) {
142
+ deepMerge(target[key], value);
143
+ } else if (isPlainObject(value)) {
144
+ target[key] = deepMerge({}, value);
145
+ } else if (Array.isArray(value)) {
146
+ target[key] = [...value];
147
+ } else {
148
+ target[key] = value;
149
+ }
150
+ }
151
+ return target;
152
+ }
153
+
154
+ // ── Helpers ───────────────────────────────────────────────────────────
155
+
156
+ function fenceName(markerFence) {
157
+ if (typeof markerFence === 'string' && markerFence.trim()) return markerFence.trim();
158
+ if (isPlainObject(markerFence) && typeof markerFence.name === 'string' && markerFence.name.trim()) {
159
+ return markerFence.name.trim();
160
+ }
161
+ throw new Error('mergeBlock: markerFence must be a non-empty string or { name }');
162
+ }
163
+
164
+ function isPlainObject(value) {
165
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
166
+ }
167
+
168
+ function escapeRegExp(value) {
169
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
170
+ }
@@ -0,0 +1,148 @@
1
+ // Model catalogs and defaults per provider.
2
+ // Provider IDs match the values used in OpenCode config (`provider/model-id`).
3
+
4
+ export const PROVIDER_COPILOT = 'copilot';
5
+ export const PROVIDER_ZEN = 'opencode';
6
+ export const PROVIDER_GO = 'opencode-go';
7
+
8
+ export const PROVIDERS = [PROVIDER_COPILOT, PROVIDER_ZEN, PROVIDER_GO];
9
+
10
+ export const PROVIDER_LABELS = {
11
+ [PROVIDER_COPILOT]: 'GitHub Copilot',
12
+ [PROVIDER_ZEN]: 'OpenCode Zen',
13
+ [PROVIDER_GO]: 'OpenCode Go',
14
+ };
15
+
16
+ const AGENTS = ['Orchestrator', 'Planner', 'Coder', 'Designer', 'Reviewer'];
17
+
18
+ export const DEFAULT_MODEL_MAP = {
19
+ [PROVIDER_COPILOT]: {
20
+ Orchestrator: 'Claude Sonnet 4.6 (copilot)',
21
+ Planner: 'Claude Opus 4.6 (copilot)',
22
+ Coder: 'GPT-5.3-Codex (copilot)',
23
+ Designer: 'Gemini 3.1 Pro (Preview) (copilot)',
24
+ Reviewer: 'Claude Sonnet 4.6 (copilot)',
25
+ },
26
+ [PROVIDER_ZEN]: {
27
+ Orchestrator: 'opencode/gpt-5.3-codex',
28
+ Planner: 'opencode/claude-opus-4-6',
29
+ Coder: 'opencode/gpt-5.3-codex',
30
+ Designer: 'opencode/gemini-3.1-pro',
31
+ Reviewer: 'opencode/claude-sonnet-4-6',
32
+ },
33
+ [PROVIDER_GO]: {
34
+ Orchestrator: 'opencode-go/qwen3.8-max',
35
+ Planner: 'opencode-go/deepseek-v4-pro',
36
+ Coder: 'opencode-go/qwen3.8-max',
37
+ Designer: 'opencode-go/gpt-5.6-luna',
38
+ Reviewer: 'opencode-go/kimi-k2.7-code',
39
+ },
40
+ };
41
+
42
+ // Curated model choices shown in the interview picker.
43
+ // Each entry is a full model reference string for the given provider.
44
+ export const PROVIDER_MODEL_CATALOG = {
45
+ [PROVIDER_COPILOT]: [
46
+ 'Claude Sonnet 4.6 (copilot)',
47
+ 'Claude Opus 4.6 (copilot)',
48
+ 'Claude Opus 4.5 (copilot)',
49
+ 'GPT-5.3-Codex (copilot)',
50
+ 'GPT-5.2-Codex (copilot)',
51
+ 'GPT-5.1-Codex (copilot)',
52
+ 'GPT-5 (copilot)',
53
+ 'GPT-5 mini (copilot)',
54
+ 'GPT-4.1 (copilot)',
55
+ 'GPT-4o (copilot)',
56
+ 'Gemini 3.1 Pro (Preview) (copilot)',
57
+ 'Gemini 3 Flash (Preview) (copilot)',
58
+ ],
59
+ [PROVIDER_ZEN]: [
60
+ 'opencode/gpt-5.3-codex',
61
+ 'opencode/gpt-5.2-codex',
62
+ 'opencode/gpt-5.1-codex',
63
+ 'opencode/gpt-5.1-codex-max',
64
+ 'opencode/gpt-5.1-codex-mini',
65
+ 'opencode/gpt-5',
66
+ 'opencode/gpt-5-nano',
67
+ 'opencode/claude-opus-4-6',
68
+ 'opencode/claude-opus-4-5',
69
+ 'opencode/claude-sonnet-4-6',
70
+ 'opencode/claude-sonnet-4-5',
71
+ 'opencode/gemini-3.1-pro',
72
+ 'opencode/gemini-3-flash',
73
+ 'opencode/kimi-k2.7-code',
74
+ 'opencode/kimi-k2.6',
75
+ 'opencode/qwen3.8-max',
76
+ 'opencode/qwen3.8-flash',
77
+ 'opencode/qwen3.7-plus',
78
+ 'opencode/deepseek-v4-pro',
79
+ 'opencode/deepseek-v4.1-flash',
80
+ ],
81
+ [PROVIDER_GO]: [
82
+ 'opencode-go/qwen3.8-max',
83
+ 'opencode-go/qwen3.8-flash',
84
+ 'opencode-go/qwen3.7-max',
85
+ 'opencode-go/qwen3.7-plus',
86
+ 'opencode-go/qwen3.6-plus',
87
+ 'opencode-go/deepseek-v4-pro',
88
+ 'opencode-go/deepseek-v4.1-flash',
89
+ 'opencode-go/deepseek-v4-flash',
90
+ 'opencode-go/kimi-k3',
91
+ 'opencode-go/kimi-k2.7-code',
92
+ 'opencode-go/kimi-k2.6',
93
+ 'opencode-go/glm-5.3',
94
+ 'opencode-go/glm-5.2',
95
+ 'opencode-go/glm-5.1',
96
+ 'opencode-go/glm-5.3-flash',
97
+ 'opencode-go/minimax-m3',
98
+ 'opencode-go/minimax-m2.7',
99
+ 'opencode-go/gpt-6-luna',
100
+ 'opencode-go/gpt-5.6-luna',
101
+ 'opencode-go/muse-spark-1.3-contributor',
102
+ 'opencode-go/muse-spark-1.2-contributor',
103
+ 'opencode-go/mimo-v2.6-flash',
104
+ 'opencode-go/mimo-v2.6-pro',
105
+ 'opencode-go/grok-4.7',
106
+ 'opencode-go/grok-4.6',
107
+ 'opencode-go/longcat-2.0',
108
+ 'opencode-go/space-bunny-free',
109
+ ],
110
+ };
111
+
112
+ export function getDefaultModelMap(provider) {
113
+ return { ...DEFAULT_MODEL_MAP[provider] };
114
+ }
115
+
116
+ export function getModelCatalog(provider) {
117
+ return PROVIDER_MODEL_CATALOG[provider] || [];
118
+ }
119
+
120
+ export function isValidProvider(value) {
121
+ return PROVIDERS.includes(value);
122
+ }
123
+
124
+ export function normalizeProvider(value) {
125
+ if (!value) return PROVIDER_COPILOT;
126
+ const lower = String(value).toLowerCase();
127
+ if (lower === 'zen') return PROVIDER_ZEN;
128
+ if (lower === 'go') return PROVIDER_GO;
129
+ if (lower === 'opencode') return PROVIDER_ZEN;
130
+ if (lower === 'opencode-go') return PROVIDER_GO;
131
+ if (lower === 'copilot' || lower === 'github-copilot') return PROVIDER_COPILOT;
132
+ return isValidProvider(lower) ? lower : PROVIDER_COPILOT;
133
+ }
134
+
135
+ export function providerLabel(provider) {
136
+ return PROVIDER_LABELS[provider] || provider;
137
+ }
138
+
139
+ export function agentNames() {
140
+ return [...AGENTS];
141
+ }
142
+
143
+ export function providerForPlatform(platform, provider) {
144
+ if (platform === 'opencode') {
145
+ return normalizeProvider(provider || PROVIDER_ZEN);
146
+ }
147
+ return PROVIDER_COPILOT;
148
+ }
@@ -0,0 +1,25 @@
1
+ // Supported cli-five platforms and CodeGraph pairing.
2
+
3
+ export const PLATFORM_COPILOT = 'copilot';
4
+ export const PLATFORM_OPENCODE = 'opencode';
5
+
6
+ export const PLATFORMS = [PLATFORM_COPILOT, PLATFORM_OPENCODE];
7
+
8
+ export function isValidPlatform(value) {
9
+ return PLATFORMS.includes(value);
10
+ }
11
+
12
+ export function platformLabel(value) {
13
+ return value === PLATFORM_OPENCODE ? 'OpenCode' : 'GitHub Copilot';
14
+ }
15
+
16
+ export function agentDirFor(platform) {
17
+ return platform === PLATFORM_OPENCODE ? '.opencode/agents' : '.github/agents';
18
+ }
19
+
20
+ export function agentFileFor(platform, name) {
21
+ if (platform === PLATFORM_OPENCODE) {
22
+ return `${name}.md`;
23
+ }
24
+ return `${name}.agent.md`;
25
+ }
@@ -0,0 +1,140 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // README variants checked in order. First match wins.
5
+ const README_CANDIDATES = [
6
+ 'README.md',
7
+ 'readme.md',
8
+ 'Readme.md',
9
+ 'README.MD',
10
+ 'README.markdown',
11
+ 'README.txt',
12
+ 'README',
13
+ ];
14
+
15
+ /**
16
+ * Best-effort auto-extraction of a project name and one-liner from the
17
+ * workspace itself (package.json and/or README), used by the minimal init
18
+ * interview so it only has to ask when the answer is genuinely missing or
19
+ * ambiguous.
20
+ *
21
+ * Returns:
22
+ * {
23
+ * name: { value, ambiguous, sources: [{ source, value }] },
24
+ * oneLiner: { value, ambiguous, sources: [{ source, value }] },
25
+ * }
26
+ *
27
+ * `value` is the first candidate (a safe fallback), `ambiguous` is true when
28
+ * two or more distinct candidates were found. Callers should ask the user
29
+ * whenever `ambiguous` is true or `value` is empty.
30
+ */
31
+ export function autoProjectInfo(cwd) {
32
+ const nameSources = [];
33
+ const oneLinerSources = [];
34
+
35
+ const pkgPath = join(cwd, 'package.json');
36
+ if (existsSync(pkgPath)) {
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
39
+ if (isNonEmptyString(pkg?.name)) {
40
+ nameSources.push({ source: 'package.json', value: pkg.name.trim() });
41
+ }
42
+ if (isNonEmptyString(pkg?.description)) {
43
+ oneLinerSources.push({ source: 'package.json', value: pkg.description.trim() });
44
+ }
45
+ } catch {
46
+ /* malformed package.json — ignore */
47
+ }
48
+ }
49
+
50
+ for (const file of README_CANDIDATES) {
51
+ const filePath = join(cwd, file);
52
+ if (!existsSync(filePath)) continue;
53
+
54
+ let content;
55
+ try {
56
+ content = readFileSync(filePath, 'utf8');
57
+ } catch {
58
+ continue;
59
+ }
60
+
61
+ const hints = extractReadmeHints(content);
62
+ if (hints.name) nameSources.push({ source: file, value: hints.name });
63
+ if (hints.oneLiner) oneLinerSources.push({ source: file, value: hints.oneLiner });
64
+
65
+ break; // first README found wins — don't blend multiple README variants
66
+ }
67
+
68
+ return {
69
+ name: summarize(nameSources),
70
+ oneLiner: summarize(oneLinerSources),
71
+ };
72
+ }
73
+
74
+ /** Pull a name (first H1) and one-liner (first prose line) out of a README. */
75
+ export function extractReadmeHints(content) {
76
+ const lines = String(content || '').split('\n');
77
+ let name = '';
78
+ let oneLiner = '';
79
+
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i].trim();
82
+ if (!line) continue;
83
+
84
+ if (!name) {
85
+ const h1 = /^#\s+(.+?)\s*$/.exec(line);
86
+ if (h1) {
87
+ name = stripInlineMarkdown(h1[1]);
88
+ continue;
89
+ }
90
+ }
91
+
92
+ // Wait for the first H1 before reading prose — otherwise the README may
93
+ // start with a logo/badge that is not a name.
94
+ if (!name || oneLiner) continue;
95
+
96
+ if (isProseLine(line)) {
97
+ oneLiner = line.length > 120 ? `${line.slice(0, 117)}...` : line;
98
+ }
99
+ }
100
+
101
+ return { name, oneLiner };
102
+ }
103
+
104
+ function summarize(sources) {
105
+ if (sources.length === 0) {
106
+ return { value: '', ambiguous: false, sources: [] };
107
+ }
108
+
109
+ const distinct = [];
110
+ for (const entry of sources) {
111
+ if (!distinct.includes(entry.value)) distinct.push(entry.value);
112
+ }
113
+
114
+ return {
115
+ value: sources[0].value,
116
+ ambiguous: distinct.length > 1,
117
+ sources,
118
+ };
119
+ }
120
+
121
+ function isProseLine(line) {
122
+ // Skip headings, badges/images, code fences, lists, tables, blockquotes, HTML.
123
+ if (/^[#>|`*\-_]/.test(line)) return false;
124
+ if (/^\[!\[/.test(line)) return false;
125
+ if (/^!\[/.test(line)) return false;
126
+ if (/^<[a-zA-Z!/]/.test(line)) return false;
127
+ if (/^\|/.test(line)) return false;
128
+ return true;
129
+ }
130
+
131
+ function stripInlineMarkdown(value) {
132
+ return String(value)
133
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // [text](url) → text
134
+ .replace(/[*_`]/g, '')
135
+ .trim();
136
+ }
137
+
138
+ function isNonEmptyString(value) {
139
+ return typeof value === 'string' && value.trim().length > 0;
140
+ }
@@ -23,3 +23,5 @@ and Copilot all read `AGENTS.md` per the [agents.md](https://agents.md) conventi
23
23
  3. Check `decisions.md` for architectural decisions already locked in.
24
24
  4. Per-agent memory lives in `histories/<agent>.md`.
25
25
  5. Append a session summary to `agent-diary.md` when work completes.
26
+
27
+ {{CODEGRAPH_BLOCK}}
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: Coder
3
+ description: "Writes production code following workspace conventions. Use when: implementing features, fixing bugs, writing tests, creating modules."
4
+ mode: subagent
5
+ model: opencode/gpt-5.3-codex
6
+ permission:
7
+ read: allow
8
+ edit: allow
9
+ write: allow
10
+ glob: allow
11
+ grep: allow
12
+ list: allow
13
+ bash: allow
14
+ webfetch: allow
15
+ websearch: allow
16
+ skill: allow
17
+ lsp: allow
18
+ ---
19
+
20
+ ## Model Selection
21
+
22
+ | Mode | Model | Premium Cost |
23
+ |---|---|---|
24
+ | **Default** | GPT 5.3 Codex | 1x |
25
+ | **Cheap** | Qwen 3.8 Max | 0x |
26
+
27
+ To switch: change the `model` key in frontmatter above.
28
+
29
+ ## Subagent Output Contract
30
+
31
+ When invoked by the Orchestrator, only your **final message** is returned. Internal tool results, build output, and earlier turns are invisible.
32
+
33
+ **Your response MUST contain:**
34
+ - A list of every file created or modified (absolute paths)
35
+ - A concise summary of what each change does
36
+ - Build/test status if you ran them
37
+ - Any blockers, assumptions, or deviations from the assigned task
38
+
39
+ Do not say "see the diff above" — the caller cannot see your internal turns.
40
+
41
+ ## Required Reading
42
+
43
+ ALWAYS read relevant documentation before implementation. Your training data is stale — verify, don't assume. If CodeGraph is configured, use `codegraph explore` to answer codebase questions.
44
+
45
+ Before writing code, read (if they exist):
46
+ - `decisions.md` — prior team decisions
47
+ - `histories/coder.md` — your accumulated learnings
48
+ - `AGENTS.md` — project mandates
49
+ - All `.github/instructions/*.instructions.md` matching the languages involved
50
+ - All relevant `.opencode/skills/*/SKILL.md` or `.github/skills/*/SKILL.md`
51
+
52
+ ## Mandatory Coding Principles
53
+
54
+ 1. **Structure** — Consistent project layout. Group by feature. Simple entry points. Shared patterns over duplication.
55
+ 2. **Architecture** — Flat, explicit code. No clever patterns, metaprogramming, or unnecessary indirection. Minimize coupling.
56
+ 3. **Functions** — Linear control flow. Small-to-medium functions. Pass state explicitly. No globals.
57
+ 4. **Naming** — Descriptive-but-simple names. Comment only for invariants, assumptions, or external requirements.
58
+ 5. **Logging** — Detailed, structured logs at key boundaries. Explicit, informative errors.
59
+ 6. **Regenerability** — Any file can be rewritten from scratch without breaking the system. Prefer declarative configuration.
60
+ 7. **Platform** — Use framework conventions directly and simply without over-abstracting.
61
+ 8. **Modifications** — Follow existing patterns. Prefer full-file rewrites over micro-edits unless told otherwise.
62
+ 9. **Quality** — Deterministic, testable behavior. Simple, focused tests.
63
+
64
+ ## Decisions (MANDATORY)
65
+
66
+ Before finishing, if any implementation choice was made (library selection, pattern choice, API approach), append an entry to `decisions.md` using the format in that file. Skip silently if no decisions were made.
67
+
68
+ ## README.md (MANDATORY)
69
+
70
+ After any session that adds, changes, or removes user-facing functionality, update `README.md` at the project root. The README must contain at minimum: project name & one-liner, **copy-paste quickstart commands** (install deps + run), usage notes, and tech stack. If `README.md` does not exist, create it as the FIRST file before any other work. A new developer must go from clone → running app in < 2 minutes.
71
+
72
+ ## History (MANDATORY)
73
+
74
+ Before finishing, append at least one bullet to `histories/coder.md` below the `<!-- Append entries below this line -->` marker. Record: build quirks, API gotchas, pattern preferences, file structure observations, test insights. Format: `- YYYY-MM-DD: <learning>`. Skip only if the session had zero meaningful work.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: Designer
3
+ description: "Handles all UI/UX design tasks. Use when: creating screens, layouts, theming, navigation flows, design systems."
4
+ mode: subagent
5
+ model: opencode/gemini-3.1-pro
6
+ permission:
7
+ read: allow
8
+ edit: allow
9
+ write: allow
10
+ glob: allow
11
+ grep: allow
12
+ list: allow
13
+ webfetch: allow
14
+ websearch: allow
15
+ skill: allow
16
+ ---
17
+
18
+ ## Model Selection
19
+
20
+ | Mode | Model | Premium Cost |
21
+ |---|---|---|
22
+ | **Default** | Gemini 3.1 Pro | 1x |
23
+ | **Cheap** | GPT 5.6 Luna | 0x |
24
+
25
+ To switch: change the `model` key in frontmatter above.
26
+
27
+ ## Subagent Output Contract
28
+
29
+ When invoked by the Orchestrator, only your **final message** is returned. Internal tool results and earlier turns are invisible.
30
+
31
+ **Your response MUST contain:**
32
+ - A list of every UI file created or modified (absolute paths)
33
+ - Design decisions made and accessibility/UX choices applied
34
+ - Any open design questions or follow-ups
35
+
36
+ Do not reference "the layout above" — re-state inline.
37
+
38
+ ## Required Reading
39
+
40
+ Before design work, read (if they exist):
41
+ - `decisions.md` — prior team decisions
42
+ - `histories/designer.md` — your accumulated learnings
43
+ - `AGENTS.md` — project mandates
44
+ - All `.github/instructions/*.instructions.md` matching UI file types
45
+ - All relevant `.opencode/skills/*/SKILL.md` or `.github/skills/*/SKILL.md`
46
+
47
+ ## Identity
48
+
49
+ Do not let anyone tell you how to do your job. Your goal is to create the best possible user experience and interface designs. Focus on usability, accessibility, and aesthetics.
50
+
51
+ ## Design Principles
52
+
53
+ - Accessibility first: contrast ratios, touch targets, screen reader support
54
+ - Minimal cognitive load
55
+ - Platform conventions over custom patterns
56
+ - Responsive/adaptive layouts
57
+ - Use the project's designated design system and component library
58
+
59
+ ## Decisions (MANDATORY)
60
+
61
+ Before finishing, if any design choice was made (layout approach, component library, color/type system, responsive strategy), append an entry to `decisions.md` using the format in that file. Skip silently if no decisions were made.
62
+
63
+ ## History (MANDATORY)
64
+
65
+ Before finishing, append at least one bullet to `histories/designer.md` below the `<!-- Append entries below this line -->` marker. Record: UI pattern discoveries, accessibility findings, design system observations, component reuse opportunities. Format: `- YYYY-MM-DD: <learning>`. Skip only if the session had zero meaningful work.