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.
Files changed (30) hide show
  1. package/dist/cli/index.js +888 -11488
  2. package/dist/index.js +52 -265
  3. package/package.json +1 -1
  4. package/templates/.claude/hooks/scripts/audit-log.sh +24 -4
  5. package/templates/.claude/hooks/scripts/failure-ledger.sh +6 -1
  6. package/templates/.claude/hooks/scripts/git-delegation-guard.sh +12 -2
  7. package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +14 -3
  8. package/templates/.claude/hooks/scripts/playwright-compress.sh +13 -5
  9. package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +3 -0
  10. package/templates/.claude/hooks/scripts/secret-filter.sh +14 -10
  11. package/templates/.claude/hooks/scripts/stuck-detector.sh +37 -3
  12. package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +99 -19
  13. package/templates/.claude/rules/MAY-optimization.md +4 -0
  14. package/templates/.claude/rules/MUST-agent-design.md +8 -0
  15. package/templates/.claude/rules/MUST-agent-teams.md +4 -0
  16. package/templates/.claude/rules/MUST-completion-verification.md +10 -0
  17. package/templates/.claude/rules/MUST-enforcement-policy.md +4 -0
  18. package/templates/.claude/rules/MUST-orchestrator-coordination.md +6 -0
  19. package/templates/.claude/rules/MUST-parallel-execution.md +2 -0
  20. package/templates/.claude/rules/MUST-permissions.md +6 -0
  21. package/templates/.claude/rules/MUST-safety.md +4 -0
  22. package/templates/.claude/rules/MUST-sync-verification.md +2 -0
  23. package/templates/.claude/rules/MUST-tool-identification.md +11 -0
  24. package/templates/.claude/rules/SHOULD-ecomode.md +4 -0
  25. package/templates/.claude/rules/SHOULD-hud-statusline.md +2 -0
  26. package/templates/.claude/rules/SHOULD-verification-ladder.md +6 -0
  27. package/templates/.claude/skills/fsd/SKILL.md +1 -0
  28. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +5 -1
  29. package/templates/manifest.json +1 -1
  30. package/templates/workflows/auto-dev.yaml +5 -1
package/dist/index.js CHANGED
@@ -1,47 +1,16 @@
1
- import { createRequire } from "node:module";
2
- var __defProp = Object.defineProperty;
3
- var __returnValue = (v) => v;
4
- function __exportSetter(name, newValue) {
5
- this[name] = __returnValue.bind(null, newValue);
6
- }
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, {
10
- get: all[name],
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 dir2 = registryDir();
357
- await mkdir(dir2, { recursive: true });
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 basename2 = relativePath.split("/").pop() ?? "";
1171
- if (PROTECTED_FRAMEWORK_FILES.includes(basename2)) {
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((resolve2, reject) => {
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
- resolve2(hash.digest("hex"));
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
- const { createTempDir: createTempDir2 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
2100
- const tempDir = await createTempDir2("omcustom-preserve-");
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.62",
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((resolve2, reject) => {
2639
- res = resolve2;
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 copy2 = resTypeIsArray ? [] : {};
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
- copy2[m] = this.translate(deepKey, {
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
- copy2[m] = this.translate(deepKey, {
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 (copy2[m] === deepKey)
3314
- copy2[m] = resForObjHndl[m];
3100
+ if (copy[m] === deepKey)
3101
+ copy[m] = resForObjHndl[m];
3315
3102
  }
3316
3103
  }
3317
- res = copy2;
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 sep2 = this.nestingOptionsSeparator;
3958
- if (!key.includes(sep2))
3744
+ const sep = this.nestingOptionsSeparator;
3745
+ if (!key.includes(sep))
3959
3746
  return key;
3960
- const c = key.split(new RegExp(`${regexEscape(sep2)}[ ]*{`));
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}${sep2}${optionsString}`;
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 (error2) {
4153
- this.logger.warn(error2);
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
- const { registerProject: registerProject2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
5423
- await registerProject2(targetDir, newVersion);
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 update2 of updates) {
5531
- const fullPath = join7(targetDir, update2.path);
5317
+ for (const update of updates) {
5318
+ const fullPath = join7(targetDir, update.path);
5532
5319
  await ensureDirectory(join7(fullPath, ".."));
5533
- await fs.writeFile(fullPath, update2.content, "utf-8");
5534
- debug("update.file_applied", { path: update2.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 warnedPaths2 = await findProtectedFilesInDir(srcPath, componentPath);
5590
- return { skipPaths: [], warnedPaths: warnedPaths2, updatedPaths: [] };
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(dir2, fs) {
5404
+ async function safeReaddir(dir, fs) {
5618
5405
  try {
5619
- return await fs.readdir(dir2, { withFileTypes: true });
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: dir2, relDir } = queue.shift();
5631
- const entries = await safeReaddir(dir2, fs);
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(dir2, entry.name);
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: dir2, relDir } = queue.shift();
5600
+ const { dir, relDir } = queue.shift();
5814
5601
  let entries;
5815
5602
  try {
5816
- entries = await fs.readdir(dir2, { withFileTypes: true });
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(dir2, entry.name);
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 dir2 of dirsToBackup) {
5851
- const srcPath = join7(targetDir, dir2);
5637
+ for (const dir of dirsToBackup) {
5638
+ const srcPath = join7(targetDir, dir);
5852
5639
  if (await fileExists(srcPath)) {
5853
- const destPath = join7(backupDir, dir2);
5640
+ const destPath = join7(backupDir, dir);
5854
5641
  await copyDirectory(srcPath, destPath, { overwrite: true });
5855
5642
  }
5856
5643
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.62",
6
+ "version": "1.1.64",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -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
- agent_type=$(printf '%s\n' "$input" | jq -r '.agent_type // "unknown"')
24
- model=$(printf '%s\n' "$input" | jq -r '.model // "unknown"')
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
- # Measured failure signals: `.tool_response.is_error` (generic) and
30
- # `.tool_response.interrupted` (present on every Bash response).
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.command // .tool_input.file_path // "") | tostring | .[0:160]),
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
- agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // ""')
18
- prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt // ""')
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 || echo "0")
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 || echo "0")
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
- recent_successes=$(tail -${COOLDOWN} "$OUTCOME_FILE" 2>/dev/null | grep -c '"outcome":"success"' 2>/dev/null || echo "0")
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=""