oh-my-customcode 1.1.62 → 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/hooks/scripts/audit-log.sh +24 -4
- package/templates/.claude/hooks/scripts/failure-ledger.sh +6 -1
- package/templates/.claude/hooks/scripts/git-delegation-guard.sh +12 -2
- package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +14 -3
- package/templates/.claude/hooks/scripts/playwright-compress.sh +13 -5
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +3 -0
- package/templates/.claude/hooks/scripts/secret-filter.sh +14 -10
- package/templates/.claude/hooks/scripts/stuck-detector.sh +37 -3
- package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +99 -19
- 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 +10 -0
- package/templates/.claude/rules/MUST-enforcement-policy.md +4 -0
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +6 -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/MUST-tool-identification.md +11 -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 +6 -0
- package/templates/.claude/skills/fsd/SKILL.md +1 -0
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +5 -1
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +5 -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
|
@@ -20,14 +20,34 @@ printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
|
20
20
|
# Extract fields from hook input
|
|
21
21
|
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
|
|
22
22
|
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // .tool_input.command? // ""' | head -c 200)
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# Agent identity and model. PostToolUse has NO top-level `model` at all, and its
|
|
24
|
+
# top-level `agent_type` is present only when the hook fires from inside a subagent
|
|
25
|
+
# (CC 2.1.259 schema: "Present when the hook fires from within a subagent") — so on
|
|
26
|
+
# the main thread both reads resolved to "unknown" on every single entry.
|
|
27
|
+
# Measured replacements (#1656 C): the Agent tool's result object carries
|
|
28
|
+
# `resolvedModel` (892 occurrences in this project's transcript corpus, e.g.
|
|
29
|
+
# "claude-sonnet-5" / "claude-opus-5[1m]") and, on the synchronous shape, `agentType`;
|
|
30
|
+
# the spawn arguments carry `subagent_type`.
|
|
31
|
+
# `?` keeps a scalar-shaped tool_input/tool_response from aborting under `set -euo pipefail`.
|
|
32
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '
|
|
33
|
+
(.agent_type?
|
|
34
|
+
// .tool_input?.subagent_type?
|
|
35
|
+
// .tool_response?.agentType?
|
|
36
|
+
// "unknown") | tostring
|
|
37
|
+
' 2>/dev/null) || agent_type="unknown"
|
|
38
|
+
model=$(printf '%s\n' "$input" | jq -r '
|
|
39
|
+
(.model?
|
|
40
|
+
// .tool_response?.resolvedModel?
|
|
41
|
+
// "unknown") | tostring
|
|
42
|
+
' 2>/dev/null) || model="unknown"
|
|
25
43
|
# Outcome signal. PostToolUse carries the result under `tool_response`, not
|
|
26
44
|
# `tool_output` (measured 2026-09-03: 1764/1764 PostToolUse payloads had
|
|
27
45
|
# `tool_response`, 0/1764 had `tool_output`), so the old `.tool_output.is_error`
|
|
28
46
|
# read was always absent and every entry was logged as outcome=success.
|
|
29
|
-
#
|
|
30
|
-
# `.tool_response.interrupted
|
|
47
|
+
# Failure signals read below: `.tool_response.is_error` (generic) and
|
|
48
|
+
# `.tool_response.interrupted`. The latter is present on every Bash response but
|
|
49
|
+
# was never observed true in this corpus (0/1555), so it is defensive coverage —
|
|
50
|
+
# not a signal measured to fire.
|
|
31
51
|
# jq's `//` treats both null and false as absent, so this chain is an OR:
|
|
32
52
|
# the result is true iff at least one signal is true.
|
|
33
53
|
is_error=$(printf '%s\n' "$input" | jq -r '
|
|
@@ -52,6 +52,9 @@ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
|
|
52
52
|
# (r007-r008-drift-advisor.sh의 `.role` vs `.message.role`과 동일 계열).
|
|
53
53
|
# .tool_error / .tool_response.* fallback은 스키마 변화에 대한 방어로만 남긴다 —
|
|
54
54
|
# tool_response가 문자열인 경우 인덱싱 에러로 레코드가 통째로 유실되므로 type 검사로 감싼다.
|
|
55
|
+
# .tool_input도 같은 이유로 type 검사를 씌운다 (#1656 A 실측): 스칼라/배열 tool_input이
|
|
56
|
+
# 오면 jq가 인덱싱 에러로 죽고 `2>/dev/null || true` 때문에 **조용히 레코드 전체가
|
|
57
|
+
# 유실**됐다 — rc는 0이고 stderr도 비어 있어 크래시가 아니라 데이터 손실로 나타난다.
|
|
55
58
|
#
|
|
56
59
|
# 단일 라인(<1KB) append 이므로 O_APPEND 원자성에 기대어 병렬 에이전트 환경에서도 안전.
|
|
57
60
|
printf '%s' "$input" \
|
|
@@ -61,7 +64,9 @@ printf '%s' "$input" \
|
|
|
61
64
|
session: (.session_id // ""),
|
|
62
65
|
cwd: $cwd,
|
|
63
66
|
tool: (.tool_name // "unknown"),
|
|
64
|
-
target: ((.tool_input
|
|
67
|
+
target: ((if (.tool_input | type) == "object"
|
|
68
|
+
then (.tool_input.command // .tool_input.file_path // "")
|
|
69
|
+
else "" end) | tostring | .[0:160]),
|
|
65
70
|
interrupt: (.is_interrupt == true),
|
|
66
71
|
err: ((.error
|
|
67
72
|
// .tool_error
|
|
@@ -14,8 +14,18 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
14
14
|
|
|
15
15
|
input=$(cat)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0
|
|
18
|
+
# instead of reflecting garbage back on stdout. The sibling hooks (audit-log.sh,
|
|
19
|
+
# model-escalation-advisor.sh, task-outcome-recorder.sh) all carry this guard;
|
|
20
|
+
# this one did not, so a non-object payload was echoed through verbatim. (#1656 F)
|
|
21
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
22
|
+
|
|
23
|
+
# `?` suppresses jq's "Cannot index <type> with string" when tool_input arrives as a
|
|
24
|
+
# scalar/array instead of an object (measured #1656 A: the bare form leaked a jq error to
|
|
25
|
+
# stderr on every non-object tool_input). 2>/dev/null + `|| echo ""` keep the hook silent
|
|
26
|
+
# and advisory-only even if jq itself fails for an unrelated reason.
|
|
27
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // ""' 2>/dev/null || echo "")
|
|
28
|
+
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt? // ""' 2>/dev/null || echo "")
|
|
19
29
|
|
|
20
30
|
# R010 violation tracking file (PPID-scoped for session persistence)
|
|
21
31
|
VIOLATION_FILE="/tmp/.claude-r010-violations-${PPID}"
|
|
@@ -34,13 +34,22 @@ CONSECUTIVE_THRESHOLD=3
|
|
|
34
34
|
COOLDOWN=5
|
|
35
35
|
|
|
36
36
|
# Count failures for this agent type
|
|
37
|
+
#
|
|
38
|
+
# `grep -c` COUNT HYGIENE (#1656 E): a no-match `grep -c` prints `0` on stdout AND
|
|
39
|
+
# exits 1, so the old `$(grep -c ... || echo "0")` appended a SECOND zero and the
|
|
40
|
+
# variable became the two-line string "0\n0". Every later `[ "$count" -ge N ]` then
|
|
41
|
+
# failed with "integer expression expected" on stderr — noise indistinguishable
|
|
42
|
+
# from a real hook fault, on the exact path (zero failures) that is the common case.
|
|
43
|
+
# `|| true` keeps grep's own `0` and `${x:-0}` covers an unreadable file.
|
|
37
44
|
agent_failures=0
|
|
38
45
|
if [ -n "$agent_type" ] && [ "$agent_type" != "unknown" ]; then
|
|
39
|
-
agent_failures=$(grep -c "\"agent_type\":\"${agent_type}\".*\"outcome\":\"failure\"" "$OUTCOME_FILE" 2>/dev/null ||
|
|
46
|
+
agent_failures=$(grep -c "\"agent_type\":\"${agent_type}\".*\"outcome\":\"failure\"" "$OUTCOME_FILE" 2>/dev/null || true)
|
|
47
|
+
agent_failures=${agent_failures:-0}
|
|
40
48
|
fi
|
|
41
49
|
|
|
42
50
|
# Count consecutive failures (tail entries)
|
|
43
|
-
consecutive_failures=$(tail -${CONSECUTIVE_THRESHOLD} "$OUTCOME_FILE" 2>/dev/null | grep -c '"outcome":"failure"' 2>/dev/null ||
|
|
51
|
+
consecutive_failures=$(tail -${CONSECUTIVE_THRESHOLD} "$OUTCOME_FILE" 2>/dev/null | grep -c '"outcome":"failure"' 2>/dev/null || true)
|
|
52
|
+
consecutive_failures=${consecutive_failures:-0}
|
|
44
53
|
|
|
45
54
|
# Escalation path
|
|
46
55
|
# NOTE: Agent tool `model` param is an enum of exactly 4 values: sonnet | opus | haiku | fable.
|
|
@@ -90,7 +99,9 @@ fi
|
|
|
90
99
|
|
|
91
100
|
# De-escalation check
|
|
92
101
|
if [ "$current_model" != "haiku" ] && [ "$current_model" != "inherit" ] && [ "$current_model" != "" ]; then
|
|
93
|
-
|
|
102
|
+
# Same `grep -c` hygiene as above (#1656 E).
|
|
103
|
+
recent_successes=$(tail -${COOLDOWN} "$OUTCOME_FILE" 2>/dev/null | grep -c '"outcome":"success"' 2>/dev/null || true)
|
|
104
|
+
recent_successes=${recent_successes:-0}
|
|
94
105
|
|
|
95
106
|
if [ "$recent_successes" -ge "$COOLDOWN" ]; then
|
|
96
107
|
lower_model=""
|