oh-my-customcode 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/cli/index.js +93 -5
- package/dist/index.js +78 -2
- package/package.json +1 -1
- package/templates/.claude/agents/fe-design-expert.md +121 -0
- package/templates/.claude/agents/fe-svelte-agent.md +2 -0
- package/templates/.claude/agents/fe-vercel-agent.md +1 -0
- package/templates/.claude/agents/fe-vuejs-agent.md +2 -0
- package/templates/.claude/hooks/scripts/eval-core-batch-save.sh +26 -3
- package/templates/.claude/skills/impeccable-design/SKILL.md +173 -0
- package/templates/.claude/skills/omcustom-auto-improve/SKILL.md +136 -0
- package/templates/.claude/skills/pipeline-guards/SKILL.md +2 -0
- package/templates/.claude/skills/secretary-routing/SKILL.md +8 -2
- package/templates/CLAUDE.md +4 -3
- package/templates/guides/impeccable-design/color-and-contrast.md +278 -0
- package/templates/guides/impeccable-design/index.yaml +12 -0
- package/templates/guides/impeccable-design/motion-design.md +390 -0
- package/templates/guides/impeccable-design/typography.md +386 -0
- package/templates/guides/impeccable-design/ux-writing.md +400 -0
- package/templates/guides/index.yaml +9 -0
- package/templates/manifest.json +5 -5
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
**[한국어 문서 (Korean)](./README_ko.md)**
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
46 agents. 94 skills. 21 rules. One command.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install -g oh-my-customcode && cd your-project && omcustom init
|
|
@@ -125,7 +125,7 @@ Agent(arch-documenter):haiku ┘
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
-
### Agents (
|
|
128
|
+
### Agents (46)
|
|
129
129
|
|
|
130
130
|
| Category | Count | Agents |
|
|
131
131
|
|----------|-------|--------|
|
|
@@ -146,7 +146,7 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
|
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
-
### Skills (
|
|
149
|
+
### Skills (94)
|
|
150
150
|
|
|
151
151
|
| Category | Count | Includes |
|
|
152
152
|
|----------|-------|----------|
|
|
@@ -236,7 +236,7 @@ Key rules: R010 (orchestrator never writes files), R009 (parallel execution mand
|
|
|
236
236
|
|
|
237
237
|
---
|
|
238
238
|
|
|
239
|
-
### Guides (
|
|
239
|
+
### Guides (31)
|
|
240
240
|
|
|
241
241
|
Reference documentation covering best practices, architecture decisions, and integration patterns. Located in `guides/` at project root, covering topics from agent design to CI/CD to observability.
|
|
242
242
|
|
|
@@ -281,15 +281,15 @@ omcustom serve-stop # Stop Web UI
|
|
|
281
281
|
your-project/
|
|
282
282
|
├── CLAUDE.md # Entry point
|
|
283
283
|
├── .claude/
|
|
284
|
-
│ ├── agents/ #
|
|
285
|
-
│ ├── skills/ #
|
|
284
|
+
│ ├── agents/ # 46 agent definitions
|
|
285
|
+
│ ├── skills/ # 94 skill modules
|
|
286
286
|
│ ├── rules/ # 21 governance rules (R000-R021)
|
|
287
287
|
│ ├── hooks/ # 15 lifecycle hook scripts
|
|
288
288
|
│ ├── schemas/ # Tool input validation schemas
|
|
289
289
|
│ ├── specs/ # Extracted canonical specs
|
|
290
290
|
│ ├── contexts/ # 4 shared context files
|
|
291
291
|
│ └── ontology/ # Knowledge graph for RAG
|
|
292
|
-
└── guides/ #
|
|
292
|
+
└── guides/ # 31 reference documents
|
|
293
293
|
```
|
|
294
294
|
|
|
295
295
|
---
|
package/dist/cli/index.js
CHANGED
|
@@ -9325,7 +9325,7 @@ var init_package = __esm(() => {
|
|
|
9325
9325
|
workspaces: [
|
|
9326
9326
|
"packages/*"
|
|
9327
9327
|
],
|
|
9328
|
-
version: "0.
|
|
9328
|
+
version: "0.58.0",
|
|
9329
9329
|
description: "Batteries-included agent harness for Claude Code",
|
|
9330
9330
|
type: "module",
|
|
9331
9331
|
bin: {
|
|
@@ -24791,6 +24791,8 @@ var en_default = {
|
|
|
24791
24791
|
backupCreated: "Backup created at {{path}}",
|
|
24792
24792
|
summary: "Update complete: {{updated}} updated, {{skipped}} skipped",
|
|
24793
24793
|
summaryFailed: "Update failed: {{error}}",
|
|
24794
|
+
hardOption: "Sync namespace (name: field) in unmodified files from upstream",
|
|
24795
|
+
namespaceSynced: "↻ Namespace synced: {{count}} file(s)",
|
|
24794
24796
|
allOption: "Batch update all outdated projects found by project discovery",
|
|
24795
24797
|
allScanning: "Scanning for oh-my-customcode projects...",
|
|
24796
24798
|
allNoneFound: "No oh-my-customcode projects found.",
|
|
@@ -25173,6 +25175,8 @@ var ko_default = {
|
|
|
25173
25175
|
backupCreated: "백업 생성됨: {{path}}",
|
|
25174
25176
|
summary: "업데이트 완료: {{updated}}개 업데이트, {{skipped}}개 건너뜀",
|
|
25175
25177
|
summaryFailed: "업데이트 실패: {{error}}",
|
|
25178
|
+
hardOption: "미수정 파일의 네임스페이스(name: 필드)를 upstream에서 동기화",
|
|
25179
|
+
namespaceSynced: "↻ 네임스페이스 동기화: {{count}}개 파일",
|
|
25176
25180
|
allOption: "프로젝트 탐색으로 발견된 모든 outdated 프로젝트 일괄 업데이트",
|
|
25177
25181
|
allScanning: "oh-my-customcode 프로젝트 검색 중...",
|
|
25178
25182
|
allNoneFound: "oh-my-customcode 프로젝트를 찾을 수 없습니다.",
|
|
@@ -25841,6 +25845,7 @@ var MESSAGES = {
|
|
|
25841
25845
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
25842
25846
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
25843
25847
|
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
|
|
25848
|
+
"update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
|
|
25844
25849
|
"config.load_failed": "Failed to load config: {{error}}",
|
|
25845
25850
|
"config.not_found": "Config not found at {{path}}, using defaults",
|
|
25846
25851
|
"config.saved": "Config saved to {{path}}",
|
|
@@ -25886,6 +25891,7 @@ var MESSAGES = {
|
|
|
25886
25891
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
25887
25892
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
25888
25893
|
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
|
|
25894
|
+
"update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
|
|
25889
25895
|
"config.load_failed": "설정 로드 실패: {{error}}",
|
|
25890
25896
|
"config.not_found": "{{path}}에 설정 없음, 기본값 사용",
|
|
25891
25897
|
"config.saved": "설정 저장: {{path}}",
|
|
@@ -29927,7 +29933,8 @@ function createUpdateResult() {
|
|
|
29927
29933
|
newVersion: "",
|
|
29928
29934
|
warnings: [],
|
|
29929
29935
|
syncedRootFiles: [],
|
|
29930
|
-
removedDeprecatedFiles: []
|
|
29936
|
+
removedDeprecatedFiles: [],
|
|
29937
|
+
namespaceSynced: []
|
|
29931
29938
|
};
|
|
29932
29939
|
}
|
|
29933
29940
|
async function handleBackupIfRequested(targetDir, backup, result) {
|
|
@@ -29952,6 +29959,10 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
29952
29959
|
const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
|
|
29953
29960
|
result.updatedComponents.push(component);
|
|
29954
29961
|
result.preservedFiles.push(...preserved);
|
|
29962
|
+
if (options.hard) {
|
|
29963
|
+
const synced = await applyNamespaceSync(targetDir, component, lockfile);
|
|
29964
|
+
result.namespaceSynced.push(...synced);
|
|
29965
|
+
}
|
|
29955
29966
|
} catch (err) {
|
|
29956
29967
|
const message = err instanceof Error ? err.message : String(err);
|
|
29957
29968
|
result.warnings.push(`Failed to update ${component}: ${message}`);
|
|
@@ -30403,6 +30414,75 @@ async function removeDeprecatedFiles(targetDir, options) {
|
|
|
30403
30414
|
}
|
|
30404
30415
|
return removed;
|
|
30405
30416
|
}
|
|
30417
|
+
function extractFrontmatterName(content) {
|
|
30418
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
30419
|
+
if (!match)
|
|
30420
|
+
return null;
|
|
30421
|
+
const nameMatch = match[1].match(/^name:\s*(.+)$/m);
|
|
30422
|
+
if (!nameMatch)
|
|
30423
|
+
return null;
|
|
30424
|
+
return nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
30425
|
+
}
|
|
30426
|
+
async function syncNamespaceInFile(targetFilePath, upstreamFilePath) {
|
|
30427
|
+
const targetContent = await readTextFile(targetFilePath);
|
|
30428
|
+
const upstreamContent = await readTextFile(upstreamFilePath);
|
|
30429
|
+
const upstreamName = extractFrontmatterName(upstreamContent);
|
|
30430
|
+
const targetName = extractFrontmatterName(targetContent);
|
|
30431
|
+
if (!upstreamName || !targetName || upstreamName === targetName)
|
|
30432
|
+
return false;
|
|
30433
|
+
const safeUpstreamName = upstreamName.replace(/\$/g, "$$$$");
|
|
30434
|
+
const updated = targetContent.replace(/^(name:\s*).+$/m, `$1${safeUpstreamName}`);
|
|
30435
|
+
if (updated === targetContent)
|
|
30436
|
+
return false;
|
|
30437
|
+
await writeTextFile(targetFilePath, updated);
|
|
30438
|
+
return true;
|
|
30439
|
+
}
|
|
30440
|
+
async function processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile) {
|
|
30441
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
30442
|
+
return null;
|
|
30443
|
+
const targetFilePath = join14(destPath, relPath);
|
|
30444
|
+
const lockfileKey = `${componentPath}/${relPath}`.replace(/\\/g, "/");
|
|
30445
|
+
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
30446
|
+
if (shouldSkip)
|
|
30447
|
+
return null;
|
|
30448
|
+
if (!await fileExists(targetFilePath))
|
|
30449
|
+
return null;
|
|
30450
|
+
const didSync = await syncNamespaceInFile(targetFilePath, fullSrcPath);
|
|
30451
|
+
return didSync ? `${componentPath}/${relPath}` : null;
|
|
30452
|
+
}
|
|
30453
|
+
async function applyNamespaceSync(targetDir, component, lockfile) {
|
|
30454
|
+
if (!lockfile)
|
|
30455
|
+
return [];
|
|
30456
|
+
const componentPath = getComponentPath2(component);
|
|
30457
|
+
const srcPath = resolveTemplatePath(componentPath);
|
|
30458
|
+
const destPath = join14(targetDir, componentPath);
|
|
30459
|
+
const fs3 = await import("node:fs/promises");
|
|
30460
|
+
const synced = [];
|
|
30461
|
+
const queue = [{ dir: srcPath, relDir: "" }];
|
|
30462
|
+
while (queue.length > 0) {
|
|
30463
|
+
const { dir: dir2, relDir } = queue.shift();
|
|
30464
|
+
let entries;
|
|
30465
|
+
try {
|
|
30466
|
+
entries = await fs3.readdir(dir2, { withFileTypes: true });
|
|
30467
|
+
} catch {
|
|
30468
|
+
continue;
|
|
30469
|
+
}
|
|
30470
|
+
for (const entry of entries) {
|
|
30471
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
30472
|
+
const fullSrcPath = join14(dir2, entry.name);
|
|
30473
|
+
if (entry.isDirectory()) {
|
|
30474
|
+
queue.push({ dir: fullSrcPath, relDir: relPath });
|
|
30475
|
+
continue;
|
|
30476
|
+
}
|
|
30477
|
+
const syncedPath = await processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile);
|
|
30478
|
+
if (syncedPath) {
|
|
30479
|
+
synced.push(syncedPath);
|
|
30480
|
+
info("update.namespace_synced", { file: relPath, component });
|
|
30481
|
+
}
|
|
30482
|
+
}
|
|
30483
|
+
}
|
|
30484
|
+
return synced;
|
|
30485
|
+
}
|
|
30406
30486
|
function getComponentPath2(component) {
|
|
30407
30487
|
const layout = getProviderLayout();
|
|
30408
30488
|
if (component === "guides") {
|
|
@@ -30469,7 +30549,8 @@ async function updateSingleProject(targetDir, options) {
|
|
|
30469
30549
|
preserveCustomizations: true,
|
|
30470
30550
|
forceOverwriteAll: options.forceOverwriteAll,
|
|
30471
30551
|
dryRun: options.dryRun,
|
|
30472
|
-
backup: options.backup
|
|
30552
|
+
backup: options.backup,
|
|
30553
|
+
hard: options.hard
|
|
30473
30554
|
};
|
|
30474
30555
|
const result = await update(updateOptions);
|
|
30475
30556
|
printUpdateResults(result);
|
|
@@ -30509,7 +30590,8 @@ async function updateAllProjects(options) {
|
|
|
30509
30590
|
preserveCustomizations: true,
|
|
30510
30591
|
forceOverwriteAll: options.forceOverwriteAll,
|
|
30511
30592
|
dryRun: options.dryRun,
|
|
30512
|
-
backup: options.backup
|
|
30593
|
+
backup: options.backup,
|
|
30594
|
+
hard: options.hard
|
|
30513
30595
|
};
|
|
30514
30596
|
const result = await update(updateOptions);
|
|
30515
30597
|
if (result.success) {
|
|
@@ -30600,6 +30682,12 @@ function printUpdateResults(result) {
|
|
|
30600
30682
|
if (result.preservedFiles.length > 0) {
|
|
30601
30683
|
console.log(i18n.t("cli.update.preservedFiles", { count: String(result.preservedFiles.length) }));
|
|
30602
30684
|
}
|
|
30685
|
+
if ((result.namespaceSynced?.length ?? 0) > 0) {
|
|
30686
|
+
console.log(i18n.t("cli.update.namespaceSynced", { count: String(result.namespaceSynced.length) }));
|
|
30687
|
+
for (const file of result.namespaceSynced) {
|
|
30688
|
+
console.log(` ↻ ${file}`);
|
|
30689
|
+
}
|
|
30690
|
+
}
|
|
30603
30691
|
if (result.backedUpPaths.length > 0) {
|
|
30604
30692
|
for (const path4 of result.backedUpPaths) {
|
|
30605
30693
|
console.log(i18n.t("cli.update.backupCreated", { path: path4 }));
|
|
@@ -30656,7 +30744,7 @@ function createProgram() {
|
|
|
30656
30744
|
program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption")).option("--domain <domain>", "Install only agents/skills for specific domain (backend, frontend, data-engineering, devops)").option("--yes", "Skip interactive wizard, use defaults").action(async (options) => {
|
|
30657
30745
|
await initCommand(options);
|
|
30658
30746
|
});
|
|
30659
|
-
program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--force-overwrite-all", i18n.t("cli.update.forceOverwriteAllOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("--all", i18n.t("cli.update.allOption")).action(async (options) => {
|
|
30747
|
+
program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--force-overwrite-all", i18n.t("cli.update.forceOverwriteAllOption")).option("--hard", i18n.t("cli.update.hardOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("--all", i18n.t("cli.update.allOption")).action(async (options) => {
|
|
30660
30748
|
await updateCommand(options);
|
|
30661
30749
|
});
|
|
30662
30750
|
program2.command("list").description(i18n.t("cli.list.description")).argument("[type]", i18n.t("cli.list.typeArgument"), "all").option("-f, --format <format>", "Output format: table, json, or simple", "table").option("--verbose", "Show detailed information").action(async (type, options) => {
|
package/dist/index.js
CHANGED
|
@@ -377,6 +377,7 @@ var MESSAGES = {
|
|
|
377
377
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
378
378
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
379
379
|
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
|
|
380
|
+
"update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
|
|
380
381
|
"config.load_failed": "Failed to load config: {{error}}",
|
|
381
382
|
"config.not_found": "Config not found at {{path}}, using defaults",
|
|
382
383
|
"config.saved": "Config saved to {{path}}",
|
|
@@ -422,6 +423,7 @@ var MESSAGES = {
|
|
|
422
423
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
423
424
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
424
425
|
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
|
|
426
|
+
"update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
|
|
425
427
|
"config.load_failed": "설정 로드 실패: {{error}}",
|
|
426
428
|
"config.not_found": "{{path}}에 설정 없음, 기본값 사용",
|
|
427
429
|
"config.saved": "설정 저장: {{path}}",
|
|
@@ -1670,7 +1672,7 @@ var package_default = {
|
|
|
1670
1672
|
workspaces: [
|
|
1671
1673
|
"packages/*"
|
|
1672
1674
|
],
|
|
1673
|
-
version: "0.
|
|
1675
|
+
version: "0.58.0",
|
|
1674
1676
|
description: "Batteries-included agent harness for Claude Code",
|
|
1675
1677
|
type: "module",
|
|
1676
1678
|
bin: {
|
|
@@ -1893,7 +1895,8 @@ function createUpdateResult() {
|
|
|
1893
1895
|
newVersion: "",
|
|
1894
1896
|
warnings: [],
|
|
1895
1897
|
syncedRootFiles: [],
|
|
1896
|
-
removedDeprecatedFiles: []
|
|
1898
|
+
removedDeprecatedFiles: [],
|
|
1899
|
+
namespaceSynced: []
|
|
1897
1900
|
};
|
|
1898
1901
|
}
|
|
1899
1902
|
async function handleBackupIfRequested(targetDir, backup, result) {
|
|
@@ -1918,6 +1921,10 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
1918
1921
|
const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
|
|
1919
1922
|
result.updatedComponents.push(component);
|
|
1920
1923
|
result.preservedFiles.push(...preserved);
|
|
1924
|
+
if (options.hard) {
|
|
1925
|
+
const synced = await applyNamespaceSync(targetDir, component, lockfile);
|
|
1926
|
+
result.namespaceSynced.push(...synced);
|
|
1927
|
+
}
|
|
1921
1928
|
} catch (err) {
|
|
1922
1929
|
const message = err instanceof Error ? err.message : String(err);
|
|
1923
1930
|
result.warnings.push(`Failed to update ${component}: ${message}`);
|
|
@@ -2390,6 +2397,75 @@ async function removeDeprecatedFiles(targetDir, options) {
|
|
|
2390
2397
|
}
|
|
2391
2398
|
return removed;
|
|
2392
2399
|
}
|
|
2400
|
+
function extractFrontmatterName(content) {
|
|
2401
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2402
|
+
if (!match)
|
|
2403
|
+
return null;
|
|
2404
|
+
const nameMatch = match[1].match(/^name:\s*(.+)$/m);
|
|
2405
|
+
if (!nameMatch)
|
|
2406
|
+
return null;
|
|
2407
|
+
return nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2408
|
+
}
|
|
2409
|
+
async function syncNamespaceInFile(targetFilePath, upstreamFilePath) {
|
|
2410
|
+
const targetContent = await readTextFile(targetFilePath);
|
|
2411
|
+
const upstreamContent = await readTextFile(upstreamFilePath);
|
|
2412
|
+
const upstreamName = extractFrontmatterName(upstreamContent);
|
|
2413
|
+
const targetName = extractFrontmatterName(targetContent);
|
|
2414
|
+
if (!upstreamName || !targetName || upstreamName === targetName)
|
|
2415
|
+
return false;
|
|
2416
|
+
const safeUpstreamName = upstreamName.replace(/\$/g, "$$$$");
|
|
2417
|
+
const updated = targetContent.replace(/^(name:\s*).+$/m, `$1${safeUpstreamName}`);
|
|
2418
|
+
if (updated === targetContent)
|
|
2419
|
+
return false;
|
|
2420
|
+
await writeTextFile(targetFilePath, updated);
|
|
2421
|
+
return true;
|
|
2422
|
+
}
|
|
2423
|
+
async function processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile) {
|
|
2424
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
2425
|
+
return null;
|
|
2426
|
+
const targetFilePath = join6(destPath, relPath);
|
|
2427
|
+
const lockfileKey = `${componentPath}/${relPath}`.replace(/\\/g, "/");
|
|
2428
|
+
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
2429
|
+
if (shouldSkip)
|
|
2430
|
+
return null;
|
|
2431
|
+
if (!await fileExists(targetFilePath))
|
|
2432
|
+
return null;
|
|
2433
|
+
const didSync = await syncNamespaceInFile(targetFilePath, fullSrcPath);
|
|
2434
|
+
return didSync ? `${componentPath}/${relPath}` : null;
|
|
2435
|
+
}
|
|
2436
|
+
async function applyNamespaceSync(targetDir, component, lockfile) {
|
|
2437
|
+
if (!lockfile)
|
|
2438
|
+
return [];
|
|
2439
|
+
const componentPath = getComponentPath2(component);
|
|
2440
|
+
const srcPath = resolveTemplatePath(componentPath);
|
|
2441
|
+
const destPath = join6(targetDir, componentPath);
|
|
2442
|
+
const fs = await import("node:fs/promises");
|
|
2443
|
+
const synced = [];
|
|
2444
|
+
const queue = [{ dir: srcPath, relDir: "" }];
|
|
2445
|
+
while (queue.length > 0) {
|
|
2446
|
+
const { dir, relDir } = queue.shift();
|
|
2447
|
+
let entries;
|
|
2448
|
+
try {
|
|
2449
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2450
|
+
} catch {
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
for (const entry of entries) {
|
|
2454
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
2455
|
+
const fullSrcPath = join6(dir, entry.name);
|
|
2456
|
+
if (entry.isDirectory()) {
|
|
2457
|
+
queue.push({ dir: fullSrcPath, relDir: relPath });
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
const syncedPath = await processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile);
|
|
2461
|
+
if (syncedPath) {
|
|
2462
|
+
synced.push(syncedPath);
|
|
2463
|
+
info("update.namespace_synced", { file: relPath, component });
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return synced;
|
|
2468
|
+
}
|
|
2393
2469
|
function getComponentPath2(component) {
|
|
2394
2470
|
const layout = getProviderLayout();
|
|
2395
2471
|
if (component === "guides") {
|
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fe-design-expert
|
|
3
|
+
description: Use for design system review, typography audit, color palette evaluation, motion design, and AI-generated design quality assessment
|
|
4
|
+
model: sonnet
|
|
5
|
+
domain: frontend
|
|
6
|
+
memory: project
|
|
7
|
+
effort: medium
|
|
8
|
+
skills:
|
|
9
|
+
- impeccable-design
|
|
10
|
+
- web-design-guidelines
|
|
11
|
+
tools: [Read, Write, Edit, Grep, Glob, Bash]
|
|
12
|
+
source:
|
|
13
|
+
type: external
|
|
14
|
+
origin: github
|
|
15
|
+
url: https://github.com/pbakaus/impeccable
|
|
16
|
+
version: 1.0.0
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
You are a frontend design specialist focused on visual quality, aesthetic craft, and eliminating "AI slop" from production UI. Your domain is the *feel* of an interface — the choices that make design feel intentional rather than generated.
|
|
20
|
+
|
|
21
|
+
## Role
|
|
22
|
+
|
|
23
|
+
You evaluate and improve the aesthetic dimensions of user interfaces: typography, color, motion, spatial layout, and UX writing. You use the Impeccable AI design language to guide critique and implementation, steering toward production-grade visual quality.
|
|
24
|
+
|
|
25
|
+
**Critical distinction**: You handle AESTHETIC and VISUAL quality. For technical compliance (accessibility, performance, semantic HTML), use `web-design-guidelines` skill or the `fe-vercel-agent`.
|
|
26
|
+
|
|
27
|
+
## Source
|
|
28
|
+
|
|
29
|
+
External from https://github.com/pbakaus/impeccable (v1.0.0)
|
|
30
|
+
|
|
31
|
+
## Capabilities
|
|
32
|
+
|
|
33
|
+
### 10 Impeccable Steering Commands
|
|
34
|
+
|
|
35
|
+
| Command | Trigger phrases | What it does |
|
|
36
|
+
|---------|----------------|--------------|
|
|
37
|
+
| **critique** | "review design", "UX feedback", "design critique" | Holistic UX review: hierarchy, clarity, emotional resonance, and intentionality |
|
|
38
|
+
| **audit** | "design audit", "quality check", "design review" | Systematic check across all design dimensions — typography, color, motion, layout, copy |
|
|
39
|
+
| **typeset** | "fix fonts", "typography", "improve text hierarchy" | Fix font choices, scale, weight contrast, line-height, and type pairing |
|
|
40
|
+
| **colorize** | "add color", "color palette", "fix colors" | Introduce strategic color using OKLCH; build tinted neutrals, avoid pure black/white |
|
|
41
|
+
| **animate** | "add motion", "animation", "transitions" | Add purposeful motion using 100ms/300ms/500ms rule; avoid decorative bounce/elastic |
|
|
42
|
+
| **normalize** | "align design system", "tokens", "consistency" | Align with design system standards; enforce spacing scale and token usage |
|
|
43
|
+
| **polish** | "final pass", "ship ready", "pre-launch review" | Pre-ship quality sweep across all dimensions; AI slop test included |
|
|
44
|
+
| **clarify** | "improve copy", "UX writing", "button labels" | Improve unclear labels, microcopy, empty states, and error messages |
|
|
45
|
+
| **arrange** | "fix layout", "spacing", "visual rhythm" | Fix layout structure, whitespace, alignment, and visual rhythm |
|
|
46
|
+
| **adapt** | "responsive", "mobile", "breakpoints" | Adapt design for different screen sizes and input modes |
|
|
47
|
+
|
|
48
|
+
### AI Slop Test
|
|
49
|
+
|
|
50
|
+
Before declaring any design "done", run the AI Slop Test. This is the critical checkpoint.
|
|
51
|
+
|
|
52
|
+
Ask: **Would someone immediately identify this as AI-generated?**
|
|
53
|
+
|
|
54
|
+
Flag these patterns as AI slop:
|
|
55
|
+
- Overused fonts: Inter, Roboto, or Arial used as default without intentional reason
|
|
56
|
+
- Pure black (`#000`) or pure gray backgrounds with no color tinting
|
|
57
|
+
- Excessive card nesting with uniform rounded corners and drop shadows on everything
|
|
58
|
+
- Generic gradient backgrounds (blue-purple, coral-orange) with no contextual rationale
|
|
59
|
+
- Bounce or elastic animations as "delight" without functional purpose
|
|
60
|
+
- Centered-everything layouts that avoid spatial decisions
|
|
61
|
+
- Hero sections with a gradient blob behind centered text
|
|
62
|
+
- Identical spacing increments used everywhere (8px, 8px, 8px)
|
|
63
|
+
- Color palettes that are purely neutral except for one brand accent
|
|
64
|
+
- Empty states with a generic icon + "No items yet" + CTA
|
|
65
|
+
|
|
66
|
+
## Workflow
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
1. Context gathering
|
|
70
|
+
- Read component/page files to understand current implementation
|
|
71
|
+
- Identify design intent and target audience
|
|
72
|
+
- Note existing design system tokens if present
|
|
73
|
+
|
|
74
|
+
2. Design direction
|
|
75
|
+
- Select appropriate Impeccable commands for the task
|
|
76
|
+
- Establish aesthetic goals before implementation
|
|
77
|
+
- Check for AI slop patterns in current state
|
|
78
|
+
|
|
79
|
+
3. Implementation
|
|
80
|
+
- Apply targeted changes with clear rationale
|
|
81
|
+
- Prefer incremental polish over full rewrites
|
|
82
|
+
- Document design decisions in comments when non-obvious
|
|
83
|
+
|
|
84
|
+
4. Slop test
|
|
85
|
+
- Rerun AI Slop Test on output
|
|
86
|
+
- Verify changes feel intentional, not generated
|
|
87
|
+
- Check that typography, color, and motion work together
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Role Separation
|
|
91
|
+
|
|
92
|
+
| Aspect | fe-design-expert (this agent) | fe-vercel-agent / web-design-guidelines |
|
|
93
|
+
|--------|-------------------------------|----------------------------------------|
|
|
94
|
+
| Typography | Font selection, pairing, type scale, expressive hierarchy | Minimum font sizes, contrast ratios |
|
|
95
|
+
| Color | Palette building, OKLCH, tinted neutrals, emotional resonance | WCAG contrast compliance |
|
|
96
|
+
| Motion | Purposeful animation, easing curves, timing strategy | prefers-reduced-motion compliance |
|
|
97
|
+
| Layout | Visual rhythm, spatial design, negative space | Accessibility, semantic HTML structure |
|
|
98
|
+
| Copy | UX writing tone, clarity, label specificity | (no overlap) |
|
|
99
|
+
|
|
100
|
+
## When to Use
|
|
101
|
+
|
|
102
|
+
- Design reviews where visual quality is the focus
|
|
103
|
+
- Typography or color audits before a release
|
|
104
|
+
- Adding motion/animation to a UI
|
|
105
|
+
- Improving UX copy and microcopy
|
|
106
|
+
- Pre-ship polish sweeps
|
|
107
|
+
- Identifying AI-generated aesthetics that need humanization
|
|
108
|
+
|
|
109
|
+
## When NOT to Use
|
|
110
|
+
|
|
111
|
+
- Performance optimization → use `tool-optimizer`
|
|
112
|
+
- Accessibility compliance testing → use `web-design-guidelines` skill
|
|
113
|
+
- Backend or API code → use appropriate language/framework agent
|
|
114
|
+
- Bundle size analysis → use `tool-npm-expert`
|
|
115
|
+
|
|
116
|
+
## Reference Guides
|
|
117
|
+
|
|
118
|
+
- `guides/impeccable-design/typography.md` — type scale, font pairing, hierarchy
|
|
119
|
+
- `guides/impeccable-design/color-and-contrast.md` — OKLCH, palette strategy, tinted neutrals
|
|
120
|
+
- `guides/impeccable-design/motion-design.md` — timing rules, easing, purposeful animation
|
|
121
|
+
- `guides/impeccable-design/ux-writing.md` — microcopy, labels, empty states, error messages
|
|
@@ -12,10 +12,33 @@ set -euo pipefail
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
PPID_FILE="/tmp/.claude-task-outcomes-${PPID}"
|
|
14
14
|
|
|
15
|
-
# Only attempt collection if outcome file exists
|
|
16
|
-
if [ -f "$PPID_FILE" ]
|
|
15
|
+
# Only attempt collection if outcome file exists
|
|
16
|
+
if [ ! -f "$PPID_FILE" ]; then
|
|
17
|
+
echo "$input"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Discover eval-core CLI using multiple strategies
|
|
22
|
+
EVAL_CORE=""
|
|
23
|
+
|
|
24
|
+
# Strategy 1: Global CLI installation
|
|
25
|
+
if command -v eval-core >/dev/null 2>&1; then
|
|
26
|
+
EVAL_CORE="eval-core"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Strategy 2: Workspace package (oh-my-customcode development)
|
|
30
|
+
if [ -z "$EVAL_CORE" ]; then
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
32
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
33
|
+
WORKSPACE_CLI="$PROJECT_ROOT/packages/eval-core/src/cli/index.ts"
|
|
34
|
+
if [ -f "$WORKSPACE_CLI" ] && command -v bun >/dev/null 2>&1; then
|
|
35
|
+
EVAL_CORE="bun run $WORKSPACE_CLI"
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ -n "$EVAL_CORE" ]; then
|
|
17
40
|
echo "[Hook] Collecting eval metrics via eval-core..." >&2
|
|
18
|
-
|
|
41
|
+
$EVAL_CORE collect --ppid "$PPID" 2>/dev/null || true
|
|
19
42
|
fi
|
|
20
43
|
|
|
21
44
|
# Always pass through input and exit 0 (advisory only)
|