oh-my-design-cli 1.7.2 → 1.8.1
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/.claude/hooks/lib/preferences-parser.cjs +10 -1
- package/.claude/hooks/lib/preferences-writer.cjs +118 -0
- package/.claude/hooks/post-edit-watch.cjs +217 -29
- package/.claude/hooks/session-end-foldin.cjs +61 -5
- package/.claude/hooks/session-state-loader.cjs +49 -1
- package/README.ja.md +4 -4
- package/README.ko.md +7 -7
- package/README.md +7 -7
- package/README.zh-TW.md +4 -4
- package/agents/omd-master.md +8 -5
- package/agents/omd-ux-engineer.md +9 -7
- package/agents/omd-ux-writer.md +1 -1
- package/data/reference-fingerprints.json +6140 -5534
- package/dist/bin/oh-my-design.js +1 -1
- package/dist/{install-skills-KDW74C5K.js → install-skills-7UUDOLG2.js} +28 -24
- package/dist/install-skills-7UUDOLG2.js.map +1 -0
- package/package.json +2 -1
- package/skills/omd-designer-review/SKILL.md +34 -0
- package/skills/omd-final-qa/SKILL.md +29 -0
- package/skills/omd-harness/SKILL.md +8 -1
- package/skills/omd-init/SKILL.md +7 -11
- package/skills/omd-kr-writer/SKILL.md +73 -3
- package/skills/omd-learn/SKILL.md +20 -0
- package/skills/omd-reference-capture/SKILL.md +10 -6
- package/skills/omd-taste/SKILL.md +79 -0
- package/web/references/3o3/DESIGN.md +454 -0
- package/web/references/abema/DESIGN.md +535 -0
- package/web/references/accupass/DESIGN.md +494 -0
- package/web/references/adobe/DESIGN.md +458 -0
- package/web/references/danawa/DESIGN.md +477 -0
- package/web/references/dropbox/DESIGN.md +445 -0
- package/web/references/firstory/DESIGN.md +487 -0
- package/web/references/funnow/DESIGN.md +431 -0
- package/web/references/google/DESIGN.md +499 -0
- package/web/references/greenvines/DESIGN.md +454 -0
- package/web/references/greeting/DESIGN.md +462 -0
- package/web/references/imweb/DESIGN.md +460 -0
- package/web/references/laundrygo/DESIGN.md +434 -0
- package/web/references/microsoft/DESIGN.md +508 -0
- package/web/references/miricanvas/DESIGN.md +464 -0
- package/web/references/modusign/DESIGN.md +524 -0
- package/web/references/omnichat/DESIGN.md +499 -0
- package/web/references/postype/DESIGN.md +431 -0
- package/web/references/rakuten/DESIGN.md +440 -0
- package/web/references/ringle/DESIGN.md +444 -0
- package/web/references/salesforce/DESIGN.md +473 -0
- package/web/references/stores/DESIGN.md +447 -0
- package/web/references/teamblind/DESIGN.md +497 -0
- package/web/references/timee/DESIGN.md +444 -0
- package/web/references/wantedly/DESIGN.md +437 -0
- package/dist/install-skills-KDW74C5K.js.map +0 -1
package/dist/bin/oh-my-design.js
CHANGED
|
@@ -26,7 +26,7 @@ var program = new Command();
|
|
|
26
26
|
program.name("oh-my-design").description("Bootstrap oh-my-design skills + agents into your project. After install, talk to your agent in natural language \u2014 no other CLI commands.").version(readPackageVersion()).showSuggestionAfterError(true).showHelpAfterError(true);
|
|
27
27
|
program.command("install-skills").description("Install omd skill files + canonical agents into agent directories (.claude/, .codex/, .opencode/). Interactive multiselect TUI by default \u2014 picks which skills + sub-agents to install.").option("--dir <path>", "Project root (defaults to cwd)").option("--agent <name...>", "Restrict to specific channels (claude-code | codex | opencode | cursor)").option("--force", "Overwrite existing files even without the omd marker").option("--all", "Skip the interactive TUI and install every shipped skill + agent (use in CI)").option("--skills <names>", "Comma-separated skill names to install (overrides TUI)", (v) => v.split(",").map((s) => s.trim()).filter(Boolean)).option("--agents-only <names>", "Comma-separated agent names to install (overrides TUI). Use --agents-only to disambiguate from --agent (channel selector).", (v) => v.split(",").map((s) => s.trim()).filter(Boolean)).option("--skills-only", "Install only the named skill files \u2014 skip sub-agents, hooks, and settings.json (minimal single-skill install, e.g. --skills claude-design --skills-only)").option("--global", "Install to the user-level dir (~/.claude/skills, available in every project) instead of this project. Writes skills + sub-agents; never touches global hooks/settings.").action(
|
|
28
28
|
async (opts) => {
|
|
29
|
-
const { runInstallSkills } = await import("../install-skills-
|
|
29
|
+
const { runInstallSkills } = await import("../install-skills-7UUDOLG2.js");
|
|
30
30
|
const validAgents = ["claude-code", "codex", "opencode", "cursor"];
|
|
31
31
|
const agents = opts.agent ? opts.agent.filter(
|
|
32
32
|
(a) => validAgents.includes(a)
|
|
@@ -157,6 +157,18 @@ function planForTarget(projectRoot, target) {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
var CHANNEL_DATA_DIRS = {
|
|
161
|
+
"claude-code": ".claude",
|
|
162
|
+
codex: ".codex",
|
|
163
|
+
opencode: null,
|
|
164
|
+
cursor: null
|
|
165
|
+
};
|
|
166
|
+
function dataDirFor(target, targets) {
|
|
167
|
+
if (target === "cursor") {
|
|
168
|
+
return targets.includes("claude-code") ? null : ".claude";
|
|
169
|
+
}
|
|
170
|
+
return CHANNEL_DATA_DIRS[target];
|
|
171
|
+
}
|
|
160
172
|
var MANAGED_HEADER = "<!-- omd:installed-skill \u2014 managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->";
|
|
161
173
|
var MANAGED_MARKER_SUBSTR = "omd:installed-skill";
|
|
162
174
|
function withManagedMarker(src) {
|
|
@@ -286,7 +298,7 @@ function installSettingsJson(packageRoot, projectRoot, force) {
|
|
|
286
298
|
writeFileSync(destPath, src);
|
|
287
299
|
return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
|
|
288
300
|
}
|
|
289
|
-
function installDataFile(packageRoot, projectRoot, channelDir, dataFilename, force, target
|
|
301
|
+
function installDataFile(packageRoot, projectRoot, channelDir, dataFilename, force, target) {
|
|
290
302
|
const skillLabel = `data:${dataFilename}`;
|
|
291
303
|
const srcPath = join2(packageRoot, "data", dataFilename);
|
|
292
304
|
const destPath = join2(projectRoot, channelDir, "data", dataFilename);
|
|
@@ -606,31 +618,20 @@ async function runInstallSkills(opts = {}) {
|
|
|
606
618
|
"opt-out-corpus.json"
|
|
607
619
|
];
|
|
608
620
|
for (const target of targets) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
} else if (target === "codex") {
|
|
614
|
-
for (const dataFile of dataFiles) {
|
|
615
|
-
results.push(installDataFile(packageRoot, installRoot, ".codex", dataFile, force));
|
|
616
|
-
}
|
|
617
|
-
} else if (target === "cursor" && !targets.includes("claude-code")) {
|
|
618
|
-
for (const dataFile of dataFiles) {
|
|
619
|
-
results.push(installDataFile(packageRoot, installRoot, ".claude", dataFile, force, "cursor"));
|
|
620
|
-
}
|
|
621
|
+
const dataDir = dataDirFor(target, targets);
|
|
622
|
+
if (!dataDir) continue;
|
|
623
|
+
for (const dataFile of dataFiles) {
|
|
624
|
+
results.push(installDataFile(packageRoot, installRoot, dataDir, dataFile, force, target));
|
|
621
625
|
}
|
|
622
626
|
}
|
|
623
627
|
for (const target of targets) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
catalogCount += installReferenceCatalog(packageRoot, installRoot, ".codex", force);
|
|
628
|
-
} else if (target === "cursor" && !targets.includes("claude-code")) {
|
|
629
|
-
catalogCount += installReferenceCatalog(packageRoot, installRoot, ".claude", force);
|
|
628
|
+
const dataDir = dataDirFor(target, targets);
|
|
629
|
+
if (dataDir) {
|
|
630
|
+
catalogCount += installReferenceCatalog(packageRoot, installRoot, dataDir, force);
|
|
630
631
|
}
|
|
631
632
|
}
|
|
632
633
|
for (const target of targets) {
|
|
633
|
-
const cd = target
|
|
634
|
+
const cd = CHANNEL_DATA_DIRS[target];
|
|
634
635
|
if (!cd) continue;
|
|
635
636
|
for (const helper of ["ctx-prime.cjs", "context.cjs"]) {
|
|
636
637
|
const srcHelper = join2(packageRoot, "scripts", helper);
|
|
@@ -648,9 +649,11 @@ async function runInstallSkills(opts = {}) {
|
|
|
648
649
|
"session-state-loader.cjs",
|
|
649
650
|
"post-edit-watch.cjs",
|
|
650
651
|
"session-end-foldin.cjs",
|
|
651
|
-
// Shared
|
|
652
|
-
// lib/ subdir; installHookFile preserves the relative path
|
|
653
|
-
|
|
652
|
+
// Shared modules required by the fold-in / state-loader / watch hooks.
|
|
653
|
+
// Live in a lib/ subdir; installHookFile preserves the relative path
|
|
654
|
+
// under .claude/hooks/.
|
|
655
|
+
join2("lib", "preferences-parser.cjs"),
|
|
656
|
+
join2("lib", "preferences-writer.cjs")
|
|
654
657
|
]) {
|
|
655
658
|
results.push(installHookFile(packageRoot, installRoot, hookFile, force));
|
|
656
659
|
}
|
|
@@ -725,6 +728,7 @@ async function runInstallSkills(opts = {}) {
|
|
|
725
728
|
return 0;
|
|
726
729
|
}
|
|
727
730
|
export {
|
|
731
|
+
dataDirFor,
|
|
728
732
|
runInstallSkills
|
|
729
733
|
};
|
|
730
|
-
//# sourceMappingURL=install-skills-
|
|
734
|
+
//# sourceMappingURL=install-skills-7UUDOLG2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/install-skills.ts","../src/core/agent-detect.ts"],"sourcesContent":["import * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport {\n readFileSync,\n readdirSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n cpSync,\n} from 'node:fs';\nimport { join, dirname, relative } from 'node:path';\nimport { homedir } from 'node:os';\nimport { createHash } from 'node:crypto';\nimport { fileURLToPath } from 'node:url';\nimport { detectInstalledAgents } from '../core/agent-detect.js';\n\nexport type SkillTarget = 'claude-code' | 'codex' | 'opencode' | 'cursor';\n\n/** Channels that host SKILL.md trees. Cursor is NOT one — it consumes a\n * `.cursor/rules` shim + the shared `.claude/data` catalog (issue #20). */\ntype SkillChannel = Exclude<SkillTarget, 'cursor'>;\n\nexport interface InstallSkillsOptions {\n dir?: string;\n agents?: SkillTarget[];\n force?: boolean;\n /** Non-interactive: install all skills + all agents without TUI prompt.\n * Default false → interactive multiselect when TTY available. */\n all?: boolean;\n /** Pre-select specific skill names from CLI flag (`--skills omd-init,omd-apply`).\n * Overrides interactive prompt when set. */\n skillsFilter?: string[];\n /** Pre-select specific agent names. Overrides interactive prompt when set. */\n agentsFilter?: string[];\n /** Minimal install: only the named skill files — skip sub-agents, data files,\n * hooks, and settings.json. Ideal for shipping a single standalone skill. */\n skillsOnly?: boolean;\n /** Install to the user-level dir (~/.claude/skills) instead of this project.\n * Writes skills + sub-agents (+ data); never touches global hooks/settings.\n * When unset and interactive, the TUI asks project-vs-global. */\n global?: boolean;\n}\n\ninterface InstallPlan {\n target: SkillChannel;\n destDir: string;\n layout: 'folder' | 'flat';\n}\n\nfunction findPackageRoot(): string | null {\n let cur = dirname(fileURLToPath(import.meta.url));\n for (let i = 0; i < 8; i++) {\n if (existsSync(join(cur, 'skills'))) return cur;\n const parent = dirname(cur);\n if (parent === cur) break;\n cur = parent;\n }\n return null;\n}\n\nfunction listShippedSkills(packageRoot: string): string[] {\n const skillsDir = join(packageRoot, 'skills');\n if (!existsSync(skillsDir)) return [];\n return readdirSync(skillsDir)\n .filter((name) => existsSync(join(skillsDir, name, 'SKILL.md')))\n .sort();\n}\n\n/**\n * Canonical agent definitions live at `agents/<name>.md` (markdown with YAML\n * frontmatter). Channel-specific files (.claude/agents/*.md, .codex/agents/*.toml)\n * are generated artifacts — never the source of truth.\n *\n * The package ships only `agents/` and the generator emits per-channel files\n * into the user's project on `omd install-skills`.\n */\nfunction listCanonicalAgents(packageRoot: string): string[] {\n const dir = join(packageRoot, 'agents');\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((name) => name.startsWith('omd-') && name.endsWith('.md'))\n .sort();\n}\n\ninterface ParsedAgent {\n name: string;\n description: string;\n tools: string[];\n model: string;\n body: string;\n}\n\n/** Parse `agents/<name>.md` YAML frontmatter + body into structured form. */\nfunction parseCanonicalAgent(packageRoot: string, filename: string): ParsedAgent {\n const src = readFileSync(join(packageRoot, 'agents', filename), 'utf8');\n const match = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/.exec(src);\n if (!match) {\n throw new Error(`agents/${filename}: missing YAML frontmatter`);\n }\n const fm = match[1];\n const body = match[2];\n const grab = (key: string): string => {\n const re = new RegExp(`^${key}:\\\\s*(.+)$`, 'm');\n const m = re.exec(fm);\n return m ? m[1].trim().replace(/^[\"']|[\"']$/g, '') : '';\n };\n return {\n name: grab('name') || filename.replace(/\\.md$/, ''),\n description: grab('description'),\n tools: grab('tools')\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean),\n model: grab('model') || 'sonnet',\n body,\n };\n}\n\n/** Map Claude tool names to Codex tool names (best-effort). */\nfunction claudeToolsToCodex(tools: string[]): string[] {\n const m: Record<string, string> = {\n Read: 'read_file',\n Write: 'write_file',\n Edit: 'edit_file',\n Bash: 'shell',\n Glob: 'search',\n Grep: 'search',\n WebFetch: 'web_fetch',\n WebSearch: 'search',\n Agent: 'spawn_agent',\n TaskCreate: 'task',\n TaskUpdate: 'task',\n TaskList: 'task',\n };\n const out = new Set<string>();\n for (const t of tools) out.add(m[t] ?? t.toLowerCase());\n return [...out].sort();\n}\n\n/** Map Claude model alias to Codex/OpenAI model id (best-effort). */\nfunction claudeModelToCodex(model: string): string {\n const m: Record<string, string> = {\n haiku: 'gpt-4.1-mini',\n sonnet: 'gpt-4.1',\n opus: 'gpt-4.1',\n };\n return m[model.toLowerCase()] ?? 'gpt-4.1';\n}\n\n/** Render a canonical agent as a Claude Code subagent file.\n * IMPORTANT: Claude Code's subagent parser requires YAML frontmatter (`---`)\n * as the FIRST line of the file. Any preceding content (HTML comments, blank\n * lines) breaks discovery. So we encode the managed-by-omd marker as a\n * custom frontmatter field (`omd_managed: true`) instead of an HTML comment.\n */\nfunction renderClaudeAgent(a: ParsedAgent): string {\n const fm = [\n '---',\n `name: ${a.name}`,\n `description: ${a.description}`,\n `tools: ${a.tools.join(', ')}`,\n `model: ${a.model}`,\n `omd_managed: true`,\n '---',\n '',\n ].join('\\n');\n return fm + a.body;\n}\n\n/** Render a canonical agent as a Codex TOML file (declarative pointer). */\nfunction renderCodexAgent(a: ParsedAgent): string {\n const tools = claudeToolsToCodex(a.tools);\n const model = claudeModelToCodex(a.model);\n const desc = a.description.replace(/\"/g, '\\\\\"');\n return [\n `[agent]`,\n `name = \"${a.name}\"`,\n `description = \"${desc}\"`,\n `model = \"${model}\"`,\n `max_threads = 1`,\n `allowed_tools = [${tools.map((t) => `\"${t}\"`).join(', ')}]`,\n '',\n `instructions = \"\"\"`,\n `Source of truth: agents/${a.name}.md (canonical). The full role spec is`,\n `mirrored to .claude/agents/${a.name}.md when installed for Claude Code.`,\n `Follow that spec verbatim regardless of channel.`,\n '',\n `Codex notes:`,\n `- Spawn sub-agents via spawn_agent with names matching .codex/agents/<name>.toml`,\n `- Use shell to invoke CLI helpers (omd init prepare, omd remember, git apply, npx axe-core, npx lighthouse)`,\n `- All artifacts go inside .omd/runs/run-<latest>/ (or skills/omd-lab-02-design-harness/runs/<lab-version>-...)`,\n `\"\"\"`,\n '',\n ].join('\\n');\n}\n\nfunction planForTarget(projectRoot: string, target: SkillChannel): InstallPlan {\n switch (target) {\n case 'claude-code':\n return {\n target,\n destDir: join(projectRoot, '.claude', 'skills'),\n layout: 'folder',\n };\n case 'codex':\n // Official Codex skill discovery path is `.agents/skills/<name>/SKILL.md`\n // (developers.openai.com/codex/skills) — NOT `.codex/skills`. Folder layout\n // so multi-file skills (scripts/, references/) install + run.\n return {\n target,\n destDir: join(projectRoot, '.agents', 'skills'),\n layout: 'folder',\n };\n case 'opencode':\n // OpenCode loads `.opencode/skills/<name>/SKILL.md` (opencode.ai/docs/skills)\n // as folder skills — the old flat `.opencode/agents/<name>.md` couldn't host\n // a skill's scripts/references.\n return {\n target,\n destDir: join(projectRoot, '.opencode', 'skills'),\n layout: 'folder',\n };\n }\n}\n\n/**\n * Channel → shared data dir (`<dir>/data/…`) for read-only data assets\n * (catalog JSONs, reference DESIGN.md catalog, ctx-prime helper scripts).\n * Single lookup table replacing the repeated if-else/ternary chains (issue #28).\n * `null` = the channel hosts no data dir of its own:\n * - opencode ships skills only (no data channel yet);\n * - cursor reads the SHARED `.claude/data` path (issue #20) — resolved by\n * `dataDirFor()` below, which also applies the claude-code dedup guard.\n * (Helper scripts intentionally stay claude-code/codex only — a cursor-only\n * install gets the catalog + JSONs but no ctx-prime, matching the shim's scope.)\n */\nconst CHANNEL_DATA_DIRS: Record<SkillTarget, '.claude' | '.codex' | null> = {\n 'claude-code': '.claude',\n codex: '.codex',\n opencode: null,\n cursor: null,\n};\n\n/**\n * Where a target's data assets (data JSONs + reference catalog) land. Cursor\n * reuses `.claude/data` so the catalog location stays single (issue #20) —\n * but ONLY when claude-code isn't also selected: the claude-code pass already\n * writes there, and a second pass would double-copy the catalog.\n */\nexport function dataDirFor(\n target: SkillTarget,\n targets: SkillTarget[]\n): '.claude' | '.codex' | null {\n if (target === 'cursor') {\n return targets.includes('claude-code') ? null : '.claude';\n }\n return CHANNEL_DATA_DIRS[target];\n}\n\nconst MANAGED_HEADER =\n '<!-- omd:installed-skill — managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->';\n\n// Substring shared by old (line 1) and new (after-frontmatter) marker formats.\n// Used for detection so upgrades from pre-v1.7.2 installs still refresh.\nconst MANAGED_MARKER_SUBSTR = 'omd:installed-skill';\n\n/**\n * Write the managed marker AFTER the YAML frontmatter block so the very first\n * line of the installed file is `---`. Claude Code's skill loader reads the\n * frontmatter `name`/`description` only when `---` is line 1 — a leading HTML\n * comment makes it register the comment as the description (issue #17).\n *\n * If the source has no frontmatter (shouldn't happen for SKILL.md, but be\n * defensive), fall back to prepending the marker.\n */\nfunction withManagedMarker(src: string): string {\n // \\r?\\n: a CRLF checkout (Windows core.autocrlf) must not miss the\n // frontmatter and fall back to a line-1 marker — that reintroduces #17.\n const fm = /^(---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n)([\\s\\S]*)$/.exec(src);\n if (!fm) {\n return MANAGED_HEADER + '\\n\\n' + src;\n }\n return fm[1] + MANAGED_HEADER + '\\n\\n' + fm[2];\n}\n\n/**\n * Detect an omd-managed installed-skill file. Matches both the new format\n * (marker after frontmatter) and the legacy format (marker on line 1) by\n * scanning the first ~30 lines for the marker substring. This keeps upgrades\n * working: a pre-v1.7.2 file with the marker at line 1 is still recognized as\n * managed and gets refreshed rather than skipped as user-edited drift.\n */\nfunction isManagedSkillFile(content: string): boolean {\n if (!content) return false;\n const head = content.split('\\n', 30).join('\\n');\n return head.includes(MANAGED_MARKER_SUBSTR);\n}\n\ninterface InstallResult {\n target: SkillTarget;\n skill: string;\n destPath: string;\n status: 'created' | 'updated' | 'unchanged' | 'skipped-drift' | 'skipped-incompat';\n}\n\n// Skill-tree entries that must never be installed (runtime state, caches, OS cruft).\nconst IGNORED_SKILL_ENTRIES = new Set(['.runtime', '__pycache__', '.DS_Store']);\n\n/**\n * A skill may restrict itself to specific agent channels via a frontmatter line\n * `x-omd-channels: claude-code` (comma/space separated). Returns the allowed\n * channels, or null when channel-agnostic (installs anywhere). Used by skills that\n * depend on a particular agent runtime — e.g. claude-design needs Claude Code's\n * claude-in-chrome MCP + Bash/python/node and is therefore claude-code only.\n */\nfunction parseSkillChannels(skillMd: string): SkillChannel[] | null {\n const fm = /^---\\n([\\s\\S]*?)\\n---/.exec(skillMd);\n if (!fm) return null;\n const m = /^x-omd-channels:\\s*(.+)$/m.exec(fm[1]);\n if (!m) return null;\n const valid: SkillChannel[] = ['claude-code', 'codex', 'opencode'];\n const list = m[1]\n .split(/[,\\s]+/)\n .map((s) => s.trim())\n .filter((s): s is SkillChannel => (valid as string[]).includes(s));\n return list.length > 0 ? list : null;\n}\n\n/**\n * The agent channels a skill can install into: its declared `x-omd-channels`\n * (if any), else all channels. All three channels now use folder layout\n * (.claude/skills, .agents/skills, .opencode/skills) so multi-file skills with\n * scripts/references install everywhere — the only restriction is what the skill\n * itself declares (e.g. claude-design needs a browser-driving runtime).\n */\nfunction skillSupportedChannels(packageRoot: string, skill: string): SkillChannel[] {\n return (\n parseSkillChannels(readFileSync(join(packageRoot, 'skills', skill, 'SKILL.md'), 'utf8')) ??\n (['claude-code', 'codex', 'opencode'] as SkillChannel[])\n );\n}\n\nfunction installOne(\n packageRoot: string,\n plan: InstallPlan,\n skill: string,\n force: boolean\n): InstallResult {\n const skillDir = join(packageRoot, 'skills', skill);\n const src = readFileSync(join(skillDir, 'SKILL.md'), 'utf8');\n // Marker goes AFTER frontmatter so `---` stays line 1 (issue #17).\n const managed = withManagedMarker(src);\n\n // Respect a skill's declared channel restriction (frontmatter `x-omd-channels:`).\n const channels = parseSkillChannels(src);\n if (channels && !channels.includes(plan.target)) {\n return {\n target: plan.target,\n skill,\n destPath: join(plan.destDir, skill + '.md'),\n status: 'skipped-incompat',\n };\n }\n\n // A skill is \"multi-file\" when it ships more than SKILL.md (scripts/, references/, …).\n const extras = readdirSync(skillDir).filter(\n (n) => n !== 'SKILL.md' && !IGNORED_SKILL_ENTRIES.has(n)\n );\n const isMultiFile = extras.length > 0;\n\n // Flat channels (codex/opencode) store a skill as a single <skill>.md and cannot\n // host a multi-file skill's scripts/references — such skills are claude-code only.\n if (plan.layout !== 'folder' && isMultiFile) {\n return {\n target: plan.target,\n skill,\n destPath: join(plan.destDir, skill + '.md'),\n status: 'skipped-incompat',\n };\n }\n\n const destPath =\n plan.layout === 'folder'\n ? join(plan.destDir, skill, 'SKILL.md')\n : join(plan.destDir, skill + '.md');\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n // Drift protection guards the user-editable SKILL.md. Single-file skills can\n // short-circuit on \"unchanged\"; multi-file skills always re-sync their tree.\n if (exists && existing === managed && !isMultiFile) {\n return { target: plan.target, skill, destPath, status: 'unchanged' };\n }\n // Drift = a file we didn't write. Detect the marker anywhere in the head\n // (new after-frontmatter position OR legacy line-1 position) so pre-v1.7.2\n // installs are recognized as managed and refreshed, not skipped.\n if (exists && !isManagedSkillFile(existing) && !force) {\n return { target: plan.target, skill, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n\n // Copy the rest of the skill tree (scripts/, references/, …) for folder layout.\n if (plan.layout === 'folder' && isMultiFile) {\n const destSkillDir = join(plan.destDir, skill);\n for (const entry of extras) {\n cpSync(join(skillDir, entry), join(destSkillDir, entry), {\n recursive: true,\n filter: (s) => !/(\\/__pycache__|\\/\\.runtime|\\.pyc$|\\.DS_Store$)/.test(s),\n });\n }\n }\n\n return {\n target: plan.target,\n skill,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/** Install a hook script from package's .claude/hooks/ to project. */\nfunction installHookFile(\n packageRoot: string,\n projectRoot: string,\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = `hook:${filename}`;\n const srcPath = join(packageRoot, '.claude', 'hooks', filename);\n const destPath = join(projectRoot, '.claude', 'hooks', filename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Install / merge .claude/settings.json. We MERGE hooks (don't clobber user\n * customizations) by checking if the omd-managed `_doc` field is present.\n * Without --force, if a user-edited settings.json exists (no _doc field),\n * we skip with `skipped-drift`.\n */\nfunction installSettingsJson(\n packageRoot: string,\n projectRoot: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = 'settings:.claude/settings.json';\n const srcPath = join(packageRoot, '.claude', 'settings.json');\n const destPath = join(projectRoot, '.claude', 'settings.json');\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Check if it's the omd-managed version\n try {\n const parsed = JSON.parse(existing);\n if (typeof parsed._doc === 'string' && parsed._doc.includes('OmD')) {\n // managed — overwrite\n } else {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n } catch {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Copy a read-only data asset (reference-fingerprints.json, vocabulary.json, …)\n * from the package's `data/` into the project's `.claude/data/` or `.codex/data/`.\n * The skill reads these at runtime — they replace the deprecated `omd init recommend` CLI.\n */\nfunction installDataFile(\n packageRoot: string,\n projectRoot: string,\n channelDir: string,\n dataFilename: string,\n force: boolean,\n // Cursor reuses the `.claude` data dir (single catalog path) — callers pass\n // an explicit target so the results table reports the real channel.\n target: SkillTarget\n): InstallResult {\n const skillLabel = `data:${dataFilename}`;\n\n const srcPath = join(packageRoot, 'data', dataFilename);\n const destPath = join(projectRoot, channelDir, 'data', dataFilename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n // Data files are pure copies — no managed header (would corrupt JSON).\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Honor user customization unless --force\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/**\n * Generate a per-channel agent file from the canonical `agents/<name>.md`.\n *\n * Channel = 'claude' → emits `.claude/agents/<name>.md` (markdown w/ frontmatter)\n * Channel = 'codex' → emits `.codex/agents/<name>.toml` (TOML pointer)\n */\nfunction installAgentFile(\n packageRoot: string,\n projectRoot: string,\n channel: 'claude' | 'codex',\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = channel === 'claude' ? 'claude-code' : 'codex';\n const skillLabel = `agent:${filename}`;\n\n const parsed = parseCanonicalAgent(packageRoot, filename);\n const rendered =\n channel === 'claude' ? renderClaudeAgent(parsed) : renderCodexAgent(parsed);\n\n const destFilename =\n channel === 'claude' ? filename : filename.replace(/\\.md$/, '.toml');\n const destPath = join(\n projectRoot,\n channel === 'claude' ? '.claude' : '.codex',\n 'agents',\n destFilename\n );\n\n // For Claude Code: managed marker is encoded as `omd_managed: true` INSIDE the\n // frontmatter (rendered above) — no HTML comment can precede `---` or the\n // subagent loader rejects the file.\n // For Codex: TOML allows leading comments, so `# omd:installed-agent ...` is fine.\n const managed =\n channel === 'claude'\n ? rendered\n : '# omd:installed-agent — generated from agents/' +\n filename +\n ' by `omd install-skills`. Do not edit; rerun the command to refresh.\\n\\n' +\n rendered;\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n if (exists && existing === managed) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n\n // Drift detection sentinels:\n // Claude — look for `omd_managed: true` line inside frontmatter\n // Codex — look for `# omd:installed-agent` comment\n const isManaged =\n channel === 'claude'\n ? /\\nomd_managed:\\s*true\\b/.test(existing)\n : existing.startsWith('# omd:installed-agent');\n\n if (exists && !isManaged && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/**\n * Copy the reference catalog (`web/references/<id>/DESIGN.md`) into the project's\n * `.claude/data/references/<id>/DESIGN.md` so it's reachable on clean npx installs\n * — where there is no `node_modules` and no dev `web/references` (issue #16).\n *\n * Only DESIGN.md per id is copied (not _promo.json/_research.md/screenshots) to\n * keep the install lean. Idempotent: skips ids whose DESIGN.md already matches.\n * Returns the number of catalog files written (created or updated).\n */\nfunction installReferenceCatalog(\n packageRoot: string,\n installRoot: string,\n channelDir: string,\n force: boolean\n): number {\n const srcRoot = join(packageRoot, 'web', 'references');\n if (!existsSync(srcRoot)) return 0;\n const destRoot = join(installRoot, channelDir, 'data', 'references');\n\n let written = 0;\n for (const id of readdirSync(srcRoot)) {\n const srcDesign = join(srcRoot, id, 'DESIGN.md');\n if (!existsSync(srcDesign)) continue;\n const destDesign = join(destRoot, id, 'DESIGN.md');\n const src = readFileSync(srcDesign, 'utf8');\n if (existsSync(destDesign)) {\n const existing = readFileSync(destDesign, 'utf8');\n if (existing === src) continue;\n if (!force) continue; // honor user edits unless --force\n }\n mkdirSync(dirname(destDesign), { recursive: true });\n writeFileSync(destDesign, src, 'utf8');\n written++;\n }\n return written;\n}\n\n/**\n * Cursor channel shim — Cursor has no skill/agent surface; it consumes a\n * project rule at `.cursor/rules/omd-design.mdc`. Frontmatter, body, and the\n * body-hash marker below mirror the omd:sync skill's cursor template EXACTLY\n * (skills/omd-sync/SKILL.md, \"whole\" mode: hash = sha256 of the body text,\n * 12-char hex prefix), so a later omd:sync run reads the installer-written\n * file as `clean` rather than drifted (issue #20).\n */\nconst CURSOR_RULE_BODY = [\n 'The authoritative design spec lives at `@DESIGN.md` (repo root). Open and read before generating/modifying UI.',\n '',\n 'Pending preference corrections: `@.omd/preferences.md`.',\n '',\n 'Precedence: DESIGN.md > preferences.md > framework defaults.',\n].join('\\n');\n\nfunction renderCursorRule(): string {\n const hash = createHash('sha256').update(CURSOR_RULE_BODY).digest('hex').slice(0, 12);\n return [\n '---',\n 'description: Authoritative brand & UI design system. Read DESIGN.md before UI work.',\n 'globs:',\n ' - \"**/*.tsx\"',\n ' - \"**/*.jsx\"',\n ' - \"**/*.vue\"',\n ' - \"**/*.svelte\"',\n ' - \"**/*.css\"',\n ' - \"**/*.scss\"',\n ' - \"**/tailwind.config.*\"',\n ' - \"**/components/**\"',\n ' - \"**/app/**/page.*\"',\n 'alwaysApply: false',\n '---',\n '',\n `<!-- omd:start v=1 hash=${hash} -->`,\n CURSOR_RULE_BODY,\n '<!-- omd:end -->',\n '',\n ].join('\\n');\n}\n\nfunction installCursorRule(installRoot: string, force: boolean): InstallResult {\n const target: SkillTarget = 'cursor';\n const skillLabel = 'rule:omd-design.mdc';\n const destPath = join(installRoot, '.cursor', 'rules', 'omd-design.mdc');\n const rendered = renderCursorRule();\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === rendered) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n // The omd marker block doubles as the managed sentinel (matching omd:sync's\n // whole-mode rules): a file without it is user content → drift unless --force.\n if (exists && !existing.includes('<!-- omd:start') && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, rendered, 'utf8');\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\nconst STATUS_LABEL: Record<InstallResult['status'], string> = {\n created: pc.green('created'),\n updated: pc.cyan('updated'),\n unchanged: pc.dim('unchanged'),\n 'skipped-drift': pc.yellow('skipped'),\n 'skipped-incompat': pc.yellow('skipped (claude-code only)'),\n};\n\nfunction autoDetectTargets(projectRoot: string): SkillTarget[] {\n const presence = detectInstalledAgents(projectRoot);\n const targets: SkillTarget[] = [];\n if (presence.claudeCode) targets.push('claude-code');\n if (presence.codex) targets.push('codex');\n if (presence.opencode) targets.push('opencode');\n // Cursor hosts no skills — its channel writes the .cursor/rules shim + the\n // shared .claude/data catalog (issue #20). Only when .cursor is detected;\n // the no-signal fallback below stays skill-channel-only so we never drop a\n // .cursor dir into projects that don't use Cursor.\n if (presence.cursor) targets.push('cursor');\n if (targets.length === 0) {\n // Fallback: install for all three skill channels so user gets coverage\n // even without explicit signal. Idempotent so cost is low.\n return ['claude-code', 'codex', 'opencode'];\n }\n return targets;\n}\n\nexport async function runInstallSkills(\n opts: InstallSkillsOptions = {}\n): Promise<number> {\n const projectRoot = opts.dir ?? process.cwd();\n const packageRoot = findPackageRoot();\n if (!packageRoot) {\n console.error(pc.red('omd install-skills: package data not found'));\n return 1;\n }\n\n const allSkills = listShippedSkills(packageRoot);\n if (allSkills.length === 0) {\n console.error(pc.red('omd install-skills: no skills found in package'));\n return 1;\n }\n const allAgents = listCanonicalAgents(packageRoot);\n\n const force = opts.force ?? false;\n const minimal = opts.skillsOnly === true;\n // Install scope: 'project' (<cwd>/.claude/…) or 'global' (~/.claude/…). --global\n // forces it; otherwise the interactive TUI asks. Global writes skills + sub-agents\n // (+ data) to the user-level dir but never touches global hooks/settings.json.\n let scope: 'project' | 'global' = opts.global ? 'global' : 'project';\n\n p.intro(\n pc.bold('omd install-skills') +\n pc.dim(` (${relative(process.cwd(), projectRoot) || '.'})`)\n );\n\n // Each dimension (scope / skills / sub-agents / channels) is resolved\n // independently: a CLI flag pins it; otherwise we prompt — but only when stdin\n // is a TTY and --all wasn't passed. This is the key fix: `--skills X` or\n // `--skills-only` no longer suppress the *channel* (where to install) prompt —\n // they only pin the dimension they name.\n const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);\n const interactive = isTTY && !opts.all;\n\n const detected = autoDetectTargets(projectRoot);\n // Real presence (not the all-3 fallback) — used for hint labels + prompt defaults.\n const presence = detectInstalledAgents(projectRoot);\n const actuallyDetected: SkillTarget[] = [\n presence.claudeCode ? 'claude-code' : null,\n presence.codex ? 'codex' : null,\n presence.opencode ? 'opencode' : null,\n presence.cursor ? 'cursor' : null,\n ].filter((x): x is SkillTarget => x !== null);\n\n // --- Scope (project vs global) — --global pins it, else ask / default project.\n if (!opts.global && interactive) {\n const scopeResult = await p.select({\n message: 'Install scope · 어디에 설치할까요?',\n options: [\n { value: 'project', label: 'Project', hint: `${relative(process.cwd(), projectRoot) || '.'}/.claude/skills · 이 프로젝트만` },\n { value: 'global', label: 'Global', hint: '~/.claude/skills · 모든 프로젝트 (skills + sub-agents, hooks/settings 제외)' },\n ],\n initialValue: 'project',\n });\n if (p.isCancel(scopeResult)) { p.cancel('Install cancelled.'); return 130; }\n scope = scopeResult as 'project' | 'global';\n }\n\n // --- Skills — --skills pins it, else ask / default ALL.\n let skills: string[];\n if (opts.skillsFilter) {\n skills = allSkills.filter((s) => opts.skillsFilter!.includes(s));\n } else if (interactive) {\n const skillResult = await p.multiselect({\n message: 'Skills · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allSkills.map((s) => ({ value: s, label: s, hint: 'omd skill' })),\n initialValues: allSkills,\n required: true,\n });\n if (p.isCancel(skillResult)) { p.cancel('Install cancelled.'); return 130; }\n skills = skillResult as string[];\n } else {\n skills = allSkills;\n }\n\n // --- Sub-agents — dropped by --skills-only, else --agents pins, else ask / ALL.\n let canonicalAgents: string[];\n if (minimal) {\n canonicalAgents = [];\n } else if (opts.agentsFilter) {\n canonicalAgents = allAgents.filter((a) => opts.agentsFilter!.includes(a.replace(/\\.md$/, '')));\n } else if (interactive && allAgents.length > 0) {\n const agentResult = await p.multiselect({\n message: 'Sub-agents · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allAgents.map((a) => ({ value: a, label: a.replace(/\\.md$/, ''), hint: 'subagent' })),\n initialValues: allAgents,\n required: false,\n });\n if (p.isCancel(agentResult)) { p.cancel('Install cancelled.'); return 130; }\n canonicalAgents = agentResult as string[];\n } else {\n canonicalAgents = allAgents;\n }\n\n // --- Channels / targets — the \"where do I install\" choice.\n // --agent pins it. Otherwise, in a TTY we ASK — limited to the channels the\n // selected skills actually support (claude-design is claude-code only, so its\n // picker shows just Claude Code). Non-TTY / --all falls back to auto-resolution.\n const supportedTargets = ((): SkillTarget[] => {\n const set = new Set<SkillTarget>(skills.flatMap((s) => skillSupportedChannels(packageRoot, s)));\n // Cursor consumes no skills — its channel install (.cursor/rules shim +\n // shared .claude/data catalog) is skill-independent, so always offer it.\n set.add('cursor');\n return (['claude-code', 'codex', 'opencode', 'cursor'] as SkillTarget[]).filter((t) => set.has(t));\n })();\n const channelLabel: Record<SkillTarget, string> = {\n 'claude-code': 'Claude Code',\n codex: 'Codex',\n opencode: 'OpenCode',\n cursor: 'Cursor',\n };\n const channelDir: Record<SkillTarget, string> = {\n 'claude-code': '.claude',\n codex: '.codex',\n opencode: '.opencode',\n cursor: '.cursor',\n };\n let targets: SkillTarget[];\n if (opts.agents) {\n targets = opts.agents;\n } else if (interactive) {\n const defaults = actuallyDetected.filter((t) => supportedTargets.includes(t));\n const targetResult = await p.multiselect({\n message: 'Agent channels · 어디에 설치할까요? · space = 토글 · enter = 확인',\n options: supportedTargets.map((t) => ({\n value: t,\n label: channelLabel[t],\n hint: actuallyDetected.includes(t) ? `${channelDir[t]}/ detected` : '',\n })) as { value: SkillTarget; label: string; hint?: string }[],\n initialValues: defaults.length > 0 ? defaults : supportedTargets,\n required: true,\n });\n if (p.isCancel(targetResult)) { p.cancel('Install cancelled.'); return 130; }\n targets = targetResult as SkillTarget[];\n } else {\n // Non-interactive (CI / piped / --all): resolve from flags + detection,\n // narrowed to channels the selected skills support.\n targets = opts.all\n ? (['claude-code', 'codex', 'opencode'] as SkillTarget[])\n : minimal\n ? (actuallyDetected.length > 0 ? actuallyDetected : (['claude-code'] as SkillTarget[]))\n : detected;\n const narrowed = targets.filter((t) => supportedTargets.includes(t));\n if (narrowed.length > 0) targets = narrowed;\n }\n\n // Global scope roots everything at the home dir, so plan dirs resolve to\n // ~/.claude/skills, ~/.claude/agents, etc. Project scope uses cwd (or --dir).\n const installRoot = scope === 'global' ? homedir() : projectRoot;\n // Cursor hosts no SKILL.md tree — it's excluded from skill plans and handled\n // below via the .cursor/rules shim + shared data copies (issue #20).\n const skillChannelTargets = targets.filter(\n (t): t is SkillChannel => t !== 'cursor'\n );\n const plans = skillChannelTargets.map((t) => planForTarget(installRoot, t));\n\n p.log.message(\n pc.bold('Scope: ') +\n pc.cyan(scope) +\n pc.dim(scope === 'global' ? ' (~/.claude)' : ` (${relative(process.cwd(), projectRoot) || '.'})`)\n );\n p.log.message(\n pc.bold(`Skills (${skills.length}): `) +\n skills.map((s) => pc.cyan(s)).join(', ')\n );\n if (minimal) {\n // --skills-only: sub-agents are intentionally skipped (minimal single-skill\n // install). Clear BEFORE the summary so we never print agents we won't write.\n canonicalAgents = [];\n p.log.message(pc.bold('Agents: ') + pc.dim('skipped (--skills-only)'));\n } else if (canonicalAgents.length > 0) {\n p.log.message(\n pc.bold(`Agents (${canonicalAgents.length}): `) +\n canonicalAgents.map((a) => pc.cyan(a.replace(/\\.md$/, ''))).join(', ')\n );\n }\n p.log.message(\n pc.bold('Targets: ') + targets.map((t) => pc.cyan(t)).join(', ')\n );\n\n const results: InstallResult[] = [];\n // Count of reference-catalog DESIGN.md files copied (issue #16) — surfaced in\n // the install summary. Declared here so the outro (outside `if (!minimal)`) sees it.\n let catalogCount = 0;\n for (const plan of plans) {\n for (const skill of skills) {\n results.push(installOne(packageRoot, plan, skill, force));\n }\n }\n\n // Generate per-channel sub-agent definitions from the canonical `agents/`.\n // This is the v2 portable source-of-truth pattern (oh-my-agent style).\n // `canonicalAgents` is already resolved above by the TUI / --agents filter.\n for (const target of targets) {\n if (target === 'claude-code') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, installRoot, 'claude', filename, force));\n }\n } else if (target === 'codex') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, installRoot, 'codex', filename, force));\n }\n }\n // OpenCode currently has no agent-definition channel — skills only.\n }\n\n if (!minimal) {\n // Cursor channel: write the `.cursor/rules` shim (the exact content omd:sync\n // renders for .cursor/rules/omd-design.mdc) so Cursor reads DESIGN.md before\n // UI work. No skills/agents/hooks — the shim plus the shared .claude/data\n // copies below are the whole Cursor install (issue #20).\n if (targets.includes('cursor')) {\n results.push(installCursorRule(installRoot, force));\n }\n\n // Ship the read-only data assets (reference fingerprints, controlled vocab,\n // human-readable tag matrix, opt-out corpus) so skills + hooks can run entirely\n // on the host CLI's own model — no external API keys.\n const dataFiles = [\n 'reference-fingerprints.json',\n 'reference-tags.md',\n 'vocabulary.json',\n 'synonyms.json',\n 'opt-out-corpus.json',\n ];\n // Channel→dir resolution (incl. the cursor shared-`.claude/data` dedup guard,\n // issue #20) lives in dataDirFor — single source for all three copy loops.\n for (const target of targets) {\n const dataDir = dataDirFor(target, targets);\n if (!dataDir) continue;\n for (const dataFile of dataFiles) {\n results.push(installDataFile(packageRoot, installRoot, dataDir, dataFile, force, target));\n }\n }\n\n // Ship the reference catalog (DESIGN.md per id) into .claude/data/references\n // so omd:init can resolve a reference on clean npx installs — no node_modules,\n // no dev web/references (issue #16). Skipped under --skills-only (handled by the\n // enclosing `if (!minimal)`). Codex gets the same copy under .codex/data.\n // Same dataDirFor single-path rule as the data JSONs above — Cursor reads\n // .claude/data/references, never a second catalog location.\n for (const target of targets) {\n const dataDir = dataDirFor(target, targets);\n if (dataDir) {\n catalogCount += installReferenceCatalog(packageRoot, installRoot, dataDir, force);\n }\n }\n\n // Copy ctx-prime.cjs (+ its companion context.cjs) into .claude/data/scripts/\n // so /omd-harness CTX-PRIME works without the package dir on npx installs\n // (issue #18 / harness OMD_DIR resolution).\n // Note: base table (no cursor special-case) — helper scripts are\n // claude-code/codex only; a cursor-only install ships no ctx-prime.\n for (const target of targets) {\n const cd = CHANNEL_DATA_DIRS[target];\n if (!cd) continue;\n for (const helper of ['ctx-prime.cjs', 'context.cjs']) {\n const srcHelper = join(packageRoot, 'scripts', helper);\n if (!existsSync(srcHelper)) continue;\n const destHelper = join(installRoot, cd, 'data', 'scripts', helper);\n const srcTxt = readFileSync(srcHelper, 'utf8');\n if (existsSync(destHelper) && readFileSync(destHelper, 'utf8') === srcTxt) continue;\n mkdirSync(dirname(destHelper), { recursive: true });\n writeFileSync(destHelper, srcTxt, 'utf8');\n }\n }\n\n // Hooks + settings.json are PROJECT-SCOPED only — a global install must not\n // mutate the user's global Claude config / make hooks fire in every project.\n if (scope === 'project' && targets.includes('claude-code')) {\n for (const hookFile of [\n 'skill-activation.cjs',\n 'session-state-loader.cjs',\n 'post-edit-watch.cjs',\n 'session-end-foldin.cjs',\n // Shared modules required by the fold-in / state-loader / watch hooks.\n // Live in a lib/ subdir; installHookFile preserves the relative path\n // under .claude/hooks/.\n join('lib', 'preferences-parser.cjs'),\n join('lib', 'preferences-writer.cjs'),\n ]) {\n results.push(installHookFile(packageRoot, installRoot, hookFile, force));\n }\n // settings.json (with merge, never clobber user)\n results.push(installSettingsJson(packageRoot, installRoot, force));\n }\n } // !minimal — skills-only skips data files, hooks, and settings.json\n\n p.log.message(pc.bold('\\nResults:'));\n for (const r of results) {\n const rel = relative(installRoot, r.destPath);\n p.log.message(\n ` ${STATUS_LABEL[r.status]} ${pc.dim(r.target.padEnd(12))} ${rel}`\n );\n }\n\n const driftCount = results.filter((r) => r.status === 'skipped-drift').length;\n const writtenCount = results.filter(\n (r) => r.status === 'created' || r.status === 'updated'\n ).length;\n\n if (driftCount > 0) {\n p.outro(\n pc.yellow(\n `${writtenCount} written, ${driftCount} skipped (existing files lack the omd marker — rerun with --force to overwrite).`\n )\n );\n return 0;\n }\n\n // Minimal single-skill install (--skills-only): no omd onboarding, no agents/hooks.\n // Ideal for shipping a standalone skill (e.g. claude-design) to people who don't\n // want the rest of the omd toolchain.\n if (minimal) {\n for (const r of results.filter((x) => x.status === 'skipped-incompat')) {\n p.log.warn(\n `${pc.bold(r.skill)} ${pc.dim('skipped for ')}${pc.cyan(r.target)}${pc.dim(' — declares x-omd-channels (channel not supported).')}`\n );\n }\n const installed = results.filter(\n (r) => r.status === 'created' || r.status === 'updated'\n );\n if (installed.length === 0) {\n p.outro(pc.yellow('Nothing installed — no compatible skill/channel match.'));\n return 0;\n }\n p.outro(\n pc.green(\n `Done. Installed ${skills.map((s) => pc.bold(s)).join(', ')} ${scope === 'global' ? 'globally (~/.claude/skills)' : `for ${targets.join(', ')}`}.`\n ) +\n pc.dim(' → restart your agent, then use the skill (e.g. ') +\n pc.cyan('/claude-design') +\n pc.dim(').')\n );\n return 0;\n }\n\n // Friendly next-step nudge after successful install.\n // The first prompt is kept identical to the README's \"Your first 60 seconds\"\n // block so the README, the terminal, and the postinstall message all teach\n // the same activation moment. Bilingual (EN + KR) so an English reader is not\n // handed a Korean-only outro.\n const nextSteps = [\n `${pc.bold('Restart your agent, then type your first prompt:')}`,\n '',\n ` ${pc.cyan('EN')} ${pc.dim('Set up our design system — Toss-style, for a family meal-tracking app.')}`,\n ` ${pc.cyan('KR')} ${pc.dim('토스 스타일로 가족 식단 공유 앱 디자인 시스템 잡아줘')}`,\n '',\n `${pc.dim('Your agent runs omd:init and writes DESIGN.md. Then build against it:')}`,\n ` ${pc.cyan('EN')} ${pc.dim('Design the home screen.')} ${pc.cyan('KR')} ${pc.dim('홈 화면 디자인해줘')}`,\n '',\n `${pc.dim('Full walkthrough → \"Your first 60 seconds\" in the README. Routing is automatic — no slash command needed.')}`,\n `${pc.dim('Power user: ')}${pc.cyan('/omd-harness <task>')}${pc.dim(' — jump straight into the pipeline.')}`,\n '',\n `${pc.yellow('⚠ Already-running session?')} ${pc.dim('Run `/agents` to reload — or quit (Cmd+Q on macOS) and relaunch. Without reload, hooks/agents do not load.')}`,\n ].join('\\n');\n p.note(nextSteps, 'Next');\n\n // Counts derived from what was actually resolved/installed — never hardcoded,\n // so the outro can't drift from the real skill/agent/hook set (or the README).\n const hookCount = scope === 'project' && targets.includes('claude-code') ? 4 : 0;\n if (catalogCount > 0) {\n p.log.message(\n pc.bold('Reference catalog: ') +\n pc.cyan(`${catalogCount}`) +\n pc.dim(' DESIGN.md copied → .claude/data/references/<id>/DESIGN.md'),\n );\n }\n p.outro(\n pc.green(\n `Done. ${skills.length} skills · ${canonicalAgents.length} sub-agents · ${hookCount} hooks · ${catalogCount} catalog refs installed (${writtenCount} files)${scope === 'global' ? ' globally (~/.claude)' : ''}.`,\n ),\n );\n return 0;\n}\n\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type AgentId = 'claude-code' | 'codex' | 'opencode' | 'cursor' | 'unknown';\n\nexport function detectCallingAgent(): AgentId {\n const env = process.env;\n\n if (env.CLAUDECODE === '1' || env.CLAUDE_CODE === '1' || env.CLAUDE_CODE_TASK_ID) {\n return 'claude-code';\n }\n if (env.CODEX_SESSION_ID || env.CODEX || env.OPENAI_CODEX) {\n return 'codex';\n }\n if (env.OPENCODE || env.OPENCODE_SESSION) {\n return 'opencode';\n }\n if (env.CURSOR_SESSION_ID || env.CURSOR_AGENT) {\n return 'cursor';\n }\n\n return 'unknown';\n}\n\nexport interface AgentPresence {\n claudeCode: boolean;\n codex: boolean;\n opencode: boolean;\n cursor: boolean;\n}\n\nexport function detectInstalledAgents(projectRoot: string): AgentPresence {\n return {\n claudeCode:\n existsSync(join(projectRoot, '.claude')) ||\n existsSync(join(projectRoot, 'CLAUDE.md')),\n codex:\n existsSync(join(projectRoot, '.codex')) ||\n existsSync(join(projectRoot, 'AGENTS.md')) ||\n existsSync(join(projectRoot, 'AGENTS.override.md')),\n opencode:\n existsSync(join(projectRoot, '.opencode')) ||\n existsSync(join(projectRoot, 'opencode.json')) ||\n existsSync(join(projectRoot, 'opencode.jsonc')),\n cursor:\n existsSync(join(projectRoot, '.cursor')) ||\n existsSync(join(projectRoot, '.cursorrules')),\n };\n}\n"],"mappings":"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","names":["existsSync","join","existsSync","join"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-design-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Bootstrap oh-my-design skills + agents into your project. After install, talk to your AI coding agent in natural language — no other CLI commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"skills/omd-learn",
|
|
16
16
|
"skills/omd-remember",
|
|
17
17
|
"skills/omd-sync",
|
|
18
|
+
"skills/omd-taste",
|
|
18
19
|
"skills/omd-reference-capture",
|
|
19
20
|
"skills/omd-asset-fetch",
|
|
20
21
|
"skills/omd-experiment-gallery",
|
|
@@ -144,3 +144,37 @@ input에 `prior_report_path` 포함되면:
|
|
|
144
144
|
- 항목별로 RESOLVED / UNRESOLVED / NEW로 표시
|
|
145
145
|
|
|
146
146
|
Round 2에도 UNRESOLVED BLOCK 있으면 orchestrator로 BLOCK escalation.
|
|
147
|
+
|
|
148
|
+
## 8. 취향 캡처 — review → taste loop (최종 phase)
|
|
149
|
+
|
|
150
|
+
review report를 emit한 **후에** 이번 run의 finding들을 한 번 스캔해, 반복 패턴을 취향 후보로 제안한다. report 자체는 advisory 그대로 — 이 phase가 유일하게 쓰기를 일으킬 수 있는 지점이고, 그것도 **사용자가 동의한 경우에만**.
|
|
151
|
+
|
|
152
|
+
### 후보 조건 (둘 중 하나)
|
|
153
|
+
|
|
154
|
+
1. **같은 axis ≥2회** — 이번 run의 finding을 axis로 분류(radius / color / spacing / typo / voice)했을 때 같은 axis가 2회 이상 등장
|
|
155
|
+
2. **기존 pending preference와 매칭** — `.omd/preferences.md`가 존재하면 read해서, finding이 `status: pending` 엔트리의 scope와 같은 축이면 1회여도 후보 (반복의 증거가 이미 파일에 있으므로)
|
|
156
|
+
|
|
157
|
+
`.omd/preferences.md`가 없으면 조건 2는 생략 — 파일을 만들지 않는다.
|
|
158
|
+
|
|
159
|
+
### 제안 (run당 질문 1개 max)
|
|
160
|
+
|
|
161
|
+
후보가 1개 이상이면 **단 한 번** 묻는다: "이 패턴, 취향으로 기록할까요?"
|
|
162
|
+
|
|
163
|
+
- **Claude Code**: AskUserQuestion — 후보가 여러 개면 multiSelect 옵션으로 묶어서 한 질문에 (후보당 한 줄: axis + 발생 횟수 + 요약)
|
|
164
|
+
- **다른 채널 (Codex / OpenCode)**: 같은 내용을 산문 질문 하나로
|
|
165
|
+
|
|
166
|
+
### 동의 시 기록
|
|
167
|
+
|
|
168
|
+
선택된 후보는 **omd:remember 스킬의 기록 절차를 그대로 수행**해 기록한다 — 포맷을 손으로 흉내 내 직접 append하지 말 것 (writer는 omd:remember 하나; id 생성·scope 매핑·frontmatter 생성·heading 규칙 전부 그 절차를 따른다):
|
|
169
|
+
|
|
170
|
+
- `signal: review` / `confidence: inferred` / `status: pending`
|
|
171
|
+
- `source_context`: 이번 review report 경로 (예: `.reviews/designer-review-round-1.md`)
|
|
172
|
+
|
|
173
|
+
### 금지
|
|
174
|
+
|
|
175
|
+
- **자동 기록 금지** — 질문 없이 append 절대 불가 (omd:remember의 "묻지 말고 기록" 룰은 사용자 발화용 — review 추론에는 적용되지 않는다)
|
|
176
|
+
- **자동 fold 금지** — `status: pending`으로만 기록. DESIGN.md 반영은 평소의 fold-in 임계/게이트(omd:learn)가 결정
|
|
177
|
+
- run당 질문 2개 이상 금지 — 후보가 많아도 multiSelect 하나로 배칭
|
|
178
|
+
- 거절된 후보를 같은 세션에서 재제안 금지
|
|
179
|
+
|
|
180
|
+
> **수동 검증**: 같은 artifact에서 radius WARN 2건이 나오는 review를 돌리면, report 출력 후 "이 패턴, 취향으로 기록할까요?" 질문이 정확히 1회 뜨고, 동의 시 `.omd/preferences.md`에 `signal: review` / `confidence: inferred` / `status: pending` 엔트리 1개가 append되어야 한다 (DESIGN.md는 변경 없음).
|
|
@@ -151,3 +151,32 @@ round 2:
|
|
|
151
151
|
|
|
152
152
|
- `omd-designer-review` 보고서 ← `prior_reviews`로 참조 (해소 여부 확인)
|
|
153
153
|
- `omd-locale-adapter` 결과물 ← rubric [5]에서 검증
|
|
154
|
+
|
|
155
|
+
## 8. 취향 캡처 — QA → taste loop (최종 phase)
|
|
156
|
+
|
|
157
|
+
verdict를 emit한 **후에**, 이번 run의 rubric FAIL들을 한 번 스캔해 반복 위반을 취향 후보로 제안한다. read-only 원칙은 그대로 — artifact·DESIGN.md는 절대 건드리지 않으며, 이 스킬이 트리거할 수 있는 **유일한 쓰기**는 사용자가 명시적으로 동의한 뒤의 preference append이고, 그것도 `omd:remember`의 canonical 절차 그대로다.
|
|
158
|
+
|
|
159
|
+
### 후보 조건 (둘 중 하나)
|
|
160
|
+
|
|
161
|
+
1. **반복 rubric 위반** — 같은 rubric item의 FAIL이 이번 run에서 2회 이상 (여러 artifact/locale에 걸쳐, 또는 round 1·2 연속 동일 item). item을 axis로 환원: [1] Brand consistency → 위반 토큰 종류에 따라 color/spacing/radius, [2] Typography hierarchy → typo, [3] Voice register → voice
|
|
162
|
+
2. **기존 pending preference와 매칭** — `.omd/preferences.md`가 존재하면 read해서, FAIL이 `status: pending` 엔트리의 scope와 같은 축이면 1회여도 후보
|
|
163
|
+
|
|
164
|
+
`.omd/preferences.md`가 없으면 조건 2는 생략 — 파일을 만들지 않는다. a11y/performance/links([6]-[8])는 취향이 아니라 hard rule — 후보에서 제외.
|
|
165
|
+
|
|
166
|
+
### 제안 (run당 질문 1개 max)
|
|
167
|
+
|
|
168
|
+
후보가 1개 이상이면 **단 한 번** 묻는다: "이 패턴, 취향으로 기록할까요?"
|
|
169
|
+
|
|
170
|
+
- **Claude Code**: AskUserQuestion — 후보 여러 개는 multiSelect 옵션으로 배칭 (후보당 한 줄: axis + FAIL 횟수 + 요약)
|
|
171
|
+
- **다른 채널**: 같은 내용을 산문 질문 하나로
|
|
172
|
+
|
|
173
|
+
동의된 후보는 **omd:remember 스킬의 기록 절차를 그대로 수행**해 기록 (직접 포맷 모방 금지 — writer는 omd:remember 하나) — `signal: review` / `confidence: inferred` / `status: pending`, `source_context`는 이번 QA report 경로 (예: `.reviews/final-qa-round-1.md`).
|
|
174
|
+
|
|
175
|
+
### 금지
|
|
176
|
+
|
|
177
|
+
- 질문 없는 자동 기록 절대 불가 — review 추론은 사용자 동의가 전제
|
|
178
|
+
- **자동 fold 금지** — `status: pending` 기록까지만. DESIGN.md 반영은 omd:learn의 평소 임계/게이트가 결정
|
|
179
|
+
- run당 질문 2개 이상 금지, 거절된 후보의 같은 세션 재제안 금지
|
|
180
|
+
- 이 phase가 verdict나 round cap에 영향을 주면 안 됨 (rubric 8 items 고정 유지)
|
|
181
|
+
|
|
182
|
+
> **수동 검증**: KR/EN 두 artifact에서 rubric [3] Voice register가 모두 FAIL인 QA를 돌리면, verdict 출력 후 "이 패턴, 취향으로 기록할까요?" 질문이 정확히 1회 뜨고, 동의 시 `.omd/preferences.md`에 `scope: voice` / `signal: review` / `confidence: inferred` / `status: pending` 엔트리가 append되어야 한다 (artifact·DESIGN.md는 변경 없음).
|
|
@@ -571,7 +571,14 @@ master에게 전달되는 prompt 첫 단락은 **반드시** 다음을 포함:
|
|
|
571
571
|
|
|
572
572
|
### 선택 절차
|
|
573
573
|
|
|
574
|
-
1. brand
|
|
574
|
+
1. brand reference DESIGN.md의 §1 (Visual Theme) + fingerprint `category`를 읽고 위 표에서 1순위 archetype 선택. reference DESIGN.md는 다음 우선순위로 resolve (omd:init Phase 4.1과 동일한 카탈로그 resolution order):
|
|
575
|
+
|
|
576
|
+
<!-- omd:catalog-resolution-order — omd-init/omd-reference-capture SKILL.md + agents/omd-master.md 와 동일 순서 강제. drift guard: test/unit/core/catalog-resolution-order.test.ts -->
|
|
577
|
+
|
|
578
|
+
1. `.claude/data/references/<id>/DESIGN.md` (installer가 복사 — npx 설치 기본 경로)
|
|
579
|
+
2. `node_modules/oh-my-design-cli/web/references/<id>/DESIGN.md` (로컬 npm 설치 직접 경로)
|
|
580
|
+
3. `web/references/<id>/DESIGN.md` (개발 레포)
|
|
581
|
+
4. `https://oh-my-design.kr/design-systems/<id>.md` 를 fetch (WebFetch 또는 `curl -fsSL`) — 200이면 본문이 곧 reference DESIGN.md. 가져온 내용을 `.claude/data/references/<id>/DESIGN.md`로 캐시해 다음부터는 로컬 캐시(경로 1)로 잡히게 한다.
|
|
575
582
|
2. 사용자가 같은 brand로 **이미 한 번 실험**했으면 (`.omd/runs/INDEX.md`에 기록) → 2순위 사용해서 variation 제공
|
|
576
583
|
3. 사용자가 명시 ("center 정렬로", "carousel로") → 그대로 따름
|
|
577
584
|
4. 선택된 archetype을 `experiment-meta.json`의 `hero_archetype` 필드에 명시 (gallery 표시용)
|
package/skills/omd-init/SKILL.md
CHANGED
|
@@ -122,18 +122,14 @@ Phase 4.2~6은 건너뛴다.
|
|
|
122
122
|
|
|
123
123
|
### 4.1 경로 결정
|
|
124
124
|
|
|
125
|
-
선택된 id를 `<id>`로 하고, 다음 순서로 Read:
|
|
125
|
+
선택된 id를 `<id>`로 하고, 다음 순서로 Read (먼저 존재하는 것 사용):
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
2. `node_modules/oh-my-design-cli/web/references/<id>/DESIGN.md`
|
|
129
|
-
3. `web/references/<id>/DESIGN.md` (개발 환경)
|
|
127
|
+
<!-- omd:catalog-resolution-order — omd-harness/omd-reference-capture SKILL.md + agents/omd-master.md 와 동일 순서 강제. drift guard: test/unit/core/catalog-resolution-order.test.ts -->
|
|
130
128
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
가져온 내용을 `.claude/data/references/<id>/DESIGN.md`로 저장해
|
|
136
|
-
다음 실행부터는 로컬 캐시(경로 1)로 잡히게 한다.
|
|
129
|
+
1. `.claude/data/references/<id>/DESIGN.md` (installer가 복사 — npx 설치 기본 경로)
|
|
130
|
+
2. `node_modules/oh-my-design-cli/web/references/<id>/DESIGN.md` (로컬 npm 설치 직접 경로)
|
|
131
|
+
3. `web/references/<id>/DESIGN.md` (개발 레포)
|
|
132
|
+
4. `https://oh-my-design.kr/design-systems/<id>.md` 를 fetch (WebFetch 또는 `curl -fsSL`) — 1~3 로컬 경로가 전부 없을 때 (npx 설치가 기본 경로라 흔한 상황). 200이면 응답 본문이 곧 reference DESIGN.md다. 가져온 내용을 `.claude/data/references/<id>/DESIGN.md`로 저장해 다음 실행부터는 로컬 캐시(경로 1)로 잡히게 한다.
|
|
137
133
|
|
|
138
134
|
4까지 전부 실패하면 **절대 DESIGN.md를 임의로 지어내지 말 것.** 사용자에게
|
|
139
135
|
"레퍼런스 `<id>` 원문을 찾지 못했어요 (오프라인이거나 카탈로그 미배포).
|
|
@@ -274,7 +270,7 @@ args: --force
|
|
|
274
270
|
|
|
275
271
|
omd:sync skill이 `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/omd-design.mdc` shim 3종을 생성/갱신하고 `.omd/sync.lock.json` 업데이트.
|
|
276
272
|
|
|
277
|
-
**옵션 B**: omd:sync skill을 호출할 수 없는 환경이면 직접 Write로 shim 작성. 템플릿은 `skills/omd-sync/SKILL.md` §"shim body" 참조. 최소한:
|
|
273
|
+
**옵션 B**: omd:sync skill을 호출할 수 없는 환경이면 직접 Write로 shim 작성. 템플릿은 `.claude/skills/omd-sync/SKILL.md` (설치 경로; dev 레포에서는 `skills/omd-sync/SKILL.md`) §"shim body" 참조. 최소한:
|
|
278
274
|
- `CLAUDE.md`: managed block ("<!-- omd:managed:start --> ... <!-- omd:managed:end -->") 안에 DESIGN.md 참조 + 핵심 token 요약
|
|
279
275
|
- `AGENTS.md`: 동일 패턴
|
|
280
276
|
- `.cursor/rules/omd-design.mdc`: 전체 파일 omd 전용 — frontmatter + DESIGN.md 인용
|
|
@@ -20,7 +20,7 @@ inputs:
|
|
|
20
20
|
...
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
지원 preset 12개.
|
|
23
|
+
지원 preset 12개. 각 preset의 핵심 spec은 아래 **§9 (preset 요약 spec)** 에 self-contained로 수록 — 이 SKILL.md만으로 12개 preset 전부 실행 가능. (풀 9-field spec + verbatim 한국어 examples + 출처는 dev 레포의 `data/research/2026-05-18-kr-style-presets.md` — npx 배포본에는 미포함.)
|
|
24
24
|
|
|
25
25
|
| # | preset_id | 종결 | 분량 (한글) | 용도 |
|
|
26
26
|
|---|---|---|---|---|
|
|
@@ -39,11 +39,11 @@ inputs:
|
|
|
39
39
|
|
|
40
40
|
**호출 시 preset 미지정 → `toss-tech-design` 적용.**
|
|
41
41
|
|
|
42
|
-
본 문서의 § 1~8은 default preset (`toss-tech-design`)의 상세 spec. 다른 preset 선택 시
|
|
42
|
+
본 문서의 § 1~8은 default preset (`toss-tech-design`)의 상세 spec. 다른 preset 선택 시 §9의 해당 preset 요약 spec 룰로 substitution.
|
|
43
43
|
|
|
44
44
|
### 전환 매트릭스 (자주 쓰는 6개)
|
|
45
45
|
|
|
46
|
-
같은 콘텐츠를 다른 preset으로 옮길 때 standard rewrite
|
|
46
|
+
같은 콘텐츠를 다른 preset으로 옮길 때 standard rewrite 룰 — 아래 핵심 변환 + §9 spec만으로 수행 가능. (확장 5-step rule은 dev 레포 research doc §14 — 배포본 미포함.)
|
|
47
47
|
|
|
48
48
|
| from | to | 핵심 변환 |
|
|
49
49
|
|---|---|---|
|
|
@@ -224,6 +224,76 @@ Toss 글은 단락 2~3개 당 이미지 1개. oh-my-design 본문에서:
|
|
|
224
224
|
- 첫 단락 "안녕하세요" 인사 포함
|
|
225
225
|
- 가져가도 좋은 것 / 따라하면 안 되는 것 = 각 5개
|
|
226
226
|
|
|
227
|
+
## 9. Preset 요약 spec (non-default 11개)
|
|
228
|
+
|
|
229
|
+
toss-tech-design 외 preset 호출 시 아래 요약 spec이 §1 (voice) / §5 (안티패턴) / §6 (어휘) 룰을 대체한다. 분량은 §0 표의 분량 컬럼, 구조(§2~4)는 preset 성격에 맞게 준용.
|
|
230
|
+
|
|
231
|
+
### karrot-neighborly — 당근 동네체
|
|
232
|
+
- register: 해요체 (가끔 "~답니다"/"~지요") · 문장 25~45자
|
|
233
|
+
- opening: "안녕하세요, [동네] 이웃 여러분." / closing: "오늘도 따뜻한 하루 보내세요."
|
|
234
|
+
- 금지: 차가운 비즈니스 한자어("고객", "유저") · 외래어 남발("커뮤니티"보단 "동네") · 단정적 -다 종결
|
|
235
|
+
- 선호: "동네/이웃/우리/근처" lexicon · 다정한 호명("이웃님") · 골목·계절·온도가 묻어나는 묘사어
|
|
236
|
+
|
|
237
|
+
### brunch-maker-popular — 브런치 에세이체
|
|
238
|
+
- register: 해요체 80% + 회상 해체 혼용 (1~2문단당 한 번 "-했다") · 문장 40~80자, 시처럼 끊기는 호흡
|
|
239
|
+
- opening: 개인 일화 한 줄 ("그날 저녁, 나는 ~") / closing: 여운 남기는 단문 ("그래서 나는 오늘도 ~.")
|
|
240
|
+
- 금지: 보고서식 bullet · 차트 인용 · 단정적 톤("~해야 한다")
|
|
241
|
+
- 선호: 1인칭 "나"의 일화 · 감각어(냄새·빛·소리) · 단문/만연체 교차 · 은유 · 짧은 의문문("정말 그럴까?")
|
|
242
|
+
|
|
243
|
+
### naver-d2-engineering — NAVER D2 기술체
|
|
244
|
+
- register: 하십시오체(-합니다/-입니다) · 문장 40~70자, 명확한 주어-서술 구조
|
|
245
|
+
- opening: "안녕하세요. [팀명] [이름]입니다. 이 글에서는 ~을 다룹니다." / closing: "이 글이 ~ 도움이 되길 바랍니다."
|
|
246
|
+
- 금지: 과도한 1인칭 감정 표현 · 비속어/인터넷 약어 · "~해요" 혼용 (문서 내 통일성)
|
|
247
|
+
- 선호: 정확한 기술 용어 + 영문 병기 · "먼저/다음으로/마지막으로" 구조 신호어 · 코드 블록·도식과 본문 1:1
|
|
248
|
+
|
|
249
|
+
### biz-formal-report — 보고서 사무체
|
|
250
|
+
- register: 하십시오체(-합니다) 또는 명사형 종결(-함/-임/-하였음) · 문장 30~55자, 짧고 단정
|
|
251
|
+
- opening: "1. 개요 / 본 보고서는 ~을 목적으로 작성되었습니다." / closing: "이상입니다." / "끝."
|
|
252
|
+
- 금지: 1인칭 "저/우리" 남발 (회사·부서 단위로 대체) · 감정 형용사 · 의문문 · 이모지/외래어 남용
|
|
253
|
+
- 선호: 섹션 번호(1.1, 1.2.1) · 명사형 종결("진행함", "확인됨") · 사무 한자어("검토/보고/회신/추진") · 결론→근거 역피라미드 · 핵심 수치 bold
|
|
254
|
+
|
|
255
|
+
### academic-paper — 학술 논문체
|
|
256
|
+
- register: 해라체(-한다/-이다) 평어체 · 문장 50~90자, 복문 빈번
|
|
257
|
+
- opening: "본 연구는 ~을 목적으로 한다." / closing: "후속 연구에서는 ~을 다룰 필요가 있다." / "본 연구의 한계는 ~이다."
|
|
258
|
+
- 금지: 비격식체 · 1인칭 단수("필자"·"본 연구자"로 대체) · 감정 형용사 · 인용 없는 단정
|
|
259
|
+
- 선호: 한자어 비중 높음("선행연구/유의수준/검정") · 수동·피동("측정되었다") · 괄호 인용(저자, 연도) · "따라서/그러나/한편" 접속어
|
|
260
|
+
|
|
261
|
+
### journalism-broadsheet — 신문 기사체
|
|
262
|
+
- register: 해라체(-다/-었다) · 문장 35~60자, 리드 문장 명확
|
|
263
|
+
- opening: 리드 = 핵심 사실 한 줄 ("~가 ~했다고 ~일 밝혔다.") / closing: "한편, ~." (배경 정보 마무리)
|
|
264
|
+
- 금지: 1인칭 (기명 칼럼 제외) · "~요/~네요" · 추측만 나열 · 감정 형용사
|
|
265
|
+
- 선호: 5W1H 리드 · 직접 인용("~라고 밝혔다/말했다/강조했다") · 출처 표기("OOO 대표") · "한편/이에 대해/앞서"
|
|
266
|
+
|
|
267
|
+
### kakao-warm-product — 카카오 따뜻한 프로덕트체
|
|
268
|
+
- register: 해요체, Toss보다 약간 더 감정적 · 문장 20~40자 (microcopy는 더 짧게)
|
|
269
|
+
- opening: "~ 축하해요!" / "~을(를) 시작해볼까요?" / closing: "오늘도 좋은 하루 보내세요." / "더 알아보기 →"
|
|
270
|
+
- 금지: 비즈니스 한자어("승인/수행/처리" → 부드러운 동사) · 부정·경고 직설("실패/오류" → "다시 시도해볼까요?") · 명령형("입력하시오" → "입력해주세요")
|
|
271
|
+
- 선호: "축하해요/고마워요/함께해요" · "혜택/선물/응원" 어휘 · 짧은 의문문("같이 시작해볼까요?")
|
|
272
|
+
|
|
273
|
+
### line-global-saas — LINE 한국어 (cross-locale)
|
|
274
|
+
- register: 하십시오체(-합니다), D2보다 한자어 비중 낮음 · 문장 35~55자, 번역 친화적 단순 구조
|
|
275
|
+
- opening: "이 글에서는 ~을 소개합니다." / "안녕하세요, LINE의 ~입니다." / closing: "감사합니다." / "다음 글에서 ~을 이어 다루겠습니다."
|
|
276
|
+
- 금지: 한국 내수 전용 비유·시사 (동시 번역 시 문제) · 4자 숙어("일석이조") · 비격식체
|
|
277
|
+
- 선호: SVO 단순 구조·능동태 · 영문 약어 병기("LINE 디자인 시스템(LDS)") · terminology 통일 · 중립 호명("독자/사용자")
|
|
278
|
+
|
|
279
|
+
### academic-lecture-essay — 교양 강연체
|
|
280
|
+
- register: 해요체 우세 + 학술 한자어 자유 혼용 · 문장 40~70자, 강의처럼 호흡 길게
|
|
281
|
+
- opening: "여러분, ~을 한 번 생각해볼까요?" / closing: "결국 ~인 거죠." / "그래서 저는 ~라고 생각해요."
|
|
282
|
+
- 금지: 보고서식 bullet · 인용 없는 단정
|
|
283
|
+
- 선호: "~인 거죠/~라는 거예요" · 청자에게 묻는 구조("그렇다면 왜 그럴까요?") · 비유와 일상 예시 · 학문 한자어 즉시 풀이("엔트로피, 그러니까 무질서의 정도")
|
|
284
|
+
|
|
285
|
+
### emotional-brand — 패션 커머스 트렌디체
|
|
286
|
+
- register: 해요체 + 체언/명사 종결 자유 혼용("오늘의 픽.") · 헤드라인 15~30자, 본문 40자 내외
|
|
287
|
+
- opening: 명사형 후크 ("이번 주의 무드." / "여름밤, 우리가 입어야 할 것.") / closing: "지금, [브랜드]에서." / "더 보기 →"
|
|
288
|
+
- 금지: 보고서 한자어 · "~합니다" 일변도 (브랜드 거리감) · 길고 설명적인 문장
|
|
289
|
+
- 선호: 외래어·영문 자유("무드/감도/베이직/에디트") · 컬러·재질·실루엣 어휘 · 시즌·시간성 cue("이번 주/오늘 밤/5월의") · 짧은 명령형("입어볼 것.")
|
|
290
|
+
|
|
291
|
+
### legal-disclosure — 법무 약관체
|
|
292
|
+
- register: 하십시오체(-합니다) 우세 + 명사 종결("~함") · 문장 50~100자, 의도적으로 길고 한정조건 빽빽
|
|
293
|
+
- opening: "본 약관은 ~을 규정함을 목적으로 합니다." / "제1조 (목적)" / closing: "본 약관은 ~일부터 시행합니다." / "부칙. ~"
|
|
294
|
+
- 금지: 1·2인칭 친밀 표현 · 비격식체 · 감정 형용사 · 모호한 수식("좀", "약간")
|
|
295
|
+
- 선호: "~합니다/~됩니다/~할 수 있습니다" · 수동·피동("간주됩니다/처리됩니다") · 정의 조항("이 약관에서 사용하는 용어의 정의는 다음과 같습니다.") · 법률 한자어("의무/책임/면책/준수") · 조·항·호 번호 체계
|
|
296
|
+
|
|
227
297
|
---
|
|
228
298
|
|
|
229
299
|
이 스킬은 oh-my-design 블로그 한국어 본문의 **단일 source of truth voice guide**. 모든 KR 글은 이 guide를 따른다.
|
|
@@ -83,6 +83,23 @@ spacing (1 pending):
|
|
|
83
83
|
Review .omd/preferences.md for details.
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
## Fold-in 제안에서 호출된 경우 (`.omd/foldin-proposal.json`)
|
|
87
|
+
|
|
88
|
+
SessionStart 컨텍스트의 OMD FOLD-IN PROPOSAL → AskUserQuestion 승인 경로로 호출되었으면 Phase 2 확인은 이미 끝난 것 — 다시 묻지 말 것.
|
|
89
|
+
|
|
90
|
+
제안 없이 사용자가 직접 omd:learn을 부른 경우에도 `.omd/foldin-proposal.json`이
|
|
91
|
+
`"status": "proposed"`로 존재하면: 그 scopes를 이번 폴드 대상에 포함할지 Phase 2에서
|
|
92
|
+
함께 확인하고, 처리 후 아래와 동일하게 status를 갱신한다 (proposed인 채로 방치 금지 —
|
|
93
|
+
다음 세션이 또 물어본다).
|
|
94
|
+
|
|
95
|
+
- **승인된 scope만** Phase 3-4로 처리. 미승인 scope의 pending 엔트리는 건드리지 않는다
|
|
96
|
+
- 처리 후 `.omd/foldin-proposal.json`의 status를 Edit 툴로 갱신:
|
|
97
|
+
- 전부 반영 → `"status": "applied"` + `"applied_at": "<ISO timestamp>"` 필드 추가
|
|
98
|
+
- 일부만 반영 → `"status": "partial"` + `scopes` 배열을 **남은(미승인) scope만**으로 갱신
|
|
99
|
+
- 전부 거절("나중에") → `"status": "snoozed"` + `"snoozed_at": "<ISO timestamp>"` 필드 추가
|
|
100
|
+
- status 값은 **JSON 계약상 영문 고정** (`proposed`/`applied`/`partial`/`snoozed`) —
|
|
101
|
+
번역·한글화 금지 (훅이 문자열 비교로 읽는다)
|
|
102
|
+
|
|
86
103
|
## 옵션 패턴
|
|
87
104
|
|
|
88
105
|
사용자가 특정 작업만 요청하는 경우:
|
|
@@ -91,6 +108,9 @@ Review .omd/preferences.md for details.
|
|
|
91
108
|
- **"X scope만 반영"** → 해당 scope만 Phase 3에서 처리
|
|
92
109
|
- **"<pref_id>를 applied로 표시"** → Phase 4의 single-entry 플립만
|
|
93
110
|
- **"<pref_id>를 rejected로 표시 + 이유"** → 동일
|
|
111
|
+
- 플립 전 현재 status를 먼저 Read로 확인: 이미 같은 값이면 no-op 보고,
|
|
112
|
+
`superseded`/`rejected` → `applied` 전환은 **금지** (이력 오염 — 사용자에게
|
|
113
|
+
"이 항목은 X 상태예요. 되살리려면 omd:remember로 재캡처하세요"라고 안내)
|
|
94
114
|
|
|
95
115
|
## 금지
|
|
96
116
|
|
|
@@ -102,12 +102,16 @@ id가 카탈로그에 없으면 종료 + "X는 reference 카탈로그에 없어
|
|
|
102
102
|
|
|
103
103
|
## Phase 2 — 라이브 URL 수집
|
|
104
104
|
|
|
105
|
-
**reference 자료 경로 `<refdir>`** 는
|
|
106
|
-
1. `.claude/data/references/<id>/` (installer가 복사 — npx 설치 기본 경로; **DESIGN.md만** 보장)
|
|
107
|
-
2. `node_modules/oh-my-design-cli/web/references/<id>/` (로컬 npm 설치 — _promo.json/_research.md 포함)
|
|
108
|
-
3. `web/references/<id>/` (개발 레포)
|
|
105
|
+
**reference 자료 경로 `<refdir>`** 는 reference DESIGN.md 위치 기준으로 resolve (먼저 존재하는 것 사용 — omd:init Phase 4.1과 동일한 카탈로그 resolution order):
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
<!-- omd:catalog-resolution-order — omd-init/omd-harness SKILL.md + agents/omd-master.md 와 동일 순서 강제. drift guard: test/unit/core/catalog-resolution-order.test.ts -->
|
|
108
|
+
|
|
109
|
+
1. `.claude/data/references/<id>/DESIGN.md` (installer가 복사 — npx 설치 기본 경로; 디렉토리에는 **DESIGN.md만** 보장)
|
|
110
|
+
2. `node_modules/oh-my-design-cli/web/references/<id>/DESIGN.md` (로컬 npm 설치 직접 경로 — 디렉토리에 _promo.json/_research.md 포함)
|
|
111
|
+
3. `web/references/<id>/DESIGN.md` (개발 레포)
|
|
112
|
+
4. `https://oh-my-design.kr/design-systems/<id>.md` 를 fetch (WebFetch 또는 `curl -fsSL`) — 1~3 로컬 경로가 전부 없을 때. 200이면 본문이 곧 reference DESIGN.md. 가져온 내용을 `.claude/data/references/<id>/DESIGN.md`로 캐시해 다음부터는 로컬 캐시(경로 1)로 잡히게 한다.
|
|
113
|
+
|
|
114
|
+
`<refdir>` = resolve된 DESIGN.md가 있는 디렉토리 (tier 4로 fetch한 경우 캐시 후 `.claude/data/references/<id>/`). `_promo.json`/`_research.md`는 (1)/(4)에 없을 수 있으니, 없으면 (2)/(3)로 폴백하고 그래도 없으면 fingerprints 기반 추론으로 진행.
|
|
111
115
|
|
|
112
116
|
다음을 순서대로 시도:
|
|
113
117
|
|
|
@@ -381,7 +385,7 @@ playwright MCP를 사용해 homepage에 navigate한 뒤 computed styles를 추
|
|
|
381
385
|
}
|
|
382
386
|
```
|
|
383
387
|
|
|
384
|
-
inspect
|
|
388
|
+
inspect 패턴: 5-10 elements 샘플링 (`body` / `header nav` / hero CTA / card / `footer` 류 대표 element의 getComputedStyle 추출 — Phase 3.9의 JS 패턴과 동일한 방식). 캡쳐하려는 brand가 카탈로그에 없으면 웹 카탈로그(`oh-my-design.kr/design-systems`)에서 먼저 찾아보고, 카탈로그에 새 brand를 직접 추가하는 워크플로우(omd-add-reference)는 dev 레포(github.com/kwakseongjae/oh-my-design)에만 존재 — 배포본(npx 설치)에는 포함되지 않는다.
|
|
385
389
|
|
|
386
390
|
## Phase 4.5 — 구조 cue 캡쳐 (structure.json)
|
|
387
391
|
|