oh-my-customcode 2.0.0 → 2.0.2
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 +1 -1
- package/dist/cli/index.js +40 -14
- package/dist/index.js +40 -14
- package/package.json +1 -1
- package/templates/.claude/hooks/scripts/claude-md-reinject.sh +7 -1
- package/templates/.claude/hooks/scripts/session-reflection.sh +60 -15
- package/templates/.claude/hooks/scripts/stuck-detector.sh +110 -14
- package/templates/.claude/rules/MAY-optimization.md +1 -1
- package/templates/.claude/rules/MUST-agent-design.md +5 -4
- package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
- package/templates/.claude/rules/SHOULD-verification-ladder.md +9 -0
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +3 -1
- package/templates/guides/claude-code/15-version-compatibility.md +397 -0
- package/templates/guides/external-tools/ecc-absorption-decisions.md +1 -1
- package/templates/guides/multi-model-routing/README.md +2 -1
- package/templates/guides/multi-provider-exec/README.md +1 -1
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +3 -1
package/README.md
CHANGED
|
@@ -296,7 +296,7 @@ your-project/
|
|
|
296
296
|
|
|
297
297
|
## External Tool Integrations
|
|
298
298
|
|
|
299
|
-
Starting with
|
|
299
|
+
Starting with v1.1.107, project RTK integration is retired: the public `/rtk-exec` skill, automatic installation, doctor integration, and RTK hooks are removed. Separately installed RTK binaries remain on your machine. Updates automatically remove retired managed files only when their hashes match the previous lockfile. Modified files and files with unknown ownership are preserved and reported as conflicts; retained user skill files may still be discovered automatically. Other tools are optional:
|
|
300
300
|
|
|
301
301
|
| Tool | Purpose | Install | Status |
|
|
302
302
|
|------|---------|---------|--------|
|
package/dist/cli/index.js
CHANGED
|
@@ -335,7 +335,7 @@ var init_package = __esm(() => {
|
|
|
335
335
|
workspaces: [
|
|
336
336
|
"packages/*"
|
|
337
337
|
],
|
|
338
|
-
version: "2.0.
|
|
338
|
+
version: "2.0.2",
|
|
339
339
|
description: "Batteries-included agent harness for Claude Code",
|
|
340
340
|
type: "module",
|
|
341
341
|
bin: {
|
|
@@ -15800,7 +15800,7 @@ var MESSAGES = {
|
|
|
15800
15800
|
"update.file_applied": "Applied update to {{path}}",
|
|
15801
15801
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
15802
15802
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
15803
|
-
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}}
|
|
15803
|
+
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} selected for update: The protected-file policy allows this template update.",
|
|
15804
15804
|
"update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
|
|
15805
15805
|
"update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
|
|
15806
15806
|
"update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
|
|
@@ -15836,8 +15836,8 @@ var MESSAGES = {
|
|
|
15836
15836
|
"update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
|
|
15837
15837
|
"update.entry_merge_warning": "Entry document merge warning: {{warning}}",
|
|
15838
15838
|
"update.entry_template_not_found": "Entry document template not found: {{template}}",
|
|
15839
|
-
"update.protected_file_force_overwrite": "Protected file {{file}} in {{component}}
|
|
15840
|
-
"update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped:
|
|
15839
|
+
"update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} selected for overwrite: The --force-overwrite-all option bypasses the protected-file preservation policy.",
|
|
15840
|
+
"update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: The protected-file preservation policy skips this update. Use --force-overwrite-all to override.",
|
|
15841
15841
|
"update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
|
|
15842
15842
|
"general.done": "Done!",
|
|
15843
15843
|
"general.failed": "Failed",
|
|
@@ -15913,7 +15913,7 @@ var MESSAGES = {
|
|
|
15913
15913
|
"update.file_applied": "{{path}} 업데이트 적용",
|
|
15914
15914
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
15915
15915
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
15916
|
-
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}})
|
|
15916
|
+
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
|
|
15917
15917
|
"update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
|
|
15918
15918
|
"update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
|
|
15919
15919
|
"update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
|
|
@@ -15949,8 +15949,8 @@ var MESSAGES = {
|
|
|
15949
15949
|
"update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
|
|
15950
15950
|
"update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
|
|
15951
15951
|
"update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
|
|
15952
|
-
"update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}
|
|
15953
|
-
"update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다:
|
|
15952
|
+
"update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
|
|
15953
|
+
"update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
|
|
15954
15954
|
"update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
|
|
15955
15955
|
"general.done": "완료!",
|
|
15956
15956
|
"general.failed": "실패",
|
|
@@ -22135,6 +22135,29 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
|
|
|
22135
22135
|
});
|
|
22136
22136
|
}
|
|
22137
22137
|
}
|
|
22138
|
+
async function getUpdateVersionDecision(installedVersion, cliVersion) {
|
|
22139
|
+
if (!(installedVersion !== "0.0.0" && compareSemver2(installedVersion, cliVersion) > 0)) {
|
|
22140
|
+
return "standard";
|
|
22141
|
+
}
|
|
22142
|
+
if (installedVersion !== "2.0.0") {
|
|
22143
|
+
return "blocked";
|
|
22144
|
+
}
|
|
22145
|
+
const match = /^1\.1\.(0|[1-9]\d*)$/.exec(cliVersion);
|
|
22146
|
+
if (!match) {
|
|
22147
|
+
return "blocked";
|
|
22148
|
+
}
|
|
22149
|
+
const patch = Number(match[1]);
|
|
22150
|
+
if (!Number.isSafeInteger(patch) || patch < 107) {
|
|
22151
|
+
return "blocked";
|
|
22152
|
+
}
|
|
22153
|
+
const manifestPath = resolveTemplatePath(getProviderLayout().manifestFile);
|
|
22154
|
+
if (!await fileExists(manifestPath)) {
|
|
22155
|
+
return "blocked";
|
|
22156
|
+
}
|
|
22157
|
+
const manifest = await readJsonFile(manifestPath);
|
|
22158
|
+
const correctionAllowed = typeof manifest === "object" && manifest !== null && !Array.isArray(manifest) && "version" in manifest && manifest.version === cliVersion;
|
|
22159
|
+
return correctionAllowed ? "correction" : "blocked";
|
|
22160
|
+
}
|
|
22138
22161
|
function compareSemver2(a, b) {
|
|
22139
22162
|
const pa = a.split(".").map(Number);
|
|
22140
22163
|
const pb = b.split(".").map(Number);
|
|
@@ -22195,7 +22218,8 @@ async function update(options) {
|
|
|
22195
22218
|
const config = await loadConfig(options.targetDir);
|
|
22196
22219
|
result.previousVersion = config.version;
|
|
22197
22220
|
const cliVersion = package_default.version;
|
|
22198
|
-
|
|
22221
|
+
const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
|
|
22222
|
+
if (versionDecision === "blocked") {
|
|
22199
22223
|
result.success = false;
|
|
22200
22224
|
result.error = `Downgrade prevented: project has v${result.previousVersion} but CLI is v${cliVersion}. Update the CLI first: npm install -g oh-my-customcode@latest`;
|
|
22201
22225
|
return result;
|
|
@@ -22204,6 +22228,11 @@ async function update(options) {
|
|
|
22204
22228
|
return result;
|
|
22205
22229
|
}
|
|
22206
22230
|
const updateCheck = await checkForUpdates(options.targetDir);
|
|
22231
|
+
if (versionDecision === "correction" && updateCheck.latestVersion !== cliVersion) {
|
|
22232
|
+
result.success = false;
|
|
22233
|
+
result.error = `Version correction prevented: CLI is v${cliVersion} but templates are v${updateCheck.latestVersion}. Reinstall the CLI: npm install -g oh-my-customcode@latest`;
|
|
22234
|
+
return result;
|
|
22235
|
+
}
|
|
22207
22236
|
result.newVersion = updateCheck.latestVersion;
|
|
22208
22237
|
if (!updateCheck.hasUpdates && !options.force) {
|
|
22209
22238
|
info("update.no_updates");
|
|
@@ -22377,22 +22406,19 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
22377
22406
|
if (options.forceOverwriteAll) {
|
|
22378
22407
|
warn("update.protected_file_force_overwrite", {
|
|
22379
22408
|
file: protectedPath,
|
|
22380
|
-
component
|
|
22381
|
-
hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
|
|
22409
|
+
component
|
|
22382
22410
|
});
|
|
22383
22411
|
} else {
|
|
22384
22412
|
warn("update.protected_file_skipped", {
|
|
22385
22413
|
file: protectedPath,
|
|
22386
|
-
component
|
|
22387
|
-
hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
|
|
22414
|
+
component
|
|
22388
22415
|
});
|
|
22389
22416
|
}
|
|
22390
22417
|
}
|
|
22391
22418
|
for (const updatedPath of protectedUpdatedPaths) {
|
|
22392
22419
|
info("update.protected_file_updated", {
|
|
22393
22420
|
file: updatedPath,
|
|
22394
|
-
component
|
|
22395
|
-
hint: "Protected file updated (unmodified by user, matches lockfile hash)."
|
|
22421
|
+
component
|
|
22396
22422
|
});
|
|
22397
22423
|
}
|
|
22398
22424
|
skipPaths.push(...protectedSkipPaths);
|
package/dist/index.js
CHANGED
|
@@ -416,7 +416,7 @@ var MESSAGES = {
|
|
|
416
416
|
"update.file_applied": "Applied update to {{path}}",
|
|
417
417
|
"update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
|
|
418
418
|
"update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
|
|
419
|
-
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}}
|
|
419
|
+
"update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} selected for update: The protected-file policy allows this template update.",
|
|
420
420
|
"update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
|
|
421
421
|
"update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
|
|
422
422
|
"update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
|
|
@@ -452,8 +452,8 @@ var MESSAGES = {
|
|
|
452
452
|
"update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
|
|
453
453
|
"update.entry_merge_warning": "Entry document merge warning: {{warning}}",
|
|
454
454
|
"update.entry_template_not_found": "Entry document template not found: {{template}}",
|
|
455
|
-
"update.protected_file_force_overwrite": "Protected file {{file}} in {{component}}
|
|
456
|
-
"update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped:
|
|
455
|
+
"update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} selected for overwrite: The --force-overwrite-all option bypasses the protected-file preservation policy.",
|
|
456
|
+
"update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: The protected-file preservation policy skips this update. Use --force-overwrite-all to override.",
|
|
457
457
|
"update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
|
|
458
458
|
"general.done": "Done!",
|
|
459
459
|
"general.failed": "Failed",
|
|
@@ -529,7 +529,7 @@ var MESSAGES = {
|
|
|
529
529
|
"update.file_applied": "{{path}} 업데이트 적용",
|
|
530
530
|
"update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
|
|
531
531
|
"update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
|
|
532
|
-
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}})
|
|
532
|
+
"update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
|
|
533
533
|
"update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
|
|
534
534
|
"update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
|
|
535
535
|
"update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
|
|
@@ -565,8 +565,8 @@ var MESSAGES = {
|
|
|
565
565
|
"update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
|
|
566
566
|
"update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
|
|
567
567
|
"update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
|
|
568
|
-
"update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}
|
|
569
|
-
"update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다:
|
|
568
|
+
"update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
|
|
569
|
+
"update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
|
|
570
570
|
"update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
|
|
571
571
|
"general.done": "완료!",
|
|
572
572
|
"general.failed": "실패",
|
|
@@ -2906,7 +2906,7 @@ var package_default = {
|
|
|
2906
2906
|
workspaces: [
|
|
2907
2907
|
"packages/*"
|
|
2908
2908
|
],
|
|
2909
|
-
version: "2.0.
|
|
2909
|
+
version: "2.0.2",
|
|
2910
2910
|
description: "Batteries-included agent harness for Claude Code",
|
|
2911
2911
|
type: "module",
|
|
2912
2912
|
bin: {
|
|
@@ -5901,6 +5901,29 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
|
|
|
5901
5901
|
});
|
|
5902
5902
|
}
|
|
5903
5903
|
}
|
|
5904
|
+
async function getUpdateVersionDecision(installedVersion, cliVersion) {
|
|
5905
|
+
if (!(installedVersion !== "0.0.0" && compareSemver(installedVersion, cliVersion) > 0)) {
|
|
5906
|
+
return "standard";
|
|
5907
|
+
}
|
|
5908
|
+
if (installedVersion !== "2.0.0") {
|
|
5909
|
+
return "blocked";
|
|
5910
|
+
}
|
|
5911
|
+
const match = /^1\.1\.(0|[1-9]\d*)$/.exec(cliVersion);
|
|
5912
|
+
if (!match) {
|
|
5913
|
+
return "blocked";
|
|
5914
|
+
}
|
|
5915
|
+
const patch = Number(match[1]);
|
|
5916
|
+
if (!Number.isSafeInteger(patch) || patch < 107) {
|
|
5917
|
+
return "blocked";
|
|
5918
|
+
}
|
|
5919
|
+
const manifestPath = resolveTemplatePath(getProviderLayout().manifestFile);
|
|
5920
|
+
if (!await fileExists(manifestPath)) {
|
|
5921
|
+
return "blocked";
|
|
5922
|
+
}
|
|
5923
|
+
const manifest = await readJsonFile(manifestPath);
|
|
5924
|
+
const correctionAllowed = typeof manifest === "object" && manifest !== null && !Array.isArray(manifest) && "version" in manifest && manifest.version === cliVersion;
|
|
5925
|
+
return correctionAllowed ? "correction" : "blocked";
|
|
5926
|
+
}
|
|
5904
5927
|
function compareSemver(a, b) {
|
|
5905
5928
|
const pa = a.split(".").map(Number);
|
|
5906
5929
|
const pb = b.split(".").map(Number);
|
|
@@ -5961,7 +5984,8 @@ async function update(options) {
|
|
|
5961
5984
|
const config = await loadConfig(options.targetDir);
|
|
5962
5985
|
result.previousVersion = config.version;
|
|
5963
5986
|
const cliVersion = package_default.version;
|
|
5964
|
-
|
|
5987
|
+
const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
|
|
5988
|
+
if (versionDecision === "blocked") {
|
|
5965
5989
|
result.success = false;
|
|
5966
5990
|
result.error = `Downgrade prevented: project has v${result.previousVersion} but CLI is v${cliVersion}. Update the CLI first: npm install -g oh-my-customcode@latest`;
|
|
5967
5991
|
return result;
|
|
@@ -5970,6 +5994,11 @@ async function update(options) {
|
|
|
5970
5994
|
return result;
|
|
5971
5995
|
}
|
|
5972
5996
|
const updateCheck = await checkForUpdates(options.targetDir);
|
|
5997
|
+
if (versionDecision === "correction" && updateCheck.latestVersion !== cliVersion) {
|
|
5998
|
+
result.success = false;
|
|
5999
|
+
result.error = `Version correction prevented: CLI is v${cliVersion} but templates are v${updateCheck.latestVersion}. Reinstall the CLI: npm install -g oh-my-customcode@latest`;
|
|
6000
|
+
return result;
|
|
6001
|
+
}
|
|
5973
6002
|
result.newVersion = updateCheck.latestVersion;
|
|
5974
6003
|
if (!updateCheck.hasUpdates && !options.force) {
|
|
5975
6004
|
info("update.no_updates");
|
|
@@ -6164,22 +6193,19 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
6164
6193
|
if (options.forceOverwriteAll) {
|
|
6165
6194
|
warn("update.protected_file_force_overwrite", {
|
|
6166
6195
|
file: protectedPath,
|
|
6167
|
-
component
|
|
6168
|
-
hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
|
|
6196
|
+
component
|
|
6169
6197
|
});
|
|
6170
6198
|
} else {
|
|
6171
6199
|
warn("update.protected_file_skipped", {
|
|
6172
6200
|
file: protectedPath,
|
|
6173
|
-
component
|
|
6174
|
-
hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
|
|
6201
|
+
component
|
|
6175
6202
|
});
|
|
6176
6203
|
}
|
|
6177
6204
|
}
|
|
6178
6205
|
for (const updatedPath of protectedUpdatedPaths) {
|
|
6179
6206
|
info("update.protected_file_updated", {
|
|
6180
6207
|
file: updatedPath,
|
|
6181
|
-
component
|
|
6182
|
-
hint: "Protected file updated (unmodified by user, matches lockfile hash)."
|
|
6208
|
+
component
|
|
6183
6209
|
});
|
|
6184
6210
|
}
|
|
6185
6211
|
skipPaths.push(...protectedSkipPaths);
|
package/package.json
CHANGED
|
@@ -38,7 +38,13 @@ fi
|
|
|
38
38
|
|
|
39
39
|
source_field=$(printf '%s' "$input" | jq -r '.source // "unknown"' 2>/dev/null) || source_field="unknown"
|
|
40
40
|
|
|
41
|
-
# #1770:
|
|
41
|
+
# #1770 / B-L1: nonempty CLAUDE_PROJECT_DIR selects the anchor. When manually
|
|
42
|
+
# running inside a Git monorepo package, set it to that package to select its
|
|
43
|
+
# CLAUDE.md; unset/empty instead selects the Git toplevel, then pwd if Git fails.
|
|
44
|
+
# The script location is never the fallback project anchor.
|
|
45
|
+
# B-L2: a nonempty invalid anchor is authoritative; there is no Git/cwd retry.
|
|
46
|
+
# If its CLAUDE.md is missing (including a missing directory or a file anchor),
|
|
47
|
+
# the file check below returns silently with exit 0, preserving advisory behavior.
|
|
42
48
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
43
49
|
CLAUDE_MD="$PROJECT_ROOT/CLAUDE.md"
|
|
44
50
|
|
|
@@ -29,22 +29,67 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
29
29
|
exit 0
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
|
-
# ──
|
|
33
|
-
|
|
32
|
+
# ── 부모 필드 추출: 한 jq 생산자, 독립 오류 fallback 유지 ──
|
|
33
|
+
# jq -j는 session_id의 원래 raw/pretty 출력을 유지한다. 문자열의 실제 NUL만
|
|
34
|
+
# 제거하여 필드 구분자와 충돌하지 않게 하고, 각 필드의 끝 LF는 기존 command
|
|
35
|
+
# substitution처럼 제거한다. JSON 문자열 안의 escaped NUL은 그대로 유지한다.
|
|
36
|
+
# slurp 후에도 입력별 출력 순서와 오류 전 부분 출력 + fallback을 유지한다.
|
|
37
|
+
# process substitution의 jq 실패를 잃지 않도록 별도 종료 상태 필드를 보낸다.
|
|
38
|
+
parent_fields=()
|
|
39
|
+
while IFS= read -r -d '' parent_field; do
|
|
40
|
+
parent_fields[${#parent_fields[@]}]="$parent_field"
|
|
41
|
+
done < <(
|
|
42
|
+
if printf '%s\n' "$input" | jq -sj '
|
|
43
|
+
. as $inputs
|
|
44
|
+
| [try ($inputs[] | .background_tasks // [] | tojson) catch "[]"] as $bg
|
|
45
|
+
| [try ($inputs[] | .session_crons // [] | tojson) catch "[]"] as $crons
|
|
46
|
+
| [try ($bg[] | fromjson | length) catch 0] as $bg_count
|
|
47
|
+
| [try ($crons[] | fromjson | length) catch 0] as $cron_count
|
|
48
|
+
| [try ($bg[] | fromjson
|
|
49
|
+
| [.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")]
|
|
50
|
+
| length) catch 0] as $dangling
|
|
51
|
+
| ($inputs[] | .session_id // empty
|
|
52
|
+
| if type == "string" then gsub("\u0000"; "") else . end
|
|
53
|
+
| (., "\n")),
|
|
54
|
+
"\u0000",
|
|
55
|
+
($bg | join("\n")), "\u0000",
|
|
56
|
+
($crons | join("\n")), "\u0000",
|
|
57
|
+
($bg_count | map(tostring) | join("\n")), "\u0000",
|
|
58
|
+
($cron_count | map(tostring) | join("\n")), "\u0000",
|
|
59
|
+
($dangling | map(tostring) | join("\n")), "\u0000"
|
|
60
|
+
' 2>/dev/null; then
|
|
61
|
+
parent_status=0
|
|
62
|
+
else
|
|
63
|
+
parent_status=$?
|
|
64
|
+
fi
|
|
65
|
+
# An extra separator terminates partial last fields from a failed producer.
|
|
66
|
+
printf '\0%s\0' "$parent_status"
|
|
67
|
+
)
|
|
68
|
+
parent_status="${parent_fields[${#parent_fields[@]}-1]}"
|
|
69
|
+
if [ "$parent_status" -ne 0 ]; then
|
|
70
|
+
exit "$parent_status"
|
|
71
|
+
fi
|
|
72
|
+
if [ "${#parent_fields[@]}" -ne 8 ] || [ -n "${parent_fields[6]}" ]; then
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
for parent_index in 0 1 2 3 4 5; do
|
|
76
|
+
while [[ "${parent_fields[$parent_index]}" == *$'\n' ]]; do
|
|
77
|
+
parent_fields[$parent_index]="${parent_fields[$parent_index]%$'\n'}"
|
|
78
|
+
done
|
|
79
|
+
done
|
|
80
|
+
session_id="${parent_fields[0]}"
|
|
34
81
|
if [ -z "$session_id" ]; then
|
|
35
82
|
printf '%s\n' "$input"
|
|
36
83
|
exit 0
|
|
37
84
|
fi
|
|
38
85
|
|
|
39
|
-
# ── Phase 2 (#1196): background_tasks / session_crons
|
|
40
|
-
#
|
|
41
|
-
bg_tasks_json
|
|
42
|
-
session_crons_json
|
|
43
|
-
bg_tasks_count
|
|
44
|
-
session_crons_count
|
|
45
|
-
|
|
46
|
-
# 미완료 백그라운드 태스크: running/pending/in_progress 상태 카운트
|
|
47
|
-
bg_dangling_count=$(printf '%s\n' "$bg_tasks_json" | jq '[.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")] | length' 2>/dev/null || echo 0)
|
|
86
|
+
# ── Phase 2 (#1196): background_tasks / session_crons (CC v2.1.145+) ──
|
|
87
|
+
# Missing/null/false fields retain [], and each count retains its own fallback.
|
|
88
|
+
bg_tasks_json="${parent_fields[1]}"
|
|
89
|
+
session_crons_json="${parent_fields[2]}"
|
|
90
|
+
bg_tasks_count="${parent_fields[3]}"
|
|
91
|
+
session_crons_count="${parent_fields[4]}"
|
|
92
|
+
bg_dangling_count="${parent_fields[5]}"
|
|
48
93
|
|
|
49
94
|
# ── 경로 결정 (환경변수 override 지원) ──
|
|
50
95
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -239,7 +284,8 @@ WORKER_EOF
|
|
|
239
284
|
chmod +x "$WORKER_SCRIPT"
|
|
240
285
|
|
|
241
286
|
# ── 백그라운드로 실행 (Stop hook 시간 예산 <3s 준수) ──
|
|
242
|
-
#
|
|
287
|
+
# 전체 subshell의 표준 입출력을 분리하여 부모의 파이프를 붙들지 않는다.
|
|
288
|
+
# 분석은 비동기이며 완료 후 임시 파일을 제거한다. setsid/session 생존 보장은 없다.
|
|
243
289
|
WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
|
|
244
290
|
|
|
245
291
|
(
|
|
@@ -250,10 +296,9 @@ WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
|
|
|
250
296
|
"$SCRIPT_DIR" \
|
|
251
297
|
"$bg_tasks_json" \
|
|
252
298
|
"$session_crons_json" \
|
|
253
|
-
"$bg_dangling_count"
|
|
254
|
-
2>>"$WORKER_ERR_LOG"
|
|
299
|
+
"$bg_dangling_count"
|
|
255
300
|
rm -f "$WORKER_SCRIPT"
|
|
256
|
-
) &
|
|
301
|
+
) </dev/null >/dev/null 2>>"$WORKER_ERR_LOG" &
|
|
257
302
|
disown $! 2>/dev/null || true
|
|
258
303
|
|
|
259
304
|
# ── 즉시 stdin pass-through 후 exit 0 (Stop hook 체인 유지) ──
|
|
@@ -536,8 +536,92 @@ input=$(cat)
|
|
|
536
536
|
# Swallow it instead: exit 0 with no stdout, exactly as if the hook had not
|
|
537
537
|
# run. "jq -e" exits non-zero both for a parse error and for a false/null
|
|
538
538
|
# result, so one guard covers every non-object shape.
|
|
539
|
-
|
|
539
|
+
# Common string fields can share one jq producer. Unusual types and embedded
|
|
540
|
+
# NUL retain the original extraction path: command substitution has platform-
|
|
541
|
+
# specific NUL handling, and independent jq failures are part of the contract.
|
|
542
|
+
extraction_mode=$(printf '%s' "$input" | jq -er '
|
|
543
|
+
if type != "object" then false
|
|
544
|
+
elif any(.. | strings; contains("\u0000")) then "legacy"
|
|
545
|
+
elif ((.tool_name // "unknown" | type) != "string") then "legacy"
|
|
546
|
+
elif ((.tool_input != null) and ((.tool_input | type) != "object")) then "legacy"
|
|
547
|
+
elif ((.tool_output != null) and ((.tool_output | type) != "object")) then "legacy"
|
|
548
|
+
elif ((.tool_input.file_path // "" | type) != "string")
|
|
549
|
+
or ((.tool_input.command // "" | type) != "string")
|
|
550
|
+
or ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "" | type) != "string")
|
|
551
|
+
then "legacy" else "batched" end
|
|
552
|
+
' 2>/dev/null) || exit 0
|
|
553
|
+
|
|
554
|
+
# NUL-delimited fields are safe only after the guard above excludes embedded
|
|
555
|
+
# NUL. A final producer-status record makes jq failures observable; process
|
|
556
|
+
# substitution alone would discard its exit status. jq -j preserves -r scalar
|
|
557
|
+
# rendering without an added newline; trim actual trailing LF just as $(...) did.
|
|
558
|
+
_read_stuck_fields() {
|
|
559
|
+
local filter="$1" field status
|
|
560
|
+
STUCK_FIELDS=()
|
|
561
|
+
while IFS= read -r -d '' field; do
|
|
562
|
+
while [[ "$field" == *$'\n' ]]; do field="${field%$'\n'}"; done
|
|
563
|
+
STUCK_FIELDS[${#STUCK_FIELDS[@]}]="$field"
|
|
564
|
+
done < <(
|
|
565
|
+
if printf '%s' "$input" | jq -j "$filter"; then
|
|
566
|
+
printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
|
|
567
|
+
else
|
|
568
|
+
printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
|
|
569
|
+
fi
|
|
570
|
+
)
|
|
571
|
+
status="${STUCK_FIELDS[${#STUCK_FIELDS[@]}-1]:-}"
|
|
572
|
+
case "$status" in
|
|
573
|
+
__OMCUSTOM_JQ_STATUS__*) status="${status#__OMCUSTOM_JQ_STATUS__}" ;;
|
|
574
|
+
*) return 1 ;;
|
|
575
|
+
esac
|
|
576
|
+
unset "STUCK_FIELDS[$((${#STUCK_FIELDS[@]} - 1))]"
|
|
577
|
+
return "$status"
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
TARGET_KEY_CAP=1000
|
|
581
|
+
|
|
582
|
+
truncate_key() {
|
|
583
|
+
local v="$1"
|
|
584
|
+
if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
|
|
585
|
+
printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
|
|
586
|
+
else
|
|
587
|
+
printf '%s' "$v"
|
|
588
|
+
fi
|
|
589
|
+
}
|
|
540
590
|
|
|
591
|
+
# Batched extraction changes no classifier or detector inputs. Preview retains
|
|
592
|
+
# its original byte-cap pipeline, including broken-pipe tolerance.
|
|
593
|
+
if [ "$extraction_mode" = "batched" ]; then
|
|
594
|
+
_read_stuck_fields '(.tool_name // "unknown"), "\u0000",
|
|
595
|
+
(if (.tool_name // "unknown") == "Bash"
|
|
596
|
+
then (.tool_input.command // .tool_input.file_path // "")
|
|
597
|
+
else (.tool_input.file_path // "") end), "\u0000",
|
|
598
|
+
(.tool_output.is_error // false), "\u0000"'
|
|
599
|
+
[ "${#STUCK_FIELDS[@]}" -eq 3 ] || exit 1
|
|
600
|
+
tool_name="${STUCK_FIELDS[0]}"
|
|
601
|
+
if [ "$tool_name" = "Bash" ]; then
|
|
602
|
+
file_path=""
|
|
603
|
+
target_key=$(truncate_key "${STUCK_FIELDS[1]}")
|
|
604
|
+
else
|
|
605
|
+
file_path_full="${STUCK_FIELDS[1]}"
|
|
606
|
+
file_path=$(printf '%s' "$file_path_full" | head -c "$TARGET_KEY_CAP" || true)
|
|
607
|
+
target_key=$(truncate_key "$file_path_full")
|
|
608
|
+
fi
|
|
609
|
+
is_error="${STUCK_FIELDS[2]}"
|
|
610
|
+
output_preview=$(printf '%s' "$input" \
|
|
611
|
+
| jq -r '(.tool_output.output // "" | tostring)[0:200]' \
|
|
612
|
+
| head -c 200 || true)
|
|
613
|
+
_read_stuck_fields '(.tool_input.command // ""), "\u0000",
|
|
614
|
+
(if (.tool_name == "Edit" or .tool_name == "Write")
|
|
615
|
+
then ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "")
|
|
616
|
+
| gsub("[[:space:]]+"; " ") | ltrimstr(" ") | rtrimstr(" ")
|
|
617
|
+
| (length as $l | if $l <= 120 then "\($l)#\(.)"
|
|
618
|
+
else ((explode | add // 0) as $sum
|
|
619
|
+
| "\($l)#\($sum)#\(.[0:80])#\(.[-40:])") end))
|
|
620
|
+
else "" end), "\u0000"'
|
|
621
|
+
[ "${#STUCK_FIELDS[@]}" -eq 2 ] || exit 1
|
|
622
|
+
raw_command="${STUCK_FIELDS[0]}"
|
|
623
|
+
edit_hash="${STUCK_FIELDS[1]}"
|
|
624
|
+
else
|
|
541
625
|
# Extract tool info
|
|
542
626
|
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
543
627
|
# Bash commands are NOT files. Feeding tool_input.command into file_path made
|
|
@@ -552,16 +636,6 @@ tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
|
552
636
|
# remove that collision class, so truncate_key() ALSO appends the ORIGINAL
|
|
553
637
|
# length: two values that share the capped prefix but differ in total length
|
|
554
638
|
# no longer produce the same key (M-3).
|
|
555
|
-
TARGET_KEY_CAP=1000
|
|
556
|
-
|
|
557
|
-
truncate_key() {
|
|
558
|
-
local v="$1"
|
|
559
|
-
if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
|
|
560
|
-
printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
|
|
561
|
-
else
|
|
562
|
-
printf '%s' "$v"
|
|
563
|
-
fi
|
|
564
|
-
}
|
|
565
639
|
|
|
566
640
|
if [ "$tool_name" = "Bash" ]; then
|
|
567
641
|
file_path=""
|
|
@@ -627,13 +701,35 @@ if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then
|
|
|
627
701
|
end)')
|
|
628
702
|
fi
|
|
629
703
|
|
|
704
|
+
fi
|
|
705
|
+
|
|
630
706
|
# History entries are written by jq, so their "path" values are JSON-ENCODED.
|
|
631
707
|
# Matching them requires the SAME encoding, not the raw text: a raw value
|
|
632
708
|
# containing a quote (common in Bash commands) can never match the encoded
|
|
633
709
|
# form. The previous BRE escaping was doubly broken — in POSIX BRE "( ) + ? { |"
|
|
634
710
|
# are literals, so escaping them turned them INTO operators and silently
|
|
635
711
|
# stopped matching. Encode once here and match with grep -F (fixed string).
|
|
636
|
-
|
|
712
|
+
# JSON-encoded literals contain no raw NUL or LF, including legacy fields.
|
|
713
|
+
encoded_matches=()
|
|
714
|
+
while IFS= read -r -d '' encoded_match; do
|
|
715
|
+
encoded_matches[${#encoded_matches[@]}]="$encoded_match"
|
|
716
|
+
done < <(
|
|
717
|
+
if jq -nj --arg path "$target_key" --arg edit "$edit_hash" \
|
|
718
|
+
'($path | tojson), "\u0000", ($edit | tojson), "\u0000"'; then
|
|
719
|
+
printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
|
|
720
|
+
else
|
|
721
|
+
printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
|
|
722
|
+
fi
|
|
723
|
+
)
|
|
724
|
+
encoded_status="${encoded_matches[${#encoded_matches[@]}-1]:-}"
|
|
725
|
+
case "$encoded_status" in
|
|
726
|
+
__OMCUSTOM_JQ_STATUS__0) ;;
|
|
727
|
+
__OMCUSTOM_JQ_STATUS__*) exit "${encoded_status#__OMCUSTOM_JQ_STATUS__}" ;;
|
|
728
|
+
*) exit 1 ;;
|
|
729
|
+
esac
|
|
730
|
+
[ "${#encoded_matches[@]}" -eq 3 ] || exit 1
|
|
731
|
+
target_key_json="${encoded_matches[0]}"
|
|
732
|
+
edit_hash_json="${encoded_matches[1]}"
|
|
637
733
|
path_match="\"path\":${target_key_json}"
|
|
638
734
|
|
|
639
735
|
# #1649 follow-up: edit_hash now preserves punctuation (see the edit_hash
|
|
@@ -642,7 +738,7 @@ path_match="\"path\":${target_key_json}"
|
|
|
642
738
|
# (--arg ehash "$edit_hash" -> jq serializes it), so Check 1 / Check 3 below
|
|
643
739
|
# must match it with the SAME JSON-encoded literal, not the raw string — same
|
|
644
740
|
# fix as path_match above, applied to edit_hash.
|
|
645
|
-
edit_hash_json
|
|
741
|
+
# edit_hash_json was encoded together with target_key_json above.
|
|
646
742
|
edit_hash_match="\"edit_hash\":${edit_hash_json}"
|
|
647
743
|
|
|
648
744
|
is_readonly="false"
|
|
@@ -651,7 +747,7 @@ if [ "$tool_name" = "Bash" ]; then
|
|
|
651
747
|
fi
|
|
652
748
|
|
|
653
749
|
# Session-scoped history
|
|
654
|
-
HISTORY_FILE="
|
|
750
|
+
HISTORY_FILE="${OMCUSTOM_STUCK_HISTORY_FILE:-/tmp/.claude-tool-history-${PPID}}"
|
|
655
751
|
|
|
656
752
|
# Create entry
|
|
657
753
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
@@ -86,7 +86,7 @@ zsh는 미인용 `$var`를 단어분할하지 않습니다(#1683 #5) — 분할
|
|
|
86
86
|
|
|
87
87
|
도구 이름 ≠ 그 프로그램(#1590): 사용 전 `type <tool>`로 실체를 확인합니다 — 이 저장소 Bash의 `grep`은 `.gitignore`를 존중하는 셸 함수이므로 저장소 전수조사는 `git grep`을 표준으로 합니다.
|
|
88
88
|
|
|
89
|
-
Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다.
|
|
89
|
+
Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다. `cd X && ( … )`도 `cd`가 서브셸 밖에 놓인 지속 `cd`이므로 금지합니다(#1816) — 괄호는 `cd` 앞에서 열어야 합니다.
|
|
90
90
|
|
|
91
91
|
<!-- DETAIL: tool name vs program
|
|
92
92
|
> **도구 이름 ≠ 그 프로그램 (#1590)**: 도구를 쓰기 전에 `type <tool>`로 실체를 확인한다. Bash 도구의 `grep`은 `~/.claude/shell-snapshots/snapshot-zsh-*.sh`의 **셸 함수**이며 `ugrep --ignore-files`에 위임한다. 그 결과 `.gitignore`의 리터럴 `CLAUDE.md` 패턴을 존중해, **force-tracked 파일을 재귀 탐색에서 조용히 누락**한다(에러 없이 exit 0). 명시 경로를 준 grep은 정상 동작하므로 **traversal만 영향**을 받는다. 실측(2026-08-15): 동일 패턴·동일 대상에 대해 셸 함수 36 / `command grep` 43 / `git grep` 38 히트 — 셸 함수만 `CLAUDE.md`를 0 히트로 놓쳤다. 진단 함정: `git check-ignore`는 **index-aware**라 tracked 파일에 "not ignored"(exit 1)를 반환한다 — 원인을 보려면 `git check-ignore --no-index`를 써야 한다. 처방: 저장소 전수 조사는 `git grep`을 표준으로 한다(R017 Count Sync cross-ref). Origin: #1590.
|
|
@@ -47,8 +47,9 @@ Tier를 섞으면(예: frontmatter에 Tier-3 전용 값을 쓰거나 그 반대)
|
|
|
47
47
|
| Full ID | Use Case |
|
|
48
48
|
|---------|----------|
|
|
49
49
|
| `claude-haiku-4-5` | Fast, cheap tasks |
|
|
50
|
+
| `claude-haiku-5-5` | Default Haiku model on the Anthropic API (v2.1.293+); 1M context |
|
|
50
51
|
| `claude-sonnet-5` | Native 1M context; previous-generation Sonnet (v2.1.197+; on the Anthropic API, `claude-sonnet-5-5` is now the default Sonnet model, v2.1.284+) |
|
|
51
|
-
| `claude-sonnet-5-5` | Now the default Sonnet model on the Anthropic API (v2.1.284+); 1M context, $2/$10 per Mtok, $0.
|
|
52
|
+
| `claude-sonnet-5-5` | Now the default Sonnet model on the Anthropic API (v2.1.284+); 1M context, $2/$10 per Mtok, $0.10/Mtok cache reads in CC cost figures (v2.1.296; was $0.20) |
|
|
52
53
|
| `claude-opus-4-6` | Opus, previous generation |
|
|
53
54
|
| `claude-opus-4-8` | Opus, previous generation; supports xhigh effort |
|
|
54
55
|
| `claude-opus-5` | Opus, previous generation (GA); native 1M context, fast mode at $10/$50 per Mtok |
|
|
@@ -164,7 +165,7 @@ Settings-level 가용성/과부하 failover — per-agent `model:` frontmatter
|
|
|
164
165
|
### Safe Mode & Bundled Skill Control (CC v2.1.169+)
|
|
165
166
|
|
|
166
167
|
<!-- ARCHIVED CC version note (historical):
|
|
167
|
-
> **v2.1.169+**: `--safe-mode` (and `CLAUDE_CODE_SAFE_MODE`) starts Claude Code with ALL customizations disabled (CLAUDE.md, plugins, skills, hooks, MCP servers) — use it to isolate whether a project customization (agent/skill/hook) causes a regression. The `disableBundledSkills` setting (and `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS` env) hides bundled skills, workflows, and built-in slash commands from the model — useful when bundled skills conflict with or duplicate project skills (R006 skill-surface management). Note: `disableBundledSkills` hides skills from the model but is a CC platform setting, distinct from the
|
|
168
|
+
> **v2.1.169+**: `--safe-mode` (and `CLAUDE_CODE_SAFE_MODE`) starts Claude Code with ALL customizations disabled (CLAUDE.md, plugins, skills, hooks, MCP servers) — use it to isolate whether a project customization (agent/skill/hook) causes a regression. The `disableBundledSkills` setting (and `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS` env) hides bundled skills, workflows, and built-in slash commands from the model — useful when bundled skills conflict with or duplicate project skills (R006 skill-surface management). Note: `disableBundledSkills` hides skills from the model but is a CC platform setting, distinct from the `skills:` frontmatter field (which preloads the listed skills into the subagent but is not a runtime allowlist).
|
|
168
169
|
-->
|
|
169
170
|
|
|
170
171
|
<!-- ARCHIVED CC version notes (historical):
|
|
@@ -193,9 +194,9 @@ Key optional fields: `memory`, `effort`, `skills`, `soul`, `isolation`, `backgro
|
|
|
193
194
|
### Note on `skills:` field
|
|
194
195
|
|
|
195
196
|
<!-- DETAIL: skills: field full rationale
|
|
196
|
-
The `skills:` frontmatter field is
|
|
197
|
+
The `skills:` frontmatter field is read by oh-my-customcode tooling (graph-builder, mgr-sauron) for documentation and validation, and Claude Code itself PRELOADS the listed skills into the subagent (context cost; v2.1.295: "Changed subagents to preload at most 32 skills from the `skills` field, each once; a subagent with the Skill tool can still invoke the rest" — preloading predates 2.1.295: the 2.0.43 CHANGELOG entry is the primary source — "Added skills frontmatter field to declare skills to auto-load for subagents" — and the 2.1.265 entry also refers to "preloaded skills"). It is **NOT a runtime allowlist** — Claude Code does not restrict skill access based on this field, so do not rely on it for access control.
|
|
197
198
|
-->
|
|
198
|
-
`skills:` frontmatter는 **
|
|
199
|
+
`skills:` frontmatter는 **runtime allowlist가 아닙니다** — CC는 이 필드로 스킬 접근을 제한하지 않으므로, access control로 의존하지 마세요(나열 스킬은 사전 로드됩니다, 최대 32개).
|
|
199
200
|
|
|
200
201
|
<!-- DETAIL: skills: field issue reference
|
|
201
202
|
Reference: research findings on issue #1055 (closed not-planned).
|
|
@@ -259,7 +259,7 @@ MEMORY.md supports an optional `## User Model` section (DISTINCT from `## Behavi
|
|
|
259
259
|
- Focus: AI agent orchestration, CLI tooling
|
|
260
260
|
|
|
261
261
|
### Override Decisions [confidence: low]
|
|
262
|
-
- Overrode /scout SKIP verdict → INTEGRATE for RTK (#756) (과거 결정 예시입니다. RTK 프로젝트 통합은
|
|
262
|
+
- Overrode /scout SKIP verdict → INTEGRATE for RTK (#756) (과거 결정 예시입니다. RTK 프로젝트 통합은 v1.1.107에서 퇴역하며 현재 실행 추천으로 해석하지 마십시오.)
|
|
263
263
|
|
|
264
264
|
### Categories
|
|
265
265
|
|
|
@@ -101,6 +101,15 @@ Fable 5 실행 에이전트/스킬은 지시(instruction) 장문화가 오히려
|
|
|
101
101
|
- [ ] Tier 1에서 잡을 수 있는 문제를 다루는가? (상위 tier 대신 시프트 권고)
|
|
102
102
|
- [ ] Ladder 순서를 문서화했는가? (어떤 검사를 먼저 실행하는지)
|
|
103
103
|
|
|
104
|
+
### 문구 작성·정정
|
|
105
|
+
|
|
106
|
+
새 주석/JSDoc/문서의 문구 작성·정정 시 확인합니다(#1816 H1):
|
|
107
|
+
|
|
108
|
+
- [ ] 새 문구의 사실 절마다 코드 앵커 또는 실제 관측 출력을 대응표에 기록했는가?
|
|
109
|
+
- [ ] 독립 문구 검토로 사실 절과 근거의 대응을 확인했는가?
|
|
110
|
+
|
|
111
|
+
lint·typecheck·테스트 통과만으로 문구의 사실 정확성을 확정하지 않습니다. pipeline auto-dev의 문구 검토 요구와 함께 확인합니다.
|
|
112
|
+
|
|
104
113
|
## Safety-Signal Rule Authoring — Carve-Out Pre-Check (shift-left)
|
|
105
114
|
|
|
106
115
|
<!-- DETAIL: Origin note
|