oh-my-customcode 1.1.63 → 1.1.64
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/dist/cli/index.js +888 -11488
- package/dist/index.js +52 -265
- package/package.json +1 -1
- package/templates/.claude/rules/MAY-optimization.md +4 -0
- package/templates/.claude/rules/MUST-agent-design.md +8 -0
- package/templates/.claude/rules/MUST-agent-teams.md +4 -0
- package/templates/.claude/rules/MUST-completion-verification.md +4 -0
- package/templates/.claude/rules/MUST-enforcement-policy.md +4 -0
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +4 -0
- package/templates/.claude/rules/MUST-parallel-execution.md +2 -0
- package/templates/.claude/rules/MUST-permissions.md +6 -0
- package/templates/.claude/rules/MUST-safety.md +4 -0
- package/templates/.claude/rules/MUST-sync-verification.md +2 -0
- package/templates/.claude/rules/SHOULD-ecomode.md +4 -0
- package/templates/.claude/rules/SHOULD-hud-statusline.md +2 -0
- package/templates/.claude/rules/SHOULD-verification-ladder.md +4 -0
- package/templates/manifest.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,47 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
enumerable: true,
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
1
|
+
var __esm = (fn, res, err) => () => {
|
|
2
|
+
if (fn)
|
|
3
|
+
try {
|
|
4
|
+
res = fn(fn = 0);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
err = [e];
|
|
7
|
+
}
|
|
8
|
+
if (err)
|
|
9
|
+
throw err[0];
|
|
10
|
+
return res;
|
|
15
11
|
};
|
|
16
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
12
|
|
|
19
13
|
// src/utils/fs.ts
|
|
20
|
-
var exports_fs = {};
|
|
21
|
-
__export(exports_fs, {
|
|
22
|
-
calculateChecksum: () => calculateChecksum,
|
|
23
|
-
copyDirectory: () => copyDirectory,
|
|
24
|
-
copyFile: () => copyFile,
|
|
25
|
-
createTempDir: () => createTempDir,
|
|
26
|
-
ensureDirectory: () => ensureDirectory,
|
|
27
|
-
fileExists: () => fileExists,
|
|
28
|
-
filesAreIdentical: () => filesAreIdentical,
|
|
29
|
-
getFileStats: () => getFileStats,
|
|
30
|
-
getPackageRoot: () => getPackageRoot,
|
|
31
|
-
getRelativePath: () => getRelativePath,
|
|
32
|
-
isAbsolutePath: () => isAbsolutePath,
|
|
33
|
-
listFiles: () => listFiles,
|
|
34
|
-
move: () => move,
|
|
35
|
-
normalizePath: () => normalizePath,
|
|
36
|
-
readJsonFile: () => readJsonFile,
|
|
37
|
-
readTextFile: () => readTextFile,
|
|
38
|
-
remove: () => remove,
|
|
39
|
-
resolvePath: () => resolvePath,
|
|
40
|
-
resolveTemplatePath: () => resolveTemplatePath,
|
|
41
|
-
validatePreserveFilePath: () => validatePreserveFilePath,
|
|
42
|
-
writeJsonFile: () => writeJsonFile,
|
|
43
|
-
writeTextFile: () => writeTextFile
|
|
44
|
-
});
|
|
45
14
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
46
15
|
import { fileURLToPath } from "node:url";
|
|
47
16
|
function validatePreserveFilePath(filePath, projectRoot) {
|
|
@@ -191,15 +160,6 @@ async function writeTextFile(path, content) {
|
|
|
191
160
|
await ensureDirectory(dirname(path));
|
|
192
161
|
await fs.writeFile(path, content, "utf-8");
|
|
193
162
|
}
|
|
194
|
-
async function remove(path) {
|
|
195
|
-
const fs = await import("node:fs/promises");
|
|
196
|
-
const stat = await fs.stat(path);
|
|
197
|
-
if (stat.isDirectory()) {
|
|
198
|
-
await fs.rm(path, { recursive: true, force: true });
|
|
199
|
-
} else {
|
|
200
|
-
await fs.unlink(path);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
163
|
function getPackageRoot() {
|
|
204
164
|
const currentFile = fileURLToPath(import.meta.url);
|
|
205
165
|
const currentDir = dirname(currentFile);
|
|
@@ -209,45 +169,11 @@ function resolveTemplatePath(relativePath) {
|
|
|
209
169
|
const packageRoot = getPackageRoot();
|
|
210
170
|
return join(packageRoot, "templates", relativePath);
|
|
211
171
|
}
|
|
212
|
-
async function listFiles(dir, options = {}) {
|
|
213
|
-
const fs = await import("node:fs/promises");
|
|
214
|
-
const path = await import("node:path");
|
|
215
|
-
const files = [];
|
|
216
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
217
|
-
for (const entry of entries) {
|
|
218
|
-
const fullPath = path.join(dir, entry.name);
|
|
219
|
-
if (entry.isDirectory() && options.recursive) {
|
|
220
|
-
const subFiles = await listFiles(fullPath, options);
|
|
221
|
-
files.push(...subFiles);
|
|
222
|
-
} else if (entry.isFile()) {
|
|
223
|
-
if (!options.pattern || matchesPattern(entry.name, options.pattern)) {
|
|
224
|
-
files.push(fullPath);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return files;
|
|
229
|
-
}
|
|
230
|
-
async function getFileStats(path) {
|
|
231
|
-
const fs = await import("node:fs/promises");
|
|
232
|
-
const stats = await fs.stat(path);
|
|
233
|
-
return {
|
|
234
|
-
size: stats.size,
|
|
235
|
-
created: stats.birthtime,
|
|
236
|
-
modified: stats.mtime,
|
|
237
|
-
isDirectory: stats.isDirectory(),
|
|
238
|
-
isFile: stats.isFile()
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
172
|
async function copyFile(src, dest) {
|
|
242
173
|
const fs = await import("node:fs/promises");
|
|
243
174
|
await ensureDirectory(dirname(dest));
|
|
244
175
|
await fs.copyFile(src, dest);
|
|
245
176
|
}
|
|
246
|
-
async function move(src, dest) {
|
|
247
|
-
const fs = await import("node:fs/promises");
|
|
248
|
-
await ensureDirectory(dirname(dest));
|
|
249
|
-
await fs.rename(src, dest);
|
|
250
|
-
}
|
|
251
177
|
async function createTempDir(prefix = "omcustom-") {
|
|
252
178
|
const fs = await import("node:fs/promises");
|
|
253
179
|
const os = await import("node:os");
|
|
@@ -257,58 +183,17 @@ async function createTempDir(prefix = "omcustom-") {
|
|
|
257
183
|
await fs.mkdir(tempDir, { recursive: true });
|
|
258
184
|
return tempDir;
|
|
259
185
|
}
|
|
260
|
-
async function calculateChecksum(path) {
|
|
261
|
-
const fs = await import("node:fs/promises");
|
|
262
|
-
const crypto = await import("node:crypto");
|
|
263
|
-
const content = await fs.readFile(path);
|
|
264
|
-
const hash = crypto.createHash("md5");
|
|
265
|
-
hash.update(content);
|
|
266
|
-
return hash.digest("hex");
|
|
267
|
-
}
|
|
268
|
-
async function filesAreIdentical(path1, path2) {
|
|
269
|
-
const [checksum1, checksum2] = await Promise.all([
|
|
270
|
-
calculateChecksum(path1),
|
|
271
|
-
calculateChecksum(path2)
|
|
272
|
-
]);
|
|
273
|
-
return checksum1 === checksum2;
|
|
274
|
-
}
|
|
275
186
|
function matchesPattern(filename, pattern) {
|
|
276
187
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
277
188
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
278
189
|
return regex.test(filename);
|
|
279
190
|
}
|
|
280
|
-
function getRelativePath(basePath, fullPath) {
|
|
281
|
-
return relative(basePath, fullPath);
|
|
282
|
-
}
|
|
283
|
-
function normalizePath(inputPath) {
|
|
284
|
-
return inputPath.replace(/\\/g, "/");
|
|
285
|
-
}
|
|
286
|
-
function isAbsolutePath(inputPath) {
|
|
287
|
-
return isAbsolute(inputPath);
|
|
288
|
-
}
|
|
289
|
-
function resolvePath(...paths) {
|
|
290
|
-
return resolve(...paths);
|
|
291
|
-
}
|
|
292
191
|
var init_fs = () => {};
|
|
293
192
|
|
|
294
193
|
// src/core/registry.ts
|
|
295
|
-
var exports_registry = {};
|
|
296
|
-
__export(exports_registry, {
|
|
297
|
-
_setRegistryDirForTesting: () => _setRegistryDirForTesting,
|
|
298
|
-
cleanRegistry: () => cleanRegistry,
|
|
299
|
-
isTempPath: () => isTempPath,
|
|
300
|
-
migrateFromLockfiles: () => migrateFromLockfiles,
|
|
301
|
-
readRegistry: () => readRegistry,
|
|
302
|
-
registerProject: () => registerProject,
|
|
303
|
-
registryToList: () => registryToList,
|
|
304
|
-
unregisterProject: () => unregisterProject
|
|
305
|
-
});
|
|
306
194
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
307
195
|
import { homedir, tmpdir } from "node:os";
|
|
308
196
|
import { basename as basename3, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
|
|
309
|
-
function _setRegistryDirForTesting(dir2) {
|
|
310
|
-
_registryDirOverride = dir2;
|
|
311
|
-
}
|
|
312
197
|
function isTempPath(projectPath) {
|
|
313
198
|
const normalized = resolve2(projectPath);
|
|
314
199
|
const candidates = new Set;
|
|
@@ -353,13 +238,10 @@ async function readRegistryRaw() {
|
|
|
353
238
|
}
|
|
354
239
|
}
|
|
355
240
|
async function writeRegistry(registry) {
|
|
356
|
-
const
|
|
357
|
-
await mkdir(
|
|
241
|
+
const dir = registryDir();
|
|
242
|
+
await mkdir(dir, { recursive: true });
|
|
358
243
|
await writeFile(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
359
244
|
}
|
|
360
|
-
async function readRegistry() {
|
|
361
|
-
return readRegistryRaw();
|
|
362
|
-
}
|
|
363
245
|
async function registerProject(projectPath, version) {
|
|
364
246
|
const normalizedPath = resolve2(projectPath);
|
|
365
247
|
if (!process.env.OMCUSTOM_REGISTRY_DIR && _registryDirOverride === undefined) {
|
|
@@ -376,101 +258,6 @@ async function registerProject(projectPath, version) {
|
|
|
376
258
|
};
|
|
377
259
|
await writeRegistry(registry);
|
|
378
260
|
}
|
|
379
|
-
async function unregisterProject(projectPath) {
|
|
380
|
-
const normalizedPath = resolve2(projectPath);
|
|
381
|
-
const registry = await readRegistryRaw();
|
|
382
|
-
if (!(normalizedPath in registry.projects))
|
|
383
|
-
return;
|
|
384
|
-
delete registry.projects[normalizedPath];
|
|
385
|
-
await writeRegistry(registry);
|
|
386
|
-
}
|
|
387
|
-
async function cleanRegistry() {
|
|
388
|
-
const { access: fsAccess } = await import("node:fs/promises");
|
|
389
|
-
const registry = await readRegistryRaw();
|
|
390
|
-
const paths = Object.keys(registry.projects);
|
|
391
|
-
let removed = 0;
|
|
392
|
-
const purgeTempPaths = !process.env.OMCUSTOM_REGISTRY_DIR && _registryDirOverride === undefined;
|
|
393
|
-
for (const projectPath of paths) {
|
|
394
|
-
if (purgeTempPaths && isTempPath(projectPath)) {
|
|
395
|
-
delete registry.projects[projectPath];
|
|
396
|
-
removed++;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
try {
|
|
400
|
-
await fsAccess(projectPath);
|
|
401
|
-
} catch {
|
|
402
|
-
delete registry.projects[projectPath];
|
|
403
|
-
removed++;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (removed > 0) {
|
|
407
|
-
await writeRegistry(registry);
|
|
408
|
-
}
|
|
409
|
-
return removed;
|
|
410
|
-
}
|
|
411
|
-
async function parseLockFile(lockPath) {
|
|
412
|
-
try {
|
|
413
|
-
const content = await readFile(lockPath, "utf-8");
|
|
414
|
-
const lock = JSON.parse(content);
|
|
415
|
-
const version = typeof lock.version === "string" ? lock.version : typeof lock.templateVersion === "string" ? lock.templateVersion : "0.0.0";
|
|
416
|
-
const now = new Date().toISOString();
|
|
417
|
-
return {
|
|
418
|
-
version,
|
|
419
|
-
installedAt: typeof lock.installedAt === "string" ? lock.installedAt : now,
|
|
420
|
-
updatedAt: typeof lock.updatedAt === "string" ? lock.updatedAt : now
|
|
421
|
-
};
|
|
422
|
-
} catch {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
async function migrateFromLockfiles(searchDirs) {
|
|
427
|
-
const { readdir: readdir3 } = await import("node:fs/promises");
|
|
428
|
-
const MAX_DEPTH = 3;
|
|
429
|
-
const discovered = new Map;
|
|
430
|
-
async function scan(dir2, depth) {
|
|
431
|
-
if (depth > MAX_DEPTH || discovered.has(dir2))
|
|
432
|
-
return;
|
|
433
|
-
let entries;
|
|
434
|
-
try {
|
|
435
|
-
entries = await readdir3(dir2, { withFileTypes: true });
|
|
436
|
-
} catch {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
const entry = await parseLockFile(join6(dir2, ".omcustom.lock.json"));
|
|
440
|
-
if (entry !== null) {
|
|
441
|
-
discovered.set(resolve2(dir2), entry);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (depth < MAX_DEPTH) {
|
|
445
|
-
const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SCAN_SKIP_DIRS.has(e.name));
|
|
446
|
-
await Promise.all(subdirs.map((sub) => scan(join6(dir2, sub.name), depth + 1)));
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
await Promise.all(searchDirs.map((dir2) => scan(dir2, 0).catch(() => {})));
|
|
450
|
-
if (discovered.size === 0)
|
|
451
|
-
return 0;
|
|
452
|
-
const registry = await readRegistryRaw();
|
|
453
|
-
let imported = 0;
|
|
454
|
-
for (const [projectPath, entry] of discovered) {
|
|
455
|
-
if (!(projectPath in registry.projects)) {
|
|
456
|
-
registry.projects[projectPath] = entry;
|
|
457
|
-
imported++;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (imported > 0) {
|
|
461
|
-
await writeRegistry(registry);
|
|
462
|
-
}
|
|
463
|
-
return imported;
|
|
464
|
-
}
|
|
465
|
-
function registryToList(registry) {
|
|
466
|
-
return Object.entries(registry.projects).map(([path, entry]) => ({
|
|
467
|
-
name: basename3(path),
|
|
468
|
-
path,
|
|
469
|
-
version: entry.version,
|
|
470
|
-
installedAt: entry.installedAt,
|
|
471
|
-
updatedAt: entry.updatedAt
|
|
472
|
-
}));
|
|
473
|
-
}
|
|
474
261
|
var _registryDirOverride, EMPTY_REGISTRY, SCAN_SKIP_DIRS;
|
|
475
262
|
var init_registry = __esm(() => {
|
|
476
263
|
EMPTY_REGISTRY = { projects: {} };
|
|
@@ -1167,8 +954,8 @@ var DEFAULT_CRITICAL_DIRECTORIES = ["agent-memory", "agent-memory-local"];
|
|
|
1167
954
|
var PROTECTED_FRAMEWORK_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1168
955
|
var PROTECTED_RULE_PATTERNS = ["rules/MUST-*.md"];
|
|
1169
956
|
function isProtectedFile(relativePath) {
|
|
1170
|
-
const
|
|
1171
|
-
if (PROTECTED_FRAMEWORK_FILES.includes(
|
|
957
|
+
const basename = relativePath.split("/").pop() ?? "";
|
|
958
|
+
if (PROTECTED_FRAMEWORK_FILES.includes(basename)) {
|
|
1172
959
|
return true;
|
|
1173
960
|
}
|
|
1174
961
|
for (const pattern of PROTECTED_RULE_PATTERNS) {
|
|
@@ -1847,7 +1634,7 @@ var LOCKFILE_COMPONENTS = [
|
|
|
1847
1634
|
];
|
|
1848
1635
|
var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
|
|
1849
1636
|
function computeFileHash(filePath) {
|
|
1850
|
-
return new Promise((
|
|
1637
|
+
return new Promise((resolve, reject) => {
|
|
1851
1638
|
const hash = createHash("sha256");
|
|
1852
1639
|
const stream = createReadStream(filePath);
|
|
1853
1640
|
stream.on("error", (err) => {
|
|
@@ -1857,7 +1644,7 @@ function computeFileHash(filePath) {
|
|
|
1857
1644
|
hash.update(chunk);
|
|
1858
1645
|
});
|
|
1859
1646
|
stream.on("end", () => {
|
|
1860
|
-
|
|
1647
|
+
resolve(hash.digest("hex"));
|
|
1861
1648
|
});
|
|
1862
1649
|
});
|
|
1863
1650
|
}
|
|
@@ -2096,8 +1883,8 @@ async function handleBackup(targetDir, shouldBackup, result) {
|
|
|
2096
1883
|
const rootDir = join5(targetDir, layout.rootDir);
|
|
2097
1884
|
let preservation = null;
|
|
2098
1885
|
if (await fileExists(rootDir)) {
|
|
2099
|
-
|
|
2100
|
-
const tempDir = await
|
|
1886
|
+
await Promise.resolve().then(() => init_fs());
|
|
1887
|
+
const tempDir = await createTempDir("omcustom-preserve-");
|
|
2101
1888
|
preservation = await extractCriticalFiles(rootDir, tempDir);
|
|
2102
1889
|
if (preservation.extractedFiles.length > 0 || preservation.extractedDirs.length > 0) {
|
|
2103
1890
|
info("install.preserved", {
|
|
@@ -2539,7 +2326,7 @@ var package_default = {
|
|
|
2539
2326
|
workspaces: [
|
|
2540
2327
|
"packages/*"
|
|
2541
2328
|
],
|
|
2542
|
-
version: "1.1.
|
|
2329
|
+
version: "1.1.64",
|
|
2543
2330
|
description: "Batteries-included agent harness for Claude Code",
|
|
2544
2331
|
type: "module",
|
|
2545
2332
|
bin: {
|
|
@@ -2635,8 +2422,8 @@ var isString = (obj) => typeof obj === "string";
|
|
|
2635
2422
|
var defer = () => {
|
|
2636
2423
|
let res;
|
|
2637
2424
|
let rej;
|
|
2638
|
-
const promise = new Promise((
|
|
2639
|
-
res =
|
|
2425
|
+
const promise = new Promise((resolve, reject) => {
|
|
2426
|
+
res = resolve;
|
|
2640
2427
|
rej = reject;
|
|
2641
2428
|
});
|
|
2642
2429
|
promise.resolve = res;
|
|
@@ -3287,13 +3074,13 @@ class Translator extends EventEmitter {
|
|
|
3287
3074
|
}
|
|
3288
3075
|
if (keySeparator) {
|
|
3289
3076
|
const resTypeIsArray = Array.isArray(resForObjHndl);
|
|
3290
|
-
const
|
|
3077
|
+
const copy = resTypeIsArray ? [] : {};
|
|
3291
3078
|
const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey;
|
|
3292
3079
|
for (const m in resForObjHndl) {
|
|
3293
3080
|
if (Object.prototype.hasOwnProperty.call(resForObjHndl, m)) {
|
|
3294
3081
|
const deepKey = `${newKeyToUse}${keySeparator}${m}`;
|
|
3295
3082
|
if (hasDefaultValue && !res) {
|
|
3296
|
-
|
|
3083
|
+
copy[m] = this.translate(deepKey, {
|
|
3297
3084
|
...opt,
|
|
3298
3085
|
defaultValue: shouldHandleAsObject(defaultValue) ? defaultValue[m] : undefined,
|
|
3299
3086
|
...{
|
|
@@ -3302,7 +3089,7 @@ class Translator extends EventEmitter {
|
|
|
3302
3089
|
}
|
|
3303
3090
|
});
|
|
3304
3091
|
} else {
|
|
3305
|
-
|
|
3092
|
+
copy[m] = this.translate(deepKey, {
|
|
3306
3093
|
...opt,
|
|
3307
3094
|
...{
|
|
3308
3095
|
joinArrays: false,
|
|
@@ -3310,11 +3097,11 @@ class Translator extends EventEmitter {
|
|
|
3310
3097
|
}
|
|
3311
3098
|
});
|
|
3312
3099
|
}
|
|
3313
|
-
if (
|
|
3314
|
-
|
|
3100
|
+
if (copy[m] === deepKey)
|
|
3101
|
+
copy[m] = resForObjHndl[m];
|
|
3315
3102
|
}
|
|
3316
3103
|
}
|
|
3317
|
-
res =
|
|
3104
|
+
res = copy;
|
|
3318
3105
|
}
|
|
3319
3106
|
} else if (handleAsObjectInI18nFormat && isString(joinArrays) && Array.isArray(res)) {
|
|
3320
3107
|
res = res.join(joinArrays);
|
|
@@ -3954,10 +3741,10 @@ class Interpolator {
|
|
|
3954
3741
|
let value;
|
|
3955
3742
|
let clonedOptions;
|
|
3956
3743
|
const handleHasOptions = (key, inheritedOptions) => {
|
|
3957
|
-
const
|
|
3958
|
-
if (!key.includes(
|
|
3744
|
+
const sep = this.nestingOptionsSeparator;
|
|
3745
|
+
if (!key.includes(sep))
|
|
3959
3746
|
return key;
|
|
3960
|
-
const c = key.split(new RegExp(`${regexEscape(
|
|
3747
|
+
const c = key.split(new RegExp(`${regexEscape(sep)}[ ]*{`));
|
|
3961
3748
|
let optionsString = `{${c[1]}`;
|
|
3962
3749
|
key = c[0];
|
|
3963
3750
|
optionsString = this.interpolate(optionsString, clonedOptions);
|
|
@@ -3975,7 +3762,7 @@ class Interpolator {
|
|
|
3975
3762
|
};
|
|
3976
3763
|
} catch (e) {
|
|
3977
3764
|
this.logger.warn(`failed parsing options string in nesting for key ${key}`, e);
|
|
3978
|
-
return `${key}${
|
|
3765
|
+
return `${key}${sep}${optionsString}`;
|
|
3979
3766
|
}
|
|
3980
3767
|
if (clonedOptions.defaultValue && clonedOptions.defaultValue.includes(this.prefix))
|
|
3981
3768
|
delete clonedOptions.defaultValue;
|
|
@@ -4149,8 +3936,8 @@ class Formatter {
|
|
|
4149
3936
|
...options,
|
|
4150
3937
|
...valOptions
|
|
4151
3938
|
});
|
|
4152
|
-
} catch (
|
|
4153
|
-
this.logger.warn(
|
|
3939
|
+
} catch (error) {
|
|
3940
|
+
this.logger.warn(error);
|
|
4154
3941
|
}
|
|
4155
3942
|
return formatted;
|
|
4156
3943
|
} else {
|
|
@@ -5419,8 +5206,8 @@ function checkAndInstallRtkAfterUpdate() {
|
|
|
5419
5206
|
}
|
|
5420
5207
|
async function updateProjectRegistry(targetDir, newVersion) {
|
|
5421
5208
|
try {
|
|
5422
|
-
|
|
5423
|
-
await
|
|
5209
|
+
await Promise.resolve().then(() => init_registry());
|
|
5210
|
+
await registerProject(targetDir, newVersion);
|
|
5424
5211
|
} catch {}
|
|
5425
5212
|
}
|
|
5426
5213
|
async function regenerateLockfile(targetDir, result) {
|
|
@@ -5527,11 +5314,11 @@ async function checkForUpdates(targetDir) {
|
|
|
5527
5314
|
}
|
|
5528
5315
|
async function applyUpdates(targetDir, updates) {
|
|
5529
5316
|
const fs = await import("node:fs/promises");
|
|
5530
|
-
for (const
|
|
5531
|
-
const fullPath = join7(targetDir,
|
|
5317
|
+
for (const update of updates) {
|
|
5318
|
+
const fullPath = join7(targetDir, update.path);
|
|
5532
5319
|
await ensureDirectory(join7(fullPath, ".."));
|
|
5533
|
-
await fs.writeFile(fullPath,
|
|
5534
|
-
debug("update.file_applied", { path:
|
|
5320
|
+
await fs.writeFile(fullPath, update.content, "utf-8");
|
|
5321
|
+
debug("update.file_applied", { path: update.path });
|
|
5535
5322
|
}
|
|
5536
5323
|
}
|
|
5537
5324
|
async function preserveCustomizations(targetDir, customizations) {
|
|
@@ -5586,8 +5373,8 @@ async function shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile) {
|
|
|
5586
5373
|
}
|
|
5587
5374
|
async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll, lockfile, targetDir) {
|
|
5588
5375
|
if (forceOverwriteAll) {
|
|
5589
|
-
const
|
|
5590
|
-
return { skipPaths: [], warnedPaths
|
|
5376
|
+
const warnedPaths = await findProtectedFilesInDir(srcPath, componentPath);
|
|
5377
|
+
return { skipPaths: [], warnedPaths, updatedPaths: [] };
|
|
5591
5378
|
}
|
|
5592
5379
|
const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
|
|
5593
5380
|
const path = await import("node:path");
|
|
@@ -5614,9 +5401,9 @@ function isEntryProtected(relPath, componentRelativePrefix) {
|
|
|
5614
5401
|
const componentPrefixed = componentRelativePrefix ? `${componentRelativePrefix}/${relPath}` : relPath;
|
|
5615
5402
|
return isProtectedFile(componentPrefixed);
|
|
5616
5403
|
}
|
|
5617
|
-
async function safeReaddir(
|
|
5404
|
+
async function safeReaddir(dir, fs) {
|
|
5618
5405
|
try {
|
|
5619
|
-
return await fs.readdir(
|
|
5406
|
+
return await fs.readdir(dir, { withFileTypes: true });
|
|
5620
5407
|
} catch {
|
|
5621
5408
|
return [];
|
|
5622
5409
|
}
|
|
@@ -5627,11 +5414,11 @@ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
|
|
|
5627
5414
|
const protected_ = [];
|
|
5628
5415
|
const queue = [{ dir: dirPath, relDir: "" }];
|
|
5629
5416
|
while (queue.length > 0) {
|
|
5630
|
-
const { dir
|
|
5631
|
-
const entries = await safeReaddir(
|
|
5417
|
+
const { dir, relDir } = queue.shift();
|
|
5418
|
+
const entries = await safeReaddir(dir, fs);
|
|
5632
5419
|
for (const entry of entries) {
|
|
5633
5420
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
5634
|
-
const fullPath = path.join(
|
|
5421
|
+
const fullPath = path.join(dir, entry.name);
|
|
5635
5422
|
if (entry.isDirectory()) {
|
|
5636
5423
|
queue.push({ dir: fullPath, relDir: relPath });
|
|
5637
5424
|
} else if (entry.isFile() && isEntryProtected(relPath, componentRelativePrefix)) {
|
|
@@ -5810,16 +5597,16 @@ async function applyNamespaceSync(targetDir, component, lockfile) {
|
|
|
5810
5597
|
const synced = [];
|
|
5811
5598
|
const queue = [{ dir: srcPath, relDir: "" }];
|
|
5812
5599
|
while (queue.length > 0) {
|
|
5813
|
-
const { dir
|
|
5600
|
+
const { dir, relDir } = queue.shift();
|
|
5814
5601
|
let entries;
|
|
5815
5602
|
try {
|
|
5816
|
-
entries = await fs.readdir(
|
|
5603
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5817
5604
|
} catch {
|
|
5818
5605
|
continue;
|
|
5819
5606
|
}
|
|
5820
5607
|
for (const entry of entries) {
|
|
5821
5608
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
5822
|
-
const fullSrcPath = join7(
|
|
5609
|
+
const fullSrcPath = join7(dir, entry.name);
|
|
5823
5610
|
if (entry.isDirectory()) {
|
|
5824
5611
|
queue.push({ dir: fullSrcPath, relDir: relPath });
|
|
5825
5612
|
continue;
|
|
@@ -5847,10 +5634,10 @@ async function backupInstallation(targetDir) {
|
|
|
5847
5634
|
await ensureDirectory(backupDir);
|
|
5848
5635
|
const layout = getProviderLayout();
|
|
5849
5636
|
const dirsToBackup = [layout.rootDir, "guides"];
|
|
5850
|
-
for (const
|
|
5851
|
-
const srcPath = join7(targetDir,
|
|
5637
|
+
for (const dir of dirsToBackup) {
|
|
5638
|
+
const srcPath = join7(targetDir, dir);
|
|
5852
5639
|
if (await fileExists(srcPath)) {
|
|
5853
|
-
const destPath = join7(backupDir,
|
|
5640
|
+
const destPath = join7(backupDir, dir);
|
|
5854
5641
|
await copyDirectory(srcPath, destPath, { overwrite: true });
|
|
5855
5642
|
}
|
|
5856
5643
|
}
|
package/package.json
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
|
|
37
37
|
> **v2.1.234+**: macOS/Linux 네이티브 빌드의 내장 `grep`이 pathological pattern에서 메모리 고갈 대신 fail fast하고, `-m N`과 `-A/-C` 옵션을 함께 쓸 때의 context 출력 정확도가 수정되었습니다(v2.1.235에서 추가 보강). 위 「도구 이름 ≠ 그 프로그램」(#1590) 조항과 인접한 함정입니다 — 이 저장소의 Bash 도구 `grep`은 셸 함수로 셰이딩돼 있으므로, 내장 `grep` 자체의 견고성 개선과 셰이딩 문제는 **별개 축**입니다. Darwin(이 저장소 실행 환경) 네이티브 빌드에 해당합니다.
|
|
38
38
|
|
|
39
|
+
> **v2.1.260/265+**: (260) 여러 세션이 같은 프로젝트 디렉토리를 공유할 때 간헐적으로 발생하던 "task output swap refused" 오류가 수정되었습니다 — 위 v2.1.252 Mac tasks-dir 결함과 **같은 오류 문구의 별개 원인**이므로, v2.1.252~259 환경에서는 이 메시지가 명령 자체의 결함이 아니라 동시 세션 경합에서 나왔을 수 있습니다(R020 Read-Before-Characterize). (260) 서브에이전트가 시작한 background 명령의 1시간 제한이 제거되어, 이제 메인 세션과 동일하게 종료되거나 중지될 때까지 실행됩니다 — 서브에이전트의 장시간 background 빌드가 더 이상 60분에 무음 종료되지 않습니다. (265) 디스크에 저장되는 도구 결과에 1GB 상한이 추가되고 저장 파일이 잘렸을 때 대화 내 미리보기에 그 사실이 표시됩니다 — 저장된 도구 결과 파일을 읽을 때는 이 절단 안내 유무를 먼저 확인한 뒤 완전한 것으로 간주합니다. (265) 비대화형 세션(`-p` + stream-json 입력, Agent SDK, cloud)이 매 사용자 메시지마다 셸 작업 디렉토리를 리셋하던 결함이 수정되어 `cd`가 턴 간 유지됩니다 — 턴마다 `cd`를 재실행하는 우회책을 쓴 `-p` 스크립트는 v2.1.265+에서 그 재실행이 불필요해집니다(무해하지만 제거 가능).
|
|
40
|
+
|
|
41
|
+
> **v2.1.259/261+**: (259) `claude plugin validate --json`이 기계 판독 가능한 검증 리포트를 제공합니다(cross-ref R017 v2.1.233 `plugin validate` 노트 — 사람 판독용 출력 파싱보다 이 쪽을 우선). (261) `/context`의 토큰 계산이 토큰-계산 API를 쓸 수 없을 때 추가 소형 모델 요청 대신 **로컬 추정치**를 사용하도록 바뀌었습니다 — 엔드포인트가 다운된 동안에는 `/context` 수치가 API 실측이 아니라 추정치일 수 있습니다. (261) `claude -p --resume <file>`이 트랜스크립트에 기록된 손상된 세션 ID를 그대로 채택하던 결함이 수정되어 이제 새 세션 ID로 재개합니다 — 트랜스크립트 기반 계수(R020)에서 v2.1.261+의 재개된 `-p` 세션은 재개 대상 파일과 다른 세션 ID를 가질 수 있습니다.
|
|
42
|
+
|
|
39
43
|
<!--
|
|
40
44
|
> **v2.1.206+**: `/doctor`에 checked-in CLAUDE.md에서 코드베이스로부터 파생 가능한 내용을 잘라내도록 제안하는 체크가 추가되었습니다 — R005 "Context Optimization via HTML Comments"의 컨텍스트 절감 원칙과 정합(모델 불필요 메타데이터 축소).
|
|
41
45
|
-->
|
|
@@ -79,10 +79,14 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
|
|
|
79
79
|
|
|
80
80
|
> **v2.1.257+**: `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`가 신설되어, 설정 시 `CLAUDE_CODE_SUBAGENT_MODEL`(또는 메인 모델)을 **모든** 서브에이전트에 강제 적용하며 per-spawn(Tier 3)과 agent-definition(Tier 1/2) model override를 무시합니다. 즉 위 v2.1.251 노트의 "이 env var가 project의 model pin을 더 이상 깨뜨릴 수 없다"는 서술은 **FORCE 미설정 시에 한해** 참으로 좁혀집니다. FORCE가 설정된 환경에서는 frontmatter의 model이 실행 모델의 증거가 아니므로(R020 "attempt ≠ outcome"의 모델 각도, v2.1.223 강등 경고 노트와 같은 계열) 무인 실행 전 `env | grep -c CLAUDE_CODE_SUBAGENT_MODEL_FORCE`처럼 값 노출 없이 **설정 여부만** 확인합니다.
|
|
81
81
|
|
|
82
|
+
> **v2.1.259+**: 프론트매터 `model:` 관련 결함 2건이 수정되었습니다 — (a) 커스텀 커맨드와 **스킬**의 프론트매터 `model:`이 interactive 세션에서 **무시**되던 결함이 수정되어, 259 이전에는 이 파일 「Skill Frontmatter」의 선택적 `model` 필드가 interactive 실행에서 **효과가 없었고**, 스킬의 실제 실행 모델은 스킬이 무엇을 선언했든 세션 모델 그대로였습니다(R020 "attempt ≠ outcome"의 스킬 model pin 각도). (b) auto mode가 커맨드·스킬 프론트매터 `model:`이 명명한, 지원하지 않는 모델로 turn을 실행하던 결함이 수정되어 이제 세션 모델을 유지합니다 — 즉 auto mode에서는 스킬 `model:` pin이 세션 모델에 조용히 override될 수 있으므로, 실제 실행 모델은 프론트매터가 아니라 v2.1.223 강등 경고로 확인합니다.
|
|
83
|
+
|
|
82
84
|
> **Claude Fable 5 (access via CC v2.1.170+)**: Mythos-class model, GA on the Claude API and positioned as a tier above Opus — its capabilities exceed any previously GA model. CC v2.1.170 is the client version that adds access (the model's GA is an API/platform property, not a CC-release milestone). Available via frontmatter full ID `claude-fable-5` (Tier 2) or Agent tool `model: fable` (Tier 3) — NOT via a Tier-1 frontmatter alias. Reserve for the most complex reasoning where its capability premium is warranted; `sonnet` remains the default for general tasks and `opus` for architecture (cost/latency awareness, R005). CC v2.1.170 also fixes session transcripts not saving (and not appearing in `--resume`) when launched from a VS Code integrated terminal or any shell inheriting Claude Code env vars — relevant to transcript-dependent skills (`homework`, `episodic-memory`). Closes #1352.
|
|
83
85
|
|
|
84
86
|
> **v2.1.257+**: Claude Fable 5.1(`claude-fable-5-1`)이 추가되어 **기본 Fable 모델**이 되었습니다 — 1M context, $10/$50 per Mtok(캐시 읽기 $0.25/Mtok). Tier-3 `model: fable` alias는 이제 Fable 5.1로 해석됩니다(단 Claude apps gateway 세션은 게이트웨이가 아직 Fable 5.1을 미지원해 당분간 Fable 5로 유지됩니다 — `/model`에서 명시 선택해야 Fable 5.1 사용 가능). frontmatter에서 확정하려면 Tier-2 full ID `claude-fable-5-1`을 쓰고, 기존 `claude-fable-5` pin은 그대로 Fable 5에 남습니다 — Tier-1 alias 해석 주체는 CC라는 위 원칙(v2.1.219/222 노트와 동일 계열)의 재확인입니다.
|
|
85
87
|
|
|
88
|
+
> **v2.1.260+**: Fable 5.1 관련 결함 3건이 수정되고 개선 1건이 적용되었습니다 — `model: fable` 에이전트가 `ANTHROPIC_DEFAULT_FABLE_MODEL` pin에 `[1m]` 태그를 붙여도 이를 무시하고 200K 컨텍스트 창으로 조용히 실행되던 결함(즉 260 이전에는 Fable에 붙인 Tier-2 `[1m]` 접미사가 이 env pin 경로에서 **존중되지 않았습니다**); `/model` 피커가 Fable 5.1을 표시하지 않던 결함(`/model claude-fable-5-1` 직접 입력만 동작); Fable 5.1의 prompt caching이 도구 결과 이후 첨부된 컨텍스트를 커버하지 못해 매 도구-호출 turn마다 uncached 입력으로 재전송되던 결함; 그리고 세션 중 `/effort` 변경이 이제 Fable 5.1의 prompt cache를 무효화하지 않도록 개선되었습니다. 또한 (260) 1M-컨텍스트 모델의 auto-compact가 강화되어 Opus·Fable 세션이 1M-token 한도 직전에 compact하며, 초대형 컨텍스트의 복구 compaction이 10분 타임아웃으로 끊기지 않습니다 — 위 R013 v2.1.251 Sonnet 5 1M auto-compact 노트를 Opus/Fable로 확장합니다(cross-ref R013).
|
|
89
|
+
|
|
86
90
|
<!-- ARCHIVED CC version notes (historical):
|
|
87
91
|
> **v2.1.173+**: Fable 5 model IDs carrying a `[1m]` suffix are now auto-normalized (the suffix is stripped) because Fable 5 includes 1M context by default. Use `claude-fable-5` / `model: fable` WITHOUT a `[1m]` suffix — appending it is redundant and normalized away. (The `[1m]` suffix remains meaningful for Opus/Sonnet IDs.)
|
|
88
92
|
|
|
@@ -93,6 +97,8 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
|
|
|
93
97
|
|
|
94
98
|
> **Fable 5 Effort 전략**: Fable 5는 **high effort가 기본값**이며, `xhigh`는 capability-sensitive 작업(최고난도 아키텍처/추론)에 한정해야 합니다. Fable 5의 `low`/`medium` effort조차 이전 세대 모델의 `xhigh`를 상회하는 품질을 보이므로, Fable 5를 사용하는 실행 에이전트는 `effort` 필드를 신중히 명시하고 불필요한 `xhigh` 남용을 지양합니다(R005 비용/지연 인식과 정합).
|
|
95
99
|
|
|
100
|
+
> **v2.1.267+**: `effort:` 프론트매터 관련 결함·신규 상한 3건 — (a) 커스텀 커맨드·스킬·서브에이전트의 `effort:` 프론트매터가, 기본 effort가 여전히 고정된 모델(Opus 4.7, Opus 4.8, Fable 5)에서 **무시**되던 결함이 수정되었습니다. 즉 267 이전에는 이 파일의 "스킬 `effort`가 에이전트 `effort`보다 우선한다"는 서술과 위 「Fable 5 Effort 전략」의 `effort` 명시 지침이 Fable 5 / Opus 4.8 에이전트에서 **런타임 효과가 없었습니다** — 프론트매터 effort는 실제 실행된 effort의 증거가 아니었습니다. (b) 신규 `maxEffortLevel` 설정(최상위 또는 `modelSettings` 하위 모델별)이 모든 provider에서 effort 레벨 상한을 강제합니다 — 사용자는 여전히 더 낮은 레벨을 선택할 수 있습니다. 이는 프론트매터 `effort`보다 **상위에 위치하는 설정 레벨 상한**이므로, `xhigh`를 선언한 에이전트도 상한이 설정돼 있으면 그 상한에서 실행됩니다(프론트매터로 유추하지 말고 실효 effort를 확인). (c) `/model opusplan[1m]`이 "Model not found"로 거부되던 결함이 265에서 수정되어, `/model` 명령에서 이 표기가 이제 수용됩니다 — 프론트매터 `model: opusplan[1m]` 경로는 릴리즈 노트가 언급하지 않으므로 미실측입니다.
|
|
101
|
+
|
|
96
102
|
> **Mythos 5 (`claude-mythos-5`)**: Project Glasswing 한정 공급 모델로, **GA가 아닙니다** — Fable 5(GA, 위 "Model Specification — 3 Tiers"의 `claude-fable-5`/`fable`)와 구분해야 합니다. 특성: adaptive-thinking 전용 아키텍처 + 안전 분류기가 개입 시 `stop_reason: "refusal"`로 fallback하는 체계를 가집니다. oh-my-customcode 에이전트 frontmatter에는 아직 alias를 등록하지 않습니다(비-GA, 공급 제한).
|
|
97
103
|
|
|
98
104
|
> **프롬프팅 패턴 상호참조**: Fable 5/Mythos 5 대상 프롬프팅 패턴(effort 조합, adaptive-thinking 활용, refusal fallback 대응)의 상세 가이드는 `guides/claude-code/16-fable5-prompting.md`를 참조하세요.
|
|
@@ -139,6 +145,8 @@ This is a settings-level resilience mechanism, distinct from the per-agent `mode
|
|
|
139
145
|
|
|
140
146
|
Key optional fields: `memory`, `effort`, `skills`, `soul`, `isolation`, `background`, `maxTurns`, `maxTokens`, `mcpServers`, `hooks`, `permissionMode`, `disallowedTools`, `limitations`, `domain`, `disableSkillShellExecution`, `experimental.cacheTtl` (v2.1.248+). Supported since CC v2.1.63+. See full optional frontmatter via Read tool.
|
|
141
147
|
|
|
148
|
+
> **v2.1.261/265/267+**: 서브에이전트/스킬 런타임 관련 3건 — (261) `--append-subagent-system-prompt-file`이 신설되어 커맨드라인에 담기 힘들 만큼 큰 서브에이전트 시스템 프롬프트를 파일에서 읽습니다(R009의 ~5000-token 프롬프트 휴리스틱과 정합 — 대형 프롬프트는 단순 파일 로드가 아니라 우선 분할의 신호입니다). (265) forked 스킬(`context: fork`)이 착수(kickoff) 프롬프트를 스트리밍하지 않고, `--forward-subagent-text`와 함께 쓸 때 그 텍스트 turn을 stream-json progress 이벤트로 내보내지 않던 결함이 수정되었습니다 — 아래 「Context Fork Criteria」의 10/12 `context: fork` 스킬을 `-p --output-format stream-json`으로 실행할 때 관련되며, 구버전에서는 fork progress 이벤트 부재가 fork가 실행되지 않았다는 증거가 아니었습니다. (267) `--system-prompt`/`--append-system-prompt`로 시작한 서브에이전트·세션이 이제 시스템 프롬프트와 도구 정의를 매 요청마다 재렌더링하는 대신 한 번만 기록합니다(prompt-cache 안정성) — `--system-prompt-snapshot off`는 프롬프트 반복 작업을 위해 매 요청 새로 렌더링합니다.
|
|
149
|
+
|
|
142
150
|
### Note on `skills:` field
|
|
143
151
|
|
|
144
152
|
The `skills:` frontmatter field is **advisory metadata** consumed by oh-my-customcode tooling (graph-builder, mgr-sauron) for documentation and validation. It is **NOT a runtime allowlist** — Claude Code does not filter the available skills based on this field, and subagents can invoke any registered skill regardless of what `skills:` declares. Use it to document a subagent's intended skill dependencies; do not rely on it for access control.
|
|
@@ -432,6 +432,10 @@ Cross-reference: R020 ("actual outcome ≠ attempt" — verifying that a command
|
|
|
432
432
|
|
|
433
433
|
> **v2.1.257+**: 세 건이 함께 수정되었습니다. (a) leader의 mailbox 쓰기가 잠시 잠긴 사이 teammate permission request가 **두 번 응답**되던 결함 수정 — v2.1.224/251 SendMessage·inbox 신뢰성 계열의 연장이며, 구버전에서 승인이 2회 적용된 흔적은 이중 승인 의도의 증거가 아닙니다. (b) tmux/iTerm2 pane의 teammate가 shutdown 확인 후에도 열려 있던 결함 수정 — 위 Lifecycle의 `TeamDelete` 이후 pane 잔존은 더 이상 정상이 아닙니다. (c) `/fork`가 원 대화의 prompt cache를 새 background 세션에서 유지하도록 개선(worktree briefing이 system-prompt 변경 대신 메시지로 도착) — R009 fork 컨텍스트 상속 노트의 비용 각도. 이 저장소는 `TeamCreate` 미등록으로 R018이 dormant이므로 (a)(b)는 기록용, (c)는 fork 사용 시 즉시 해당합니다.
|
|
434
434
|
|
|
435
|
+
> **v2.1.260+**: `SendMessage`/teams 신뢰성 계열에서 세 건이 추가로 수정되었습니다 — 다른 에이전트를 `SendMessage`로 resume한 서브에이전트가 그 에이전트의 완료 알림을 받지 못하고(알림이 대신 main conversation으로 갔습니다), in-process teammate의 트랜스크립트가 긴 API 재시도 대기(`CLAUDE_CODE_RETRY_WATCHDOG` 등) 중 재시도 알림에 실제 메시지가 덮여 유실되거나 비어 보였으며, background로 이동한 세션이 `ListAgents`에 동일 이름의 "interactive" 유령 쌍둥이로 **두 번** 나타나 뷰어 쪽이 `SendMessage` 전달을 받는 문제가 있었습니다 — 위 v2.1.229 `ListAgents` 노트("열거됐다고 도달 가능하다고 가정하지 않는다")를 확장합니다: 나열된 이름이 단일 실제 대상이라는 보장조차 없었습니다. 결론은 그대로입니다 — 결정론적 ground-truth만이 완료 증거입니다.
|
|
436
|
+
|
|
437
|
+
> **v2.1.261+**: 다른 머신의 OFFLINE Remote Control 세션으로 `SendMessage`를 보내면 전송됨으로 읽혔으나, 이제 그 머신이 재연결될 때까지 전달이 큐잉된다는 결과를 반환합니다 — v2.1.224 inbox 쓰기 실패·v2.1.251 최종 답변 유실에 이은 이 계열의 세 번째 사례이며, 구버전의 "Message sent"는 상대가 온라인이라는 보장조차 아니었습니다. 같은 릴리즈에서 in-process agent-team teammate가 두 번째 턴에 첫 턴의 도구·스킬 announcement를 재전송해 요청 prefix가 바뀌고 prompt cache를 놓치던 결함도 수정되었고, (265) teammate·resumed 서브에이전트가 `SubagentStart` 훅 컨텍스트와 preloaded skill을 prompt prefix 밖으로 이동시키던 결함도 수정되었습니다. 이 저장소의 R018은 `TeamCreate` 부재로 dormant이므로, 이 노트는 cross-session `SendMessage` 경로에 한해 기록합니다.
|
|
438
|
+
|
|
435
439
|
<!-- ARCHIVED CC version note (historical):
|
|
436
440
|
> **CC v2.1.162+**: `claude agents --json` now includes a `waitingFor` field showing what a waiting session is blocked on (e.g. a permission prompt). Use it as an additional deterministic ground-truth signal — a member with a non-empty `waitingFor` is blocked on input (needs unblocking), NOT silently stalled (reassign per stall handling below). This distinguishes the two failure modes the verification is meant to separate.
|
|
437
441
|
|
|
@@ -426,6 +426,10 @@ Origin: #1269 ① (R020 self-violation, session 106).
|
|
|
426
426
|
|
|
427
427
|
**교훈**: 위 Core Rule("actual outcome ≠ attempt")은 방향이 없다 — 도구가 성공을 보고하든 실패를 보고하든, 보고 자체는 ground-truth가 아니다. 실패 보고를 받았다고 곧바로 재시도·롤백에 들어가지 말고, 먼저 실제 산출물 상태를 확인한다.
|
|
428
428
|
|
|
429
|
+
> **v2.1.259/261+**: "정지 보고 ≠ 실제 정지" 결함 3건이 수정되었습니다. (259) Stop이 remote-control 세션의 background agent·workflow를 실제로 멈추지 못했던 결함 — 이제 kill된 task는 프로세스가 종료될 때까지 계속 표시되고 재정지 가능합니다. (261) SDK·cloud 세션이 첫 프롬프트 직후, turn이 시작하기 전에 도착한 Stop/interrupt를 무시하고 turn을 끝까지 실행하던 결함이 수정되었습니다. (261) 터미널 진행 표시(iTerm2, Ghostty, ConEmu)가 background workflow/agent가 아직 실행 중인데도 세션을 완료된 것으로 표시하던 결함이 수정되었습니다. 세 건 모두 이 섹션 원칙의 **역방향 쌍둥이**입니다 — "정지됨"/"완료됨" 신호도 ground-truth가 아니므로, 인터럽트가 실제로 적용됐다고 단정하기 전에 프로세스/run 상태(`gh run list`, task 패널, `pgrep`)로 확인합니다. 같은 릴리즈에서 (259) 이전 정지 실행이 종료 중인 상태에서 workflow run을 재개하면 그 에이전트들이 **중복 실행**될 수 있던 결함도 수정되었습니다(cross-ref R023 Workflow resume).
|
|
430
|
+
|
|
431
|
+
> **v2.1.265/267+**: (265) 이전 프로세스가 도구 실행 중 죽은 뒤 재개하면 마지막 프롬프트를 더 이상 재작성하지 않고, 중단된 도구 호출을 유지하며 **interrupted로 명시 표시**합니다 — 위 v2.1.246 행(인터럽트된 셸 명령이 단순 "실행됨"으로만 표시)의 연장선으로, 이 경로에서는 이제 트랜스크립트에 명시적 interrupted 마커가 남으므로 v2.1.265+에서는 그 부재가 유의미한 증거지만 구버전에서는 아닙니다. (267) `/compact` 또는 다른 슬래시 커맨드 직후 `-p --resume`으로 재개할 때 가짜 "Continue from where you left off." turn이 더 이상 삽입되지 않습니다 — `/fsd` 류 `-p` 루프에서 이런 turn을 사용자 입력으로 오인할 수 있었던 경로와 관련됩니다(R015: 이것은 지시가 아닙니다). 또한 큰 세션(트랜스크립트 5MB 초과)을 재개할 때 병렬 도구 호출과 그 훅 출력이 재로드된 대화에서 누락되던 결함도 수정되었습니다(cross-ref R021) — 회고적 트랜스크립트 계수(「Self-Violation Counting Is Also Diagnosis」)에서, 구버전으로 재개된 5MB 초과 세션은 도구 호출이 유실됐을 수 있으므로 그런 트랜스크립트의 계수는 **하한값**으로 취급합니다.
|
|
432
|
+
|
|
429
433
|
### CI Publish-Step Error vs Published-Artifact Ground Truth
|
|
430
434
|
|
|
431
435
|
> Origin: #1332 — `npm publish --provenance` emitted a Sigstore `TLOG_CREATE_ENTRY_ERROR` 409, but the publish step's `|| npm view <pkg>@<ver>` fallback recovered (the package WAS published) and release.yml succeeded on all jobs. A subagent read the tlog error in the logs and prematurely declared the run "failed", recommending a re-run; deterministic ground-truth (`npm view`, `gh release view`) showed the release had fully succeeded.
|
|
@@ -47,6 +47,10 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
47
47
|
|
|
48
48
|
> **v2.1.248+**: 훅 관측성이 두 건 강화되었습니다 — (a) `PermissionRequest`/`PreToolUse` 훅이 유효하지 않은 응답을 출력해 background session이 조용히 대기하던 결함이 수정되어, 이제 `claude agents` 행이 해당 훅 이름과 스키마 에러를 표시합니다. (b) 훅이 stdout으로 낸 `{…}` 객체가 유효한 JSON이 아닐 때 조용히 plain text로 처리하던 결함이 수정되어, 이제 parse 에러와 함께 훅 에러로 보고됩니다. 위 v2.1.214 "훅 stdout JSON이 스키마 검증에 실패할 때 exit code 2가 문서대로 차단하지 못하던 문제" 노트와 같은 계열 — 훅 실패가 무음에서 가시화되는 흐름의 연속입니다.
|
|
49
49
|
|
|
50
|
+
> **v2.1.259+**: blocking Stop 훅이 발동한 턴의 **다음** 턴이 그 턴의 모델 reasoning을 잃고, 일부 모델에서는 prompt cache까지 miss하던 결함이 수정되었습니다. 이 저장소의 Stop-hook 계층(`session-reflection.sh`, retroactive R007/R008 advisory)에 직접 해당됩니다 — 구버전에서는 Stop 훅의 block이 차단 자체 외에 **숨은 비용**(reasoning 유실 + cache miss)을 수반했으므로, v1.1.53~56에 기록된 "Stop 훅 잠식" 패턴의 **원인 후보 중 하나**로 재해석할 여지가 있습니다. 단 확정 원인은 아니며 가설 후보로만 취급합니다(R020 hypothesis 규율). 같은 릴리즈에서 `claude plugin validate --json`이 기계 판독 가능한 리포트를 제공하게 되었습니다(위 R017 v2.1.233 `plugin validate` 노트의 연장) — Tier-1 결정론적 검사로 활용 가능합니다.
|
|
51
|
+
|
|
52
|
+
> **v2.1.261/265/267+**: resume 시 훅 컨텍스트 무결성 결함 3건이 수정되었습니다. (261) 세션 재개 시 **병렬 도구 호출 주변**의 훅 출력 등 컨텍스트가 유실되어 재개된 요청이 달라지던 결함이 수정되었습니다. (267) 대용량 세션(트랜스크립트 5MB 초과) 재개 시 병렬 도구 호출과 그 훅 출력이 누락되던 결함이 수정되었습니다. (265) agent teammate와 재개된 서브에이전트가 이후 턴에서 `SubagentStart` 훅 컨텍스트와 preload된 스킬을 prompt prefix 밖으로 이동시켜 prompt-cache 재사용을 깨뜨리던 결함이 수정되었습니다. 함의: 구버전에서 재개된 세션의 대화에 어떤 훅의 출력이 보이지 않는다고 해서 그 훅이 **발화하지 않았다**는 증거는 아닙니다 — 「배선 확인 ≠ 전달 확인 ≠ 발화 확인 ≠ 로드 확인」4층 구분에 5번째 각도(**발화 ≠ 재개 후 보존**)가 추가됩니다. 또한 (267) managed `allowedHttpHookUrls`/`httpHookAllowedEnvVars`가 읽을 수 없을 때 아무것도 허용하지 않도록(fail-closed) 변경되었습니다.
|
|
53
|
+
|
|
50
54
|
## Why Advisory-First
|
|
51
55
|
|
|
52
56
|
1. **Agent flexibility**: Hard blocks can trap agents in unrecoverable states
|