oh-my-customcode 0.49.0 → 0.51.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 +3 -3
- package/dist/cli/index.js +59 -14
- package/dist/index.js +83 -14
- package/package.json +1 -1
- package/templates/.claude/skills/scout/SKILL.md +248 -0
- package/templates/.claude/skills/systematic-debugging/SKILL.md +288 -0
- package/templates/.claude/skills/systematic-debugging/condition-based-waiting-example.ts +278 -0
- package/templates/.claude/skills/systematic-debugging/condition-based-waiting.md +240 -0
- package/templates/.claude/skills/systematic-debugging/defense-in-depth.md +252 -0
- package/templates/.claude/skills/systematic-debugging/find-polluter.sh +147 -0
- package/templates/.claude/skills/systematic-debugging/root-cause-tracing.md +87 -0
- package/templates/CLAUDE.md +2 -1
- package/templates/manifest.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
**[한국어 문서 (Korean)](./README_ko.md)**
|
|
15
15
|
|
|
16
|
-
45 agents.
|
|
16
|
+
45 agents. 91 skills. 21 rules. One command.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install -g oh-my-customcode && cd your-project && omcustom init
|
|
@@ -138,7 +138,7 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
|
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
141
|
-
### Skills (
|
|
141
|
+
### Skills (91)
|
|
142
142
|
|
|
143
143
|
| Category | Count | Includes |
|
|
144
144
|
|----------|-------|----------|
|
|
@@ -274,7 +274,7 @@ your-project/
|
|
|
274
274
|
├── CLAUDE.md # Entry point
|
|
275
275
|
├── .claude/
|
|
276
276
|
│ ├── agents/ # 45 agent definitions
|
|
277
|
-
│ ├── skills/ #
|
|
277
|
+
│ ├── skills/ # 91 skill modules
|
|
278
278
|
│ ├── rules/ # 21 governance rules (R000-R021)
|
|
279
279
|
│ ├── hooks/ # 15 lifecycle hook scripts
|
|
280
280
|
│ ├── schemas/ # Tool input validation schemas
|
package/dist/cli/index.js
CHANGED
|
@@ -9323,7 +9323,7 @@ var init_package = __esm(() => {
|
|
|
9323
9323
|
package_default = {
|
|
9324
9324
|
name: "oh-my-customcode",
|
|
9325
9325
|
workspaces: ["packages/*"],
|
|
9326
|
-
version: "0.
|
|
9326
|
+
version: "0.51.0",
|
|
9327
9327
|
description: "Batteries-included agent harness for Claude Code",
|
|
9328
9328
|
type: "module",
|
|
9329
9329
|
bin: {
|
|
@@ -25838,6 +25838,7 @@ var MESSAGES = {
|
|
|
25838
25838
|
"update.file_applied": "Applied update to {{path}}",
|
|
25839
25839
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
25840
25840
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
25841
|
+
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
|
|
25841
25842
|
"config.load_failed": "Failed to load config: {{error}}",
|
|
25842
25843
|
"config.not_found": "Config not found at {{path}}, using defaults",
|
|
25843
25844
|
"config.saved": "Config saved to {{path}}",
|
|
@@ -25882,6 +25883,7 @@ var MESSAGES = {
|
|
|
25882
25883
|
"update.file_applied": "{{path}} 업데이트 적용",
|
|
25883
25884
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
25884
25885
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
25886
|
+
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
|
|
25885
25887
|
"config.load_failed": "설정 로드 실패: {{error}}",
|
|
25886
25888
|
"config.not_found": "{{path}}에 설정 없음, 기본값 사용",
|
|
25887
25889
|
"config.saved": "설정 저장: {{path}}",
|
|
@@ -29933,7 +29935,7 @@ async function handleBackupIfRequested(targetDir, backup, result) {
|
|
|
29933
29935
|
result.backedUpPaths.push(backupPath);
|
|
29934
29936
|
info("update.backup_created", { path: backupPath });
|
|
29935
29937
|
}
|
|
29936
|
-
async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config) {
|
|
29938
|
+
async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile) {
|
|
29937
29939
|
const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
|
|
29938
29940
|
if (!componentUpdate && !options.force) {
|
|
29939
29941
|
result.skippedComponents.push(component);
|
|
@@ -29945,7 +29947,7 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
29945
29947
|
return;
|
|
29946
29948
|
}
|
|
29947
29949
|
try {
|
|
29948
|
-
const preserved = await updateComponent(targetDir, component, customizations, options, config);
|
|
29950
|
+
const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
|
|
29949
29951
|
result.updatedComponents.push(component);
|
|
29950
29952
|
result.preservedFiles.push(...preserved);
|
|
29951
29953
|
} catch (err) {
|
|
@@ -29954,9 +29956,9 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
29954
29956
|
result.skippedComponents.push(component);
|
|
29955
29957
|
}
|
|
29956
29958
|
}
|
|
29957
|
-
async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config) {
|
|
29959
|
+
async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config, lockfile) {
|
|
29958
29960
|
for (const component of components) {
|
|
29959
|
-
await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config);
|
|
29961
|
+
await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile);
|
|
29960
29962
|
}
|
|
29961
29963
|
}
|
|
29962
29964
|
function getEntryTemplateName2(language) {
|
|
@@ -30138,8 +30140,9 @@ async function update(options) {
|
|
|
30138
30140
|
const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
|
|
30139
30141
|
const configPreserveFiles = resolveConfigPreserveFiles(options, config);
|
|
30140
30142
|
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
|
|
30143
|
+
const lockfile = await readLockfile(options.targetDir);
|
|
30141
30144
|
const components = options.components || getAllUpdateComponents();
|
|
30142
|
-
await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
|
|
30145
|
+
await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
|
|
30143
30146
|
await runFullUpdatePostProcessing(options, result, config);
|
|
30144
30147
|
const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
|
|
30145
30148
|
if (lockfileResult.warning) {
|
|
@@ -30200,15 +30203,46 @@ async function componentHasUpdate(_targetDir, component, config) {
|
|
|
30200
30203
|
const latestVersion = await getLatestVersion();
|
|
30201
30204
|
return installedVersion !== latestVersion;
|
|
30202
30205
|
}
|
|
30203
|
-
async function
|
|
30206
|
+
async function shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile) {
|
|
30207
|
+
if (!lockfile) {
|
|
30208
|
+
return true;
|
|
30209
|
+
}
|
|
30210
|
+
const lockfileEntry = lockfile.files[lockfileKey];
|
|
30211
|
+
if (!lockfileEntry) {
|
|
30212
|
+
return false;
|
|
30213
|
+
}
|
|
30214
|
+
if (!await fileExists(targetFilePath)) {
|
|
30215
|
+
return false;
|
|
30216
|
+
}
|
|
30217
|
+
try {
|
|
30218
|
+
const currentHash = await computeFileHash(targetFilePath);
|
|
30219
|
+
return currentHash !== lockfileEntry.templateHash;
|
|
30220
|
+
} catch {
|
|
30221
|
+
return true;
|
|
30222
|
+
}
|
|
30223
|
+
}
|
|
30224
|
+
async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll, lockfile, targetDir) {
|
|
30204
30225
|
if (forceOverwriteAll) {
|
|
30205
|
-
const
|
|
30206
|
-
return { skipPaths: [], warnedPaths };
|
|
30226
|
+
const warnedPaths2 = await findProtectedFilesInDir(srcPath, componentPath);
|
|
30227
|
+
return { skipPaths: [], warnedPaths: warnedPaths2, updatedPaths: [] };
|
|
30207
30228
|
}
|
|
30208
30229
|
const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
|
|
30209
30230
|
const path3 = await import("node:path");
|
|
30210
|
-
const skipPaths =
|
|
30211
|
-
|
|
30231
|
+
const skipPaths = [];
|
|
30232
|
+
const warnedPaths = [];
|
|
30233
|
+
const updatedPaths = [];
|
|
30234
|
+
for (const p of protectedRelative) {
|
|
30235
|
+
const targetFilePath = join14(targetDir, componentPath, p);
|
|
30236
|
+
const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
|
|
30237
|
+
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
30238
|
+
if (shouldSkip) {
|
|
30239
|
+
skipPaths.push(path3.relative(destPath, join14(destPath, p)));
|
|
30240
|
+
warnedPaths.push(p);
|
|
30241
|
+
} else {
|
|
30242
|
+
updatedPaths.push(p);
|
|
30243
|
+
}
|
|
30244
|
+
}
|
|
30245
|
+
return { skipPaths, warnedPaths, updatedPaths };
|
|
30212
30246
|
}
|
|
30213
30247
|
function isEntryProtected(relPath, componentRelativePrefix) {
|
|
30214
30248
|
if (isProtectedFile(relPath)) {
|
|
@@ -30244,7 +30278,7 @@ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
|
|
|
30244
30278
|
}
|
|
30245
30279
|
return protected_;
|
|
30246
30280
|
}
|
|
30247
|
-
async function updateComponent(targetDir, component, customizations, options, config) {
|
|
30281
|
+
async function updateComponent(targetDir, component, customizations, options, config, lockfile) {
|
|
30248
30282
|
const preservedFiles = [];
|
|
30249
30283
|
const componentPath = getComponentPath2(component);
|
|
30250
30284
|
const srcPath = resolveTemplatePath(componentPath);
|
|
@@ -30261,7 +30295,11 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
30261
30295
|
skipPaths.push(cc.path);
|
|
30262
30296
|
}
|
|
30263
30297
|
}
|
|
30264
|
-
const {
|
|
30298
|
+
const {
|
|
30299
|
+
skipPaths: protectedSkipPaths,
|
|
30300
|
+
warnedPaths: protectedWarnedPaths,
|
|
30301
|
+
updatedPaths: protectedUpdatedPaths
|
|
30302
|
+
} = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll, lockfile, targetDir);
|
|
30265
30303
|
for (const protectedPath of protectedWarnedPaths) {
|
|
30266
30304
|
if (options.forceOverwriteAll) {
|
|
30267
30305
|
warn("update.protected_file_force_overwrite", {
|
|
@@ -30273,10 +30311,17 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
30273
30311
|
warn("update.protected_file_skipped", {
|
|
30274
30312
|
file: protectedPath,
|
|
30275
30313
|
component,
|
|
30276
|
-
hint: "File
|
|
30314
|
+
hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
|
|
30277
30315
|
});
|
|
30278
30316
|
}
|
|
30279
30317
|
}
|
|
30318
|
+
for (const updatedPath of protectedUpdatedPaths) {
|
|
30319
|
+
info("update.protected_file_updated", {
|
|
30320
|
+
file: updatedPath,
|
|
30321
|
+
component,
|
|
30322
|
+
hint: "Protected file updated (unmodified by user, matches lockfile hash)."
|
|
30323
|
+
});
|
|
30324
|
+
}
|
|
30280
30325
|
skipPaths.push(...protectedSkipPaths);
|
|
30281
30326
|
const path3 = await import("node:path");
|
|
30282
30327
|
const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join14(targetDir, p)));
|
package/dist/index.js
CHANGED
|
@@ -376,6 +376,7 @@ var MESSAGES = {
|
|
|
376
376
|
"update.file_applied": "Applied update to {{path}}",
|
|
377
377
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
378
378
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
379
|
+
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
|
|
379
380
|
"config.load_failed": "Failed to load config: {{error}}",
|
|
380
381
|
"config.not_found": "Config not found at {{path}}, using defaults",
|
|
381
382
|
"config.saved": "Config saved to {{path}}",
|
|
@@ -420,6 +421,7 @@ var MESSAGES = {
|
|
|
420
421
|
"update.file_applied": "{{path}} 업데이트 적용",
|
|
421
422
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
422
423
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
424
|
+
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
|
|
423
425
|
"config.load_failed": "설정 로드 실패: {{error}}",
|
|
424
426
|
"config.not_found": "{{path}}에 설정 없음, 기본값 사용",
|
|
425
427
|
"config.saved": "설정 저장: {{path}}",
|
|
@@ -1125,6 +1127,30 @@ function computeFileHash(filePath) {
|
|
|
1125
1127
|
});
|
|
1126
1128
|
});
|
|
1127
1129
|
}
|
|
1130
|
+
async function readLockfile(targetDir) {
|
|
1131
|
+
const lockfilePath = join4(targetDir, LOCKFILE_NAME);
|
|
1132
|
+
const exists = await fileExists(lockfilePath);
|
|
1133
|
+
if (!exists) {
|
|
1134
|
+
debug("lockfile.not_found", { path: lockfilePath });
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
const data = await readJsonFile(lockfilePath);
|
|
1139
|
+
if (typeof data !== "object" || data === null || data.lockfileVersion !== LOCKFILE_VERSION) {
|
|
1140
|
+
warn("lockfile.invalid_version", { path: lockfilePath });
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const record = data;
|
|
1144
|
+
if (typeof record.files !== "object" || record.files === null) {
|
|
1145
|
+
warn("lockfile.invalid_structure", { path: lockfilePath });
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
return data;
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
warn("lockfile.read_failed", { path: lockfilePath, error: String(err) });
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1128
1154
|
async function writeLockfile(targetDir, lockfile) {
|
|
1129
1155
|
const lockfilePath = join4(targetDir, LOCKFILE_NAME);
|
|
1130
1156
|
await writeJsonFile(lockfilePath, lockfile);
|
|
@@ -1642,7 +1668,7 @@ import { join as join6 } from "node:path";
|
|
|
1642
1668
|
var package_default = {
|
|
1643
1669
|
name: "oh-my-customcode",
|
|
1644
1670
|
workspaces: ["packages/*"],
|
|
1645
|
-
version: "0.
|
|
1671
|
+
version: "0.51.0",
|
|
1646
1672
|
description: "Batteries-included agent harness for Claude Code",
|
|
1647
1673
|
type: "module",
|
|
1648
1674
|
bin: {
|
|
@@ -1875,7 +1901,7 @@ async function handleBackupIfRequested(targetDir, backup, result) {
|
|
|
1875
1901
|
result.backedUpPaths.push(backupPath);
|
|
1876
1902
|
info("update.backup_created", { path: backupPath });
|
|
1877
1903
|
}
|
|
1878
|
-
async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config) {
|
|
1904
|
+
async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile) {
|
|
1879
1905
|
const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
|
|
1880
1906
|
if (!componentUpdate && !options.force) {
|
|
1881
1907
|
result.skippedComponents.push(component);
|
|
@@ -1887,7 +1913,7 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
1887
1913
|
return;
|
|
1888
1914
|
}
|
|
1889
1915
|
try {
|
|
1890
|
-
const preserved = await updateComponent(targetDir, component, customizations, options, config);
|
|
1916
|
+
const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
|
|
1891
1917
|
result.updatedComponents.push(component);
|
|
1892
1918
|
result.preservedFiles.push(...preserved);
|
|
1893
1919
|
} catch (err) {
|
|
@@ -1896,9 +1922,9 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
|
|
|
1896
1922
|
result.skippedComponents.push(component);
|
|
1897
1923
|
}
|
|
1898
1924
|
}
|
|
1899
|
-
async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config) {
|
|
1925
|
+
async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config, lockfile) {
|
|
1900
1926
|
for (const component of components) {
|
|
1901
|
-
await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config);
|
|
1927
|
+
await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile);
|
|
1902
1928
|
}
|
|
1903
1929
|
}
|
|
1904
1930
|
function getEntryTemplateName2(language) {
|
|
@@ -2080,8 +2106,9 @@ async function update(options) {
|
|
|
2080
2106
|
const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
|
|
2081
2107
|
const configPreserveFiles = resolveConfigPreserveFiles(options, config);
|
|
2082
2108
|
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
|
|
2109
|
+
const lockfile = await readLockfile(options.targetDir);
|
|
2083
2110
|
const components = options.components || getAllUpdateComponents();
|
|
2084
|
-
await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
|
|
2111
|
+
await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
|
|
2085
2112
|
await runFullUpdatePostProcessing(options, result, config);
|
|
2086
2113
|
const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
|
|
2087
2114
|
if (lockfileResult.warning) {
|
|
@@ -2163,15 +2190,46 @@ async function componentHasUpdate(_targetDir, component, config) {
|
|
|
2163
2190
|
const latestVersion = await getLatestVersion();
|
|
2164
2191
|
return installedVersion !== latestVersion;
|
|
2165
2192
|
}
|
|
2166
|
-
async function
|
|
2193
|
+
async function shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile) {
|
|
2194
|
+
if (!lockfile) {
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
const lockfileEntry = lockfile.files[lockfileKey];
|
|
2198
|
+
if (!lockfileEntry) {
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
if (!await fileExists(targetFilePath)) {
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const currentHash = await computeFileHash(targetFilePath);
|
|
2206
|
+
return currentHash !== lockfileEntry.templateHash;
|
|
2207
|
+
} catch {
|
|
2208
|
+
return true;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll, lockfile, targetDir) {
|
|
2167
2212
|
if (forceOverwriteAll) {
|
|
2168
|
-
const
|
|
2169
|
-
return { skipPaths: [], warnedPaths };
|
|
2213
|
+
const warnedPaths2 = await findProtectedFilesInDir(srcPath, componentPath);
|
|
2214
|
+
return { skipPaths: [], warnedPaths: warnedPaths2, updatedPaths: [] };
|
|
2170
2215
|
}
|
|
2171
2216
|
const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
|
|
2172
2217
|
const path = await import("node:path");
|
|
2173
|
-
const skipPaths =
|
|
2174
|
-
|
|
2218
|
+
const skipPaths = [];
|
|
2219
|
+
const warnedPaths = [];
|
|
2220
|
+
const updatedPaths = [];
|
|
2221
|
+
for (const p of protectedRelative) {
|
|
2222
|
+
const targetFilePath = join6(targetDir, componentPath, p);
|
|
2223
|
+
const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
|
|
2224
|
+
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
2225
|
+
if (shouldSkip) {
|
|
2226
|
+
skipPaths.push(path.relative(destPath, join6(destPath, p)));
|
|
2227
|
+
warnedPaths.push(p);
|
|
2228
|
+
} else {
|
|
2229
|
+
updatedPaths.push(p);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return { skipPaths, warnedPaths, updatedPaths };
|
|
2175
2233
|
}
|
|
2176
2234
|
function isEntryProtected(relPath, componentRelativePrefix) {
|
|
2177
2235
|
if (isProtectedFile(relPath)) {
|
|
@@ -2207,7 +2265,7 @@ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
|
|
|
2207
2265
|
}
|
|
2208
2266
|
return protected_;
|
|
2209
2267
|
}
|
|
2210
|
-
async function updateComponent(targetDir, component, customizations, options, config) {
|
|
2268
|
+
async function updateComponent(targetDir, component, customizations, options, config, lockfile) {
|
|
2211
2269
|
const preservedFiles = [];
|
|
2212
2270
|
const componentPath = getComponentPath2(component);
|
|
2213
2271
|
const srcPath = resolveTemplatePath(componentPath);
|
|
@@ -2224,7 +2282,11 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
2224
2282
|
skipPaths.push(cc.path);
|
|
2225
2283
|
}
|
|
2226
2284
|
}
|
|
2227
|
-
const {
|
|
2285
|
+
const {
|
|
2286
|
+
skipPaths: protectedSkipPaths,
|
|
2287
|
+
warnedPaths: protectedWarnedPaths,
|
|
2288
|
+
updatedPaths: protectedUpdatedPaths
|
|
2289
|
+
} = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll, lockfile, targetDir);
|
|
2228
2290
|
for (const protectedPath of protectedWarnedPaths) {
|
|
2229
2291
|
if (options.forceOverwriteAll) {
|
|
2230
2292
|
warn("update.protected_file_force_overwrite", {
|
|
@@ -2236,10 +2298,17 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
2236
2298
|
warn("update.protected_file_skipped", {
|
|
2237
2299
|
file: protectedPath,
|
|
2238
2300
|
component,
|
|
2239
|
-
hint: "File
|
|
2301
|
+
hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
|
|
2240
2302
|
});
|
|
2241
2303
|
}
|
|
2242
2304
|
}
|
|
2305
|
+
for (const updatedPath of protectedUpdatedPaths) {
|
|
2306
|
+
info("update.protected_file_updated", {
|
|
2307
|
+
file: updatedPath,
|
|
2308
|
+
component,
|
|
2309
|
+
hint: "Protected file updated (unmodified by user, matches lockfile hash)."
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2243
2312
|
skipPaths.push(...protectedSkipPaths);
|
|
2244
2313
|
const path = await import("node:path");
|
|
2245
2314
|
const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join6(targetDir, p)));
|
package/package.json
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scout
|
|
3
|
+
description: Analyze external URL to evaluate fit with oh-my-customcode project and auto-create GitHub issue with verdict
|
|
4
|
+
scope: core
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
user-invocable: true
|
|
7
|
+
argument-hint: "<url>"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /scout — External Link Analysis
|
|
11
|
+
|
|
12
|
+
Analyze an external URL (tech blog, tool, library, methodology) to evaluate its fit with the oh-my-customcode project and auto-create a GitHub issue with a structured verdict.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/scout <url>
|
|
18
|
+
/scout https://news.hada.io/topic?id=27673
|
|
19
|
+
/scout https://github.com/user/repo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Verdict Taxonomy
|
|
23
|
+
|
|
24
|
+
| Verdict | Meaning | Label | Follow-up |
|
|
25
|
+
|---------|---------|-------|-----------|
|
|
26
|
+
| **INTERNALIZE** | Aligns with project philosophy; should become a skill/agent/guide | `scout:internalize` + `P1`/`P2`/`P3` | `/research` or direct implementation |
|
|
27
|
+
| **INTEGRATE** | Useful but best kept as external dependency | `scout:integrate` + `P2`/`P3` | Plugin/MCP integration review |
|
|
28
|
+
| **SKIP** | Irrelevant or duplicates existing functionality | `scout:skip` | Issue created then closed |
|
|
29
|
+
|
|
30
|
+
## Pre-flight Guards
|
|
31
|
+
|
|
32
|
+
### Guard 1: URL Validity (GATE)
|
|
33
|
+
|
|
34
|
+
Before any work, validate the URL:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Check URL is syntactically valid
|
|
38
|
+
echo "$URL" | grep -qE '^https?://'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If invalid: `[Pre-flight] GATE: Invalid or unreachable URL. Please check and retry.` — abort.
|
|
42
|
+
|
|
43
|
+
### Guard 2: Duplicate Scout (WARN)
|
|
44
|
+
|
|
45
|
+
Search existing GitHub issues for prior scout reports on the same URL domain:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
DOMAIN=$(echo "$URL" | sed 's|https\?://||' | cut -d'/' -f1)
|
|
49
|
+
gh issue list --state all --label "scout:internalize,scout:integrate,scout:skip" \
|
|
50
|
+
--json number,title,body --jq ".[] | select(.body | contains(\"$DOMAIN\"))" 2>/dev/null
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If found: `[Pre-flight] WARN: Similar URL already scouted in issue #N. Proceed anyway? [Y/n]`
|
|
54
|
+
|
|
55
|
+
## Display Format
|
|
56
|
+
|
|
57
|
+
Before execution, show the plan:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
[Scout] {url}
|
|
61
|
+
├── Phase 1: Fetch & Summarize
|
|
62
|
+
├── Phase 2: Load Project Philosophy
|
|
63
|
+
├── Phase 3: Fit Analysis (sonnet)
|
|
64
|
+
└── Phase 4: Issue Creation
|
|
65
|
+
|
|
66
|
+
Estimated: ~1 min | Cost: ~$0.5-1.5
|
|
67
|
+
Execute? [Y/n]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Workflow
|
|
71
|
+
|
|
72
|
+
### Phase 1: Fetch & Summarize
|
|
73
|
+
|
|
74
|
+
1. `WebFetch(url)` — retrieve page content
|
|
75
|
+
2. Extract core information:
|
|
76
|
+
- Title and purpose
|
|
77
|
+
- Key technology / methodology
|
|
78
|
+
- Approach and principles
|
|
79
|
+
3. If fetch fails — report error, abort
|
|
80
|
+
|
|
81
|
+
### Phase 2: Load Project Philosophy
|
|
82
|
+
|
|
83
|
+
1. `Read(CLAUDE.md)` — extract architecture philosophy:
|
|
84
|
+
- Compilation metaphor (source -> build -> artifact)
|
|
85
|
+
- Separation of concerns (R006)
|
|
86
|
+
- Dynamic agent creation ("no expert? create one")
|
|
87
|
+
- Skill/agent/guide/rule structure
|
|
88
|
+
2. `Read(README.md)` — extract project overview and component inventory
|
|
89
|
+
3. `Glob(.claude/skills/*/SKILL.md)` — list existing skills for overlap detection
|
|
90
|
+
|
|
91
|
+
### Phase 3: Fit Analysis
|
|
92
|
+
|
|
93
|
+
Spawn 1 sonnet agent with the following analysis prompt.
|
|
94
|
+
|
|
95
|
+
**Inputs**:
|
|
96
|
+
- Fetched content summary (Phase 1)
|
|
97
|
+
- Project philosophy context (Phase 2)
|
|
98
|
+
- Existing skill list (Phase 2)
|
|
99
|
+
|
|
100
|
+
**Analysis dimensions**:
|
|
101
|
+
|
|
102
|
+
| Dimension | Question |
|
|
103
|
+
|-----------|----------|
|
|
104
|
+
| Philosophy alignment | Does it match the compilation metaphor, separation of concerns, "create experts on demand"? |
|
|
105
|
+
| Technical fit | Does it complement or overlap with existing skills/agents/guides? |
|
|
106
|
+
| Integration effort | How much work to internalize vs. use externally? |
|
|
107
|
+
| Value proposition | What concrete benefit does it bring to the project? |
|
|
108
|
+
|
|
109
|
+
**Agent prompt template**:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
You are a project fit analyst. Given:
|
|
113
|
+
|
|
114
|
+
1. External content summary:
|
|
115
|
+
{phase1_summary}
|
|
116
|
+
|
|
117
|
+
2. Project philosophy:
|
|
118
|
+
{phase2_philosophy}
|
|
119
|
+
|
|
120
|
+
3. Existing skills ({skill_count} total):
|
|
121
|
+
{skill_list}
|
|
122
|
+
|
|
123
|
+
Analyze the external content against the project philosophy across 4 dimensions:
|
|
124
|
+
- Philosophy alignment
|
|
125
|
+
- Technical fit (overlap with existing skills?)
|
|
126
|
+
- Integration effort (XS/S/M/L)
|
|
127
|
+
- Value proposition
|
|
128
|
+
|
|
129
|
+
Return a structured verdict:
|
|
130
|
+
- verdict: INTERNALIZE | INTEGRATE | SKIP
|
|
131
|
+
- priority: P1 | P2 | P3
|
|
132
|
+
- rationale: 2-3 sentences
|
|
133
|
+
- philosophy_table: criterion/fit/rationale for each dimension
|
|
134
|
+
- recommendation: specific integration plan or skip reason
|
|
135
|
+
- next_steps: 2-3 actionable items
|
|
136
|
+
- escalation: true/false (INTERNALIZE + M/L effort = true)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Output**: Structured verdict with rationale.
|
|
140
|
+
|
|
141
|
+
### Phase 4: Issue Creation
|
|
142
|
+
|
|
143
|
+
1. Ensure scout labels exist (defensive, idempotent):
|
|
144
|
+
```bash
|
|
145
|
+
gh label create "scout:internalize" --color "0E8A16" --description "Scout: should be internalized" 2>/dev/null || true
|
|
146
|
+
gh label create "scout:integrate" --color "1D76DB" --description "Scout: keep as external" 2>/dev/null || true
|
|
147
|
+
gh label create "scout:skip" --color "D4C5F9" --description "Scout: skip" 2>/dev/null || true
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. Create GitHub issue:
|
|
151
|
+
```bash
|
|
152
|
+
gh issue create \
|
|
153
|
+
--title "[scout:{verdict}] {content_title}" \
|
|
154
|
+
--label "scout:{verdict},P{n}" \
|
|
155
|
+
--body "{issue_body}"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
3. If verdict is `SKIP`: auto-close the issue:
|
|
159
|
+
```bash
|
|
160
|
+
gh issue close {number} -c "Auto-closed: scout verdict is SKIP"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Issue Body Template
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
## Scout Report: {title}
|
|
167
|
+
|
|
168
|
+
**Source**: {url}
|
|
169
|
+
**Verdict**: {INTERNALIZE / INTEGRATE / SKIP}
|
|
170
|
+
**Priority**: {P1 / P2 / P3}
|
|
171
|
+
|
|
172
|
+
## Summary
|
|
173
|
+
{2-3 sentence summary of the external content}
|
|
174
|
+
|
|
175
|
+
## Philosophy Alignment
|
|
176
|
+
| Criterion | Fit | Rationale |
|
|
177
|
+
|-----------|-----|-----------|
|
|
178
|
+
| Compilation metaphor | {check/cross} | {explanation} |
|
|
179
|
+
| Separation of concerns (R006) | {check/cross} | {explanation} |
|
|
180
|
+
| Dynamic agent creation | {check/cross} | {explanation} |
|
|
181
|
+
| Existing skill overlap | {check/cross} | {overlapping skills list} |
|
|
182
|
+
|
|
183
|
+
## Recommendation
|
|
184
|
+
{Specific integration plan — which skill/agent/guide to create, or why to skip}
|
|
185
|
+
|
|
186
|
+
## Next Steps
|
|
187
|
+
- [ ] {follow-up action 1}
|
|
188
|
+
- [ ] {follow-up action 2}
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
Generated by `/scout`
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Escalation
|
|
195
|
+
|
|
196
|
+
When verdict is `INTERNALIZE` and integration effort is M or L:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
[Advisory] Deep analysis recommended.
|
|
200
|
+
└── Consider running: /research {url}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Result Display
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
[Scout Complete] {title}
|
|
207
|
+
├── Verdict: {INTERNALIZE / INTEGRATE / SKIP}
|
|
208
|
+
├── Priority: {P1 / P2 / P3}
|
|
209
|
+
├── Issue: #{number}
|
|
210
|
+
└── Escalation: {/research recommended | none}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Model Selection
|
|
214
|
+
|
|
215
|
+
| Phase | Model | Rationale |
|
|
216
|
+
|-------|-------|-----------|
|
|
217
|
+
| Phase 1 (Fetch) | orchestrator | Simple WebFetch, no agent needed |
|
|
218
|
+
| Phase 2 (Load) | orchestrator | Simple Read/Glob, no agent needed |
|
|
219
|
+
| Phase 3 (Analysis) | sonnet | Balanced reasoning for fit analysis |
|
|
220
|
+
| Phase 4 (Issue) | orchestrator | gh issue create via Bash |
|
|
221
|
+
|
|
222
|
+
## Integration
|
|
223
|
+
|
|
224
|
+
| Rule | How |
|
|
225
|
+
|------|-----|
|
|
226
|
+
| R009 | Single agent in Phase 3 — no parallelism needed |
|
|
227
|
+
| R010 | Orchestrator manages phases 1/2/4; analysis delegated to sonnet agent in Phase 3 |
|
|
228
|
+
| R015 | Display scout plan before execution (Display Format section) |
|
|
229
|
+
|
|
230
|
+
## When NOT to Use
|
|
231
|
+
|
|
232
|
+
| Scenario | Better Alternative |
|
|
233
|
+
|----------|--------------------|
|
|
234
|
+
| Deep multi-source research | `/research <url>` |
|
|
235
|
+
| Internal project analysis | `/omcustom:analysis` |
|
|
236
|
+
| Known tool evaluation | Direct agent conversation |
|
|
237
|
+
| Bulk URL analysis (5+) | `/research` with URL list |
|
|
238
|
+
|
|
239
|
+
## Differences from /research
|
|
240
|
+
|
|
241
|
+
| Aspect | /scout | /research |
|
|
242
|
+
|--------|--------|-----------|
|
|
243
|
+
| Purpose | Quick fit evaluation | Deep multi-dimensional analysis |
|
|
244
|
+
| Teams | 1 agent | 10 teams |
|
|
245
|
+
| Cost | ~$0.5-1.5 | ~$8-15 |
|
|
246
|
+
| Duration | 1-2 min | 10-20 min |
|
|
247
|
+
| Output | Issue with verdict | Full report with ADOPT/ADAPT/AVOID |
|
|
248
|
+
| When | First contact with new link | Deep dive after scout recommends |
|