oh-my-customcode 1.1.102 → 1.1.103

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 (52) hide show
  1. package/dist/cli/index.js +363 -26
  2. package/dist/index.js +95 -5
  3. package/package.json +1 -1
  4. package/templates/.claude/output-styles/korean-engineer.md +1 -1
  5. package/templates/.claude/rules/MUST-agent-design.md +2 -2
  6. package/templates/.claude/rules/MUST-agent-teams.md +13 -2
  7. package/templates/.claude/rules/MUST-orchestrator-coordination.md +41 -29
  8. package/templates/.claude/rules/MUST-permissions.md +12 -9
  9. package/templates/.claude/skills/action-validator/SKILL.md +2 -2
  10. package/templates/.claude/skills/adaptive-harness/SKILL.md +2 -2
  11. package/templates/.claude/skills/adversarial-review/SKILL.md +1 -1
  12. package/templates/.claude/skills/agent-eval-framework/SKILL.md +2 -2
  13. package/templates/.claude/skills/de-lead-routing/SKILL.md +2 -2
  14. package/templates/.claude/skills/deep-plan/SKILL.md +4 -4
  15. package/templates/.claude/skills/deep-verify/SKILL.md +3 -3
  16. package/templates/.claude/skills/design-shotgun/SKILL.md +1 -1
  17. package/templates/.claude/skills/dev-lead-routing/SKILL.md +2 -2
  18. package/templates/.claude/skills/dev-review/SKILL.md +2 -2
  19. package/templates/.claude/skills/evaluator-optimizer/SKILL.md +1 -1
  20. package/templates/.claude/skills/fsd/SKILL.md +3 -4
  21. package/templates/.claude/skills/harness-eval/SKILL.md +1 -1
  22. package/templates/.claude/skills/harness-synthesizer/SKILL.md +1 -1
  23. package/templates/.claude/skills/homework/SKILL.md +3 -3
  24. package/templates/.claude/skills/idea/SKILL.md +4 -2
  25. package/templates/.claude/skills/instinct-extractor/SKILL.md +8 -8
  26. package/templates/.claude/skills/intent-detection/SKILL.md +1 -1
  27. package/templates/.claude/skills/multi-model-verification/SKILL.md +2 -2
  28. package/templates/.claude/skills/omcustom-release-notes/SKILL.md +1 -1
  29. package/templates/.claude/skills/pipeline/SKILL.md +1 -1
  30. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +2 -2
  31. package/templates/.claude/skills/post-release-followup/SKILL.md +2 -2
  32. package/templates/.claude/skills/professor-triage/SKILL.md +10 -10
  33. package/templates/.claude/skills/profile/SKILL.md +4 -4
  34. package/templates/.claude/skills/qa-lead-routing/SKILL.md +2 -2
  35. package/templates/.claude/skills/release-plan/SKILL.md +1 -1
  36. package/templates/.claude/skills/research/SKILL.md +8 -6
  37. package/templates/.claude/skills/result-aggregation/SKILL.md +1 -1
  38. package/templates/.claude/skills/roundtable-debate/SKILL.md +2 -2
  39. package/templates/.claude/skills/scout/SKILL.md +3 -8
  40. package/templates/.claude/skills/sec-agentshield-wrapper/SKILL.md +1 -1
  41. package/templates/.claude/skills/secretary-routing/SKILL.md +2 -2
  42. package/templates/.claude/skills/skill-extractor/SKILL.md +1 -1
  43. package/templates/.claude/skills/structured-dev-cycle/SKILL.md +1 -1
  44. package/templates/.claude/skills/task-decomposition/SKILL.md +1 -1
  45. package/templates/.claude/skills/worker-reviewer-pipeline/SKILL.md +5 -1
  46. package/templates/guides/claude-code/15-version-compatibility.md +1 -1
  47. package/templates/guides/deep-plan/phases.md +11 -9
  48. package/templates/guides/harness-engineering/README.md +2 -1
  49. package/templates/guides/professor-triage/README.md +1 -1
  50. package/templates/guides/professor-triage/phases.md +1 -1
  51. package/templates/manifest.json +1 -1
  52. package/templates/workflows/auto-dev.yaml +2 -2
package/dist/cli/index.js CHANGED
@@ -335,7 +335,7 @@ var init_package = __esm(() => {
335
335
  workspaces: [
336
336
  "packages/*"
337
337
  ],
338
- version: "1.1.102",
338
+ version: "1.1.103",
339
339
  description: "Batteries-included agent harness for Claude Code",
340
340
  type: "module",
341
341
  bin: {
@@ -15786,6 +15786,7 @@ var MESSAGES = {
15786
15786
  "lockfile.component_dir_missing": "Component directory missing: {{path}}",
15787
15787
  "lockfile.hash_failed": "Failed to hash file: {{path}} — {{error}}",
15788
15788
  "lockfile.entry_added": "Lockfile entry added: {{path}} ({{component}})",
15789
+ "lockfile.entry_git_excluded": "Lockfile entry skipped (not in git listing): {{path}}",
15789
15790
  "update.start": "Checking for updates...",
15790
15791
  "update.success": "Updated from {{from}} to {{to}}",
15791
15792
  "update.components_synced": "Components synced (version {{version}}): {{components}}",
@@ -15907,6 +15908,7 @@ var MESSAGES = {
15907
15908
  "lockfile.component_dir_missing": "컴포넌트 디렉토리 없음: {{path}}",
15908
15909
  "lockfile.hash_failed": "파일 해시 실패: {{path}} — {{error}}",
15909
15910
  "lockfile.entry_added": "잠금 파일 항목 추가: {{path}} ({{component}})",
15911
+ "lockfile.entry_git_excluded": "잠금 파일 항목 제외 (git 목록에 없음): {{path}}",
15910
15912
  "update.start": "업데이트 확인 중...",
15911
15913
  "update.success": "{{from}}에서 {{to}}로 업데이트 완료",
15912
15914
  "update.components_synced": "컴포넌트 동기화 완료 (버전 {{version}}): {{components}}",
@@ -16362,10 +16364,12 @@ function getComponentPath(component) {
16362
16364
 
16363
16365
  // src/core/lockfile.ts
16364
16366
  init_fs();
16367
+ import { execFile } from "node:child_process";
16365
16368
  import { createHash } from "node:crypto";
16366
16369
  import { createReadStream } from "node:fs";
16367
- import { readdir, stat } from "node:fs/promises";
16370
+ import { readdir, realpath, stat } from "node:fs/promises";
16368
16371
  import { join as join7, relative as relative2 } from "node:path";
16372
+ import { promisify } from "node:util";
16369
16373
  var LOCKFILE_NAME = ".omcustom.lock.json";
16370
16374
  var LOCKFILE_VERSION = 1;
16371
16375
  var LOCKFILE_COMPONENTS = [
@@ -16378,6 +16382,87 @@ var LOCKFILE_COMPONENTS = [
16378
16382
  "guides"
16379
16383
  ];
16380
16384
  var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
16385
+ var execFileAsync = promisify(execFile);
16386
+ var GIT_LOCATION_ENV_KEYS = [
16387
+ "GIT_DIR",
16388
+ "GIT_WORK_TREE",
16389
+ "GIT_INDEX_FILE",
16390
+ "GIT_PREFIX",
16391
+ "GIT_COMMON_DIR"
16392
+ ];
16393
+ var GIT_INJECTED_CONFIG_ENV_KEYS = ["GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT"];
16394
+ var GIT_INJECTED_CONFIG_ENV_PREFIXES = ["GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_"];
16395
+ var GIT_REQUIRED_HINT = "build from a git checkout: excluding ignored local files from the lockfile requires git (#1819)";
16396
+ function gitChildEnv() {
16397
+ const env = { ...process.env };
16398
+ for (const key of [...GIT_LOCATION_ENV_KEYS, ...GIT_INJECTED_CONFIG_ENV_KEYS]) {
16399
+ delete env[key];
16400
+ }
16401
+ for (const key of Object.keys(env)) {
16402
+ if (GIT_INJECTED_CONFIG_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
16403
+ delete env[key];
16404
+ }
16405
+ }
16406
+ return env;
16407
+ }
16408
+ async function runGit(targetDir, args, env) {
16409
+ try {
16410
+ const { stdout } = await execFileAsync("git", args, {
16411
+ cwd: targetDir,
16412
+ env,
16413
+ encoding: "utf-8",
16414
+ maxBuffer: 256 * 1024 * 1024
16415
+ });
16416
+ return stdout;
16417
+ } catch (err) {
16418
+ const detail = err instanceof Error ? err.message.trim() : String(err);
16419
+ throw new Error(`git ${args[0]} failed in ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
16420
+ }
16421
+ }
16422
+ function foldCase(nfcPath) {
16423
+ return nfcPath.toLowerCase();
16424
+ }
16425
+ function isGitVisible(visible, relativePath) {
16426
+ if (visible.exact.has(relativePath)) {
16427
+ return true;
16428
+ }
16429
+ const nfc = relativePath.normalize("NFC");
16430
+ if (visible.nfc.has(nfc)) {
16431
+ return true;
16432
+ }
16433
+ return visible.folded?.has(foldCase(nfc)) ?? false;
16434
+ }
16435
+ function stripFinalNewline(output) {
16436
+ return output.endsWith(`
16437
+ `) ? output.slice(0, -1) : output;
16438
+ }
16439
+ async function assertGitWorkTreeRoot(targetDir, env) {
16440
+ const topLevel = stripFinalNewline(await runGit(targetDir, ["rev-parse", "--show-toplevel"], env));
16441
+ let realTop;
16442
+ let realTarget;
16443
+ try {
16444
+ [realTop, realTarget] = await Promise.all([realpath(topLevel), realpath(targetDir)]);
16445
+ } catch (err) {
16446
+ const detail = err instanceof Error ? err.message : String(err);
16447
+ throw new Error(`cannot resolve the git work tree root of ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
16448
+ }
16449
+ if (realTop !== realTarget) {
16450
+ throw new Error(`${targetDir} is not the root of its git work tree (root: ${topLevel}) — ${GIT_REQUIRED_HINT}`);
16451
+ }
16452
+ }
16453
+ async function listGitVisiblePaths(targetDir) {
16454
+ const env = gitChildEnv();
16455
+ await assertGitWorkTreeRoot(targetDir, env);
16456
+ const pathspecs = COMPONENT_PATHS.map(([prefix]) => prefix);
16457
+ const [listing, ignoreCase] = await Promise.all([
16458
+ runGit(targetDir, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspecs], env),
16459
+ runGit(targetDir, ["config", "--type=bool", "--default=false", "--get", "core.ignorecase"], env)
16460
+ ]);
16461
+ const exact = new Set(listing.split("\x00").filter((entry) => entry.length > 0));
16462
+ const nfc = new Set([...exact].map((entry) => entry.normalize("NFC")));
16463
+ const folded = stripFinalNewline(ignoreCase) === "true" ? new Set([...nfc].map(foldCase)) : null;
16464
+ return { exact, nfc, folded };
16465
+ }
16381
16466
  function computeFileHash(filePath) {
16382
16467
  return new Promise((resolve, reject) => {
16383
16468
  const hash = createHash("sha256");
@@ -16463,8 +16548,9 @@ function sortFilesByPath(files) {
16463
16548
  const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
16464
16549
  return Object.fromEntries(sorted.map((key) => [key, files[key]]));
16465
16550
  }
16466
- async function generateLockfile(targetDir, generatorVersion, templateVersion) {
16551
+ async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
16467
16552
  const files = {};
16553
+ const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
16468
16554
  const componentRoots = COMPONENT_PATHS.map(([prefix]) => join7(targetDir, prefix));
16469
16555
  for (const componentRoot of componentRoots) {
16470
16556
  const exists = await fileExists(componentRoot);
@@ -16475,6 +16561,10 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion) {
16475
16561
  const allFiles = await collectFiles(componentRoot, targetDir, false);
16476
16562
  for (const absolutePath of allFiles) {
16477
16563
  const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
16564
+ if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
16565
+ debug("lockfile.entry_git_excluded", { path: relativePath });
16566
+ continue;
16567
+ }
16478
16568
  let hash;
16479
16569
  let size;
16480
16570
  try {
@@ -16535,12 +16625,12 @@ function preserveGeneratedAt(next, previous) {
16535
16625
  }
16536
16626
  return { ...next, generatedAt: previous.generatedAt };
16537
16627
  }
16538
- async function generateAndWriteLockfileForDir(targetDir) {
16628
+ async function generateAndWriteLockfileForDir(targetDir, options = {}) {
16539
16629
  try {
16540
16630
  const packageRoot = getPackageRoot();
16541
16631
  const manifest = await readJsonFile(join7(packageRoot, "templates", "manifest.json"));
16542
16632
  const { version: generatorVersion } = await readJsonFile(join7(packageRoot, "package.json"));
16543
- const generated = await generateLockfile(targetDir, generatorVersion, manifest.version);
16633
+ const generated = await generateLockfile(targetDir, generatorVersion, manifest.version, options);
16544
16634
  const previous = await readLockfile(targetDir);
16545
16635
  const lockfile = preserveGeneratedAt(generated, previous);
16546
16636
  await writeLockfile(targetDir, lockfile);
@@ -20938,11 +21028,30 @@ import { join as join17 } from "node:path";
20938
21028
  // src/cli/serve.ts
20939
21029
  init_home();
20940
21030
  import { spawn } from "node:child_process";
21031
+ import { randomUUID } from "node:crypto";
20941
21032
  import { constants as constants3, existsSync as existsSync3 } from "node:fs";
20942
- import { access, readFile as readFile3, unlink, writeFile as writeFile3 } from "node:fs/promises";
21033
+ import {
21034
+ access,
21035
+ link,
21036
+ readdir as readdir3,
21037
+ readFile as readFile3,
21038
+ readlink,
21039
+ rename as rename2,
21040
+ stat as stat3,
21041
+ unlink,
21042
+ writeFile as writeFile3
21043
+ } from "node:fs/promises";
20943
21044
  import { dirname as dirname7, isAbsolute as isAbsolute4, join as join16 } from "node:path";
21045
+ import { setTimeout as delay } from "node:timers/promises";
20944
21046
  var DEFAULT_PORT = 4321;
20945
21047
  var PID_FILE_NAME = ".omcustom-serve.pid";
21048
+ var STARTING_RECORD = /^starting:(\d+):/;
21049
+ var MAX_CLAIM_ATTEMPTS = 3;
21050
+ var CLAIM_MAX_AGE_MS = 60000;
21051
+ var PARTIAL_RECORD_GRACE_MS = 1e4;
21052
+ var ABSENT_RETRY_DELAY_MS = 10;
21053
+ var HARD_LINK_UNSUPPORTED = new Set(["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
21054
+ var SIBLING_REMNANT = /^\.omcustom-serve\.pid\.(\d+)\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:tmp|taken)$/;
20946
21055
 
20947
21056
  class ServePidFileError extends Error {
20948
21057
  reason;
@@ -20972,6 +21081,51 @@ function findServeBuildDir(projectRoot, options) {
20972
21081
  }
20973
21082
  return null;
20974
21083
  }
21084
+ function parsePidRecord(raw) {
21085
+ const text = raw.trim();
21086
+ const match = STARTING_RECORD.exec(text);
21087
+ const pid = Number(match === null ? text : match[1]);
21088
+ if (!Number.isFinite(pid) || pid <= 0) {
21089
+ return null;
21090
+ }
21091
+ return { pid, starting: match !== null };
21092
+ }
21093
+ function isProcessAlive(pid) {
21094
+ try {
21095
+ process.kill(pid, 0);
21096
+ return true;
21097
+ } catch {
21098
+ return false;
21099
+ }
21100
+ }
21101
+ function errorCode(error) {
21102
+ return error?.code;
21103
+ }
21104
+ async function isOlderThan(path, ms) {
21105
+ try {
21106
+ const { mtimeMs } = await stat3(path);
21107
+ return Date.now() - mtimeMs >= ms;
21108
+ } catch {
21109
+ return false;
21110
+ }
21111
+ }
21112
+ async function isRecordLive(pidFile, record) {
21113
+ if (!isProcessAlive(record.pid)) {
21114
+ return false;
21115
+ }
21116
+ return !record.starting || !await isOlderThan(pidFile, CLAIM_MAX_AGE_MS);
21117
+ }
21118
+ async function readPidFileSettled(pidFile) {
21119
+ try {
21120
+ return await readFile3(pidFile, "utf-8");
21121
+ } catch (error) {
21122
+ if (errorCode(error) !== "ENOENT") {
21123
+ throw error;
21124
+ }
21125
+ }
21126
+ await delay(ABSENT_RETRY_DELAY_MS);
21127
+ return readFile3(pidFile, "utf-8");
21128
+ }
20975
21129
  async function isServeRunning() {
20976
21130
  const pidFile = resolveServePidFile();
20977
21131
  if (pidFile === null) {
@@ -20980,19 +21134,14 @@ async function isServeRunning() {
20980
21134
  return isServeRunningAt(pidFile);
20981
21135
  }
20982
21136
  async function isServeRunningAt(pidFile) {
21137
+ let raw;
20983
21138
  try {
20984
- const raw = await readFile3(pidFile, "utf-8");
20985
- const pid = Number(raw.trim());
20986
- if (!Number.isFinite(pid) || pid <= 0) {
20987
- await cleanupPidFile(pidFile);
20988
- return false;
20989
- }
20990
- process.kill(pid, 0);
20991
- return true;
21139
+ raw = await readPidFileSettled(pidFile);
20992
21140
  } catch {
20993
- await cleanupPidFile(pidFile);
20994
21141
  return false;
20995
21142
  }
21143
+ const record = parsePidRecord(raw);
21144
+ return record !== null && await isRecordLive(pidFile, record);
20996
21145
  }
20997
21146
  async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOpts) {
20998
21147
  const pidFile = resolveServePidFile();
@@ -21011,6 +21160,15 @@ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOp
21011
21160
  } catch (error) {
21012
21161
  throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
21013
21162
  }
21163
+ let claim;
21164
+ try {
21165
+ claim = await claimPidFile(pidFile);
21166
+ } catch (error) {
21167
+ throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
21168
+ }
21169
+ if (claim === null) {
21170
+ return;
21171
+ }
21014
21172
  const child = spawn("node", [join16(buildDir, "index.js")], {
21015
21173
  env: {
21016
21174
  ...process.env,
@@ -21025,38 +21183,217 @@ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOp
21025
21183
  child.on("error", () => {});
21026
21184
  child.unref();
21027
21185
  if (child.pid === undefined) {
21186
+ await removePidFileIf(pidFile, claim);
21028
21187
  return;
21029
21188
  }
21189
+ let recorded;
21030
21190
  try {
21031
- await writeFile3(pidFile, String(child.pid), "utf-8");
21191
+ recorded = await recordServerPid(pidFile, claim, String(child.pid));
21032
21192
  } catch (error) {
21033
21193
  child.kill("SIGTERM");
21194
+ await removePidFileIf(pidFile, claim);
21034
21195
  throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
21035
21196
  }
21197
+ if (!recorded) {
21198
+ child.kill("SIGTERM");
21199
+ }
21200
+ }
21201
+ async function recordServerPid(pidFile, claim, pid) {
21202
+ for (let attempt = 0;attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
21203
+ if (await createPidFileExclusive(pidFile, pid)) {
21204
+ return true;
21205
+ }
21206
+ let current;
21207
+ try {
21208
+ current = await readFile3(pidFile, "utf-8");
21209
+ } catch (error) {
21210
+ if (errorCode(error) === "ENOENT") {
21211
+ continue;
21212
+ }
21213
+ throw error;
21214
+ }
21215
+ if (current !== claim) {
21216
+ return false;
21217
+ }
21218
+ await replacePidFile(pidFile, pid);
21219
+ return true;
21220
+ }
21221
+ return false;
21036
21222
  }
21037
21223
  async function stopServe() {
21038
21224
  const pidFile = resolveServePidFile();
21039
21225
  if (pidFile === null) {
21040
21226
  return false;
21041
21227
  }
21228
+ let raw;
21042
21229
  try {
21043
- const raw = await readFile3(pidFile, "utf-8");
21044
- const pid = Number(raw.trim());
21045
- if (!Number.isFinite(pid) || pid <= 0) {
21046
- await cleanupPidFile(pidFile);
21047
- return false;
21230
+ raw = await readPidFileSettled(pidFile);
21231
+ } catch {
21232
+ return false;
21233
+ }
21234
+ const record = parsePidRecord(raw);
21235
+ if (record === null || record.starting) {
21236
+ return false;
21237
+ }
21238
+ try {
21239
+ process.kill(record.pid, "SIGTERM");
21240
+ } catch {
21241
+ return false;
21242
+ }
21243
+ await removePidFileIf(pidFile, raw);
21244
+ return true;
21245
+ }
21246
+ function siblingPath(pidFile, suffix) {
21247
+ return `${pidFile}.${process.pid}.${randomUUID()}.${suffix}`;
21248
+ }
21249
+ async function claimPidFile(pidFile) {
21250
+ await removeDeadSiblings(pidFile);
21251
+ const claim = `starting:${process.pid}:${randomUUID()}`;
21252
+ for (let attempt = 0;attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
21253
+ if (await createPidFileExclusive(pidFile, claim)) {
21254
+ return claim;
21048
21255
  }
21049
- process.kill(pid, "SIGTERM");
21050
- await cleanupPidFile(pidFile);
21256
+ let raw;
21257
+ try {
21258
+ raw = await readFile3(pidFile, "utf-8");
21259
+ } catch (error) {
21260
+ if (errorCode(error) === "ENOENT") {
21261
+ await removeDanglingLink(pidFile);
21262
+ continue;
21263
+ }
21264
+ throw error;
21265
+ }
21266
+ if (await isRecordHeld(pidFile, raw)) {
21267
+ return null;
21268
+ }
21269
+ await removePidFileIf(pidFile, raw);
21270
+ }
21271
+ return null;
21272
+ }
21273
+ async function isRecordHeld(pidFile, raw) {
21274
+ const record = parsePidRecord(raw);
21275
+ if (record === null) {
21276
+ return isPossiblyPartialRecord(raw) && !await isOlderThan(pidFile, PARTIAL_RECORD_GRACE_MS);
21277
+ }
21278
+ return isRecordLive(pidFile, record);
21279
+ }
21280
+ function isPossiblyPartialRecord(raw) {
21281
+ return "starting:".startsWith(raw) || /^starting:\d*$/.test(raw);
21282
+ }
21283
+ async function removeDeadSiblings(pidFile) {
21284
+ const dir = dirname7(pidFile);
21285
+ let names;
21286
+ try {
21287
+ names = await readdir3(dir);
21288
+ } catch {
21289
+ return;
21290
+ }
21291
+ for (const name of names) {
21292
+ const match = SIBLING_REMNANT.exec(name);
21293
+ if (match !== null && isProcessGone(Number(match[1]))) {
21294
+ await unlinkQuietly(join16(dir, name));
21295
+ }
21296
+ }
21297
+ }
21298
+ function isProcessGone(pid) {
21299
+ try {
21300
+ process.kill(pid, 0);
21301
+ return false;
21302
+ } catch (error) {
21303
+ return errorCode(error) === "ESRCH";
21304
+ }
21305
+ }
21306
+ async function createPidFileExclusive(pidFile, content) {
21307
+ const staged = siblingPath(pidFile, "tmp");
21308
+ try {
21309
+ await writeFile3(staged, content, { encoding: "utf-8", flag: "wx" });
21310
+ await linkExclusive(staged, pidFile, content);
21051
21311
  return true;
21312
+ } catch (error) {
21313
+ if (errorCode(error) === "EEXIST") {
21314
+ return false;
21315
+ }
21316
+ throw error;
21317
+ } finally {
21318
+ await unlinkQuietly(staged);
21319
+ }
21320
+ }
21321
+ async function linkExclusive(source, target, content) {
21322
+ try {
21323
+ await link(source, target);
21324
+ } catch (error) {
21325
+ if (!HARD_LINK_UNSUPPORTED.has(errorCode(error) ?? "")) {
21326
+ throw error;
21327
+ }
21328
+ await writeFile3(target, content, { encoding: "utf-8", flag: "wx" });
21329
+ }
21330
+ }
21331
+ async function replacePidFile(pidFile, content) {
21332
+ const staged = siblingPath(pidFile, "tmp");
21333
+ try {
21334
+ await writeFile3(staged, content, { encoding: "utf-8", flag: "wx" });
21335
+ await rename2(staged, pidFile);
21336
+ } catch (error) {
21337
+ await unlinkQuietly(staged);
21338
+ throw error;
21339
+ }
21340
+ }
21341
+ async function removePidFileIf(pidFile, expected) {
21342
+ await removePidFileWhen(pidFile, async (taken) => {
21343
+ try {
21344
+ return await readFile3(taken, "utf-8") === expected;
21345
+ } catch {
21346
+ return false;
21347
+ }
21348
+ });
21349
+ }
21350
+ async function removeDanglingLink(pidFile) {
21351
+ if (!await isDanglingLink(pidFile)) {
21352
+ return;
21353
+ }
21354
+ await removePidFileWhen(pidFile, isDanglingLink);
21355
+ }
21356
+ async function isDanglingLink(path) {
21357
+ try {
21358
+ await readlink(path);
21052
21359
  } catch {
21053
- await cleanupPidFile(pidFile);
21054
21360
  return false;
21055
21361
  }
21362
+ try {
21363
+ await stat3(path);
21364
+ return false;
21365
+ } catch {
21366
+ return true;
21367
+ }
21368
+ }
21369
+ async function removePidFileWhen(pidFile, isExpected) {
21370
+ const taken = siblingPath(pidFile, "taken");
21371
+ try {
21372
+ await rename2(pidFile, taken);
21373
+ } catch {
21374
+ return;
21375
+ }
21376
+ if (!await isExpected(taken)) {
21377
+ await putBack(taken, pidFile);
21378
+ }
21379
+ await unlinkQuietly(taken);
21380
+ }
21381
+ async function putBack(taken, pidFile) {
21382
+ try {
21383
+ await link(taken, pidFile);
21384
+ return;
21385
+ } catch (error) {
21386
+ if (!HARD_LINK_UNSUPPORTED.has(errorCode(error) ?? "")) {
21387
+ return;
21388
+ }
21389
+ }
21390
+ try {
21391
+ await writeFile3(pidFile, await readFile3(taken, "utf-8"), { encoding: "utf-8", flag: "wx" });
21392
+ } catch {}
21056
21393
  }
21057
- async function cleanupPidFile(pidFile) {
21394
+ async function unlinkQuietly(path) {
21058
21395
  try {
21059
- await unlink(pidFile);
21396
+ await unlink(path);
21060
21397
  } catch {}
21061
21398
  }
21062
21399
 
package/dist/index.js CHANGED
@@ -404,6 +404,7 @@ var MESSAGES = {
404
404
  "lockfile.component_dir_missing": "Component directory missing: {{path}}",
405
405
  "lockfile.hash_failed": "Failed to hash file: {{path}} — {{error}}",
406
406
  "lockfile.entry_added": "Lockfile entry added: {{path}} ({{component}})",
407
+ "lockfile.entry_git_excluded": "Lockfile entry skipped (not in git listing): {{path}}",
407
408
  "update.start": "Checking for updates...",
408
409
  "update.success": "Updated from {{from}} to {{to}}",
409
410
  "update.components_synced": "Components synced (version {{version}}): {{components}}",
@@ -525,6 +526,7 @@ var MESSAGES = {
525
526
  "lockfile.component_dir_missing": "컴포넌트 디렉토리 없음: {{path}}",
526
527
  "lockfile.hash_failed": "파일 해시 실패: {{path}} — {{error}}",
527
528
  "lockfile.entry_added": "잠금 파일 항목 추가: {{path}} ({{component}})",
529
+ "lockfile.entry_git_excluded": "잠금 파일 항목 제외 (git 목록에 없음): {{path}}",
528
530
  "update.start": "업데이트 확인 중...",
529
531
  "update.success": "{{from}}에서 {{to}}로 업데이트 완료",
530
532
  "update.components_synced": "컴포넌트 동기화 완료 (버전 {{version}}): {{components}}",
@@ -2035,10 +2037,12 @@ function getComponentPath(component) {
2035
2037
 
2036
2038
  // src/core/lockfile.ts
2037
2039
  init_fs();
2040
+ import { execFile } from "node:child_process";
2038
2041
  import { createHash } from "node:crypto";
2039
2042
  import { createReadStream } from "node:fs";
2040
- import { readdir, stat } from "node:fs/promises";
2043
+ import { readdir, realpath, stat } from "node:fs/promises";
2041
2044
  import { join as join4, relative as relative2 } from "node:path";
2045
+ import { promisify } from "node:util";
2042
2046
  var LOCKFILE_NAME = ".omcustom.lock.json";
2043
2047
  var LOCKFILE_VERSION = 1;
2044
2048
  var LOCKFILE_COMPONENTS = [
@@ -2051,6 +2055,87 @@ var LOCKFILE_COMPONENTS = [
2051
2055
  "guides"
2052
2056
  ];
2053
2057
  var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
2058
+ var execFileAsync = promisify(execFile);
2059
+ var GIT_LOCATION_ENV_KEYS = [
2060
+ "GIT_DIR",
2061
+ "GIT_WORK_TREE",
2062
+ "GIT_INDEX_FILE",
2063
+ "GIT_PREFIX",
2064
+ "GIT_COMMON_DIR"
2065
+ ];
2066
+ var GIT_INJECTED_CONFIG_ENV_KEYS = ["GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT"];
2067
+ var GIT_INJECTED_CONFIG_ENV_PREFIXES = ["GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_"];
2068
+ var GIT_REQUIRED_HINT = "build from a git checkout: excluding ignored local files from the lockfile requires git (#1819)";
2069
+ function gitChildEnv() {
2070
+ const env = { ...process.env };
2071
+ for (const key of [...GIT_LOCATION_ENV_KEYS, ...GIT_INJECTED_CONFIG_ENV_KEYS]) {
2072
+ delete env[key];
2073
+ }
2074
+ for (const key of Object.keys(env)) {
2075
+ if (GIT_INJECTED_CONFIG_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
2076
+ delete env[key];
2077
+ }
2078
+ }
2079
+ return env;
2080
+ }
2081
+ async function runGit(targetDir, args, env) {
2082
+ try {
2083
+ const { stdout } = await execFileAsync("git", args, {
2084
+ cwd: targetDir,
2085
+ env,
2086
+ encoding: "utf-8",
2087
+ maxBuffer: 256 * 1024 * 1024
2088
+ });
2089
+ return stdout;
2090
+ } catch (err) {
2091
+ const detail = err instanceof Error ? err.message.trim() : String(err);
2092
+ throw new Error(`git ${args[0]} failed in ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
2093
+ }
2094
+ }
2095
+ function foldCase(nfcPath) {
2096
+ return nfcPath.toLowerCase();
2097
+ }
2098
+ function isGitVisible(visible, relativePath) {
2099
+ if (visible.exact.has(relativePath)) {
2100
+ return true;
2101
+ }
2102
+ const nfc = relativePath.normalize("NFC");
2103
+ if (visible.nfc.has(nfc)) {
2104
+ return true;
2105
+ }
2106
+ return visible.folded?.has(foldCase(nfc)) ?? false;
2107
+ }
2108
+ function stripFinalNewline(output) {
2109
+ return output.endsWith(`
2110
+ `) ? output.slice(0, -1) : output;
2111
+ }
2112
+ async function assertGitWorkTreeRoot(targetDir, env) {
2113
+ const topLevel = stripFinalNewline(await runGit(targetDir, ["rev-parse", "--show-toplevel"], env));
2114
+ let realTop;
2115
+ let realTarget;
2116
+ try {
2117
+ [realTop, realTarget] = await Promise.all([realpath(topLevel), realpath(targetDir)]);
2118
+ } catch (err) {
2119
+ const detail = err instanceof Error ? err.message : String(err);
2120
+ throw new Error(`cannot resolve the git work tree root of ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
2121
+ }
2122
+ if (realTop !== realTarget) {
2123
+ throw new Error(`${targetDir} is not the root of its git work tree (root: ${topLevel}) — ${GIT_REQUIRED_HINT}`);
2124
+ }
2125
+ }
2126
+ async function listGitVisiblePaths(targetDir) {
2127
+ const env = gitChildEnv();
2128
+ await assertGitWorkTreeRoot(targetDir, env);
2129
+ const pathspecs = COMPONENT_PATHS.map(([prefix]) => prefix);
2130
+ const [listing, ignoreCase] = await Promise.all([
2131
+ runGit(targetDir, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspecs], env),
2132
+ runGit(targetDir, ["config", "--type=bool", "--default=false", "--get", "core.ignorecase"], env)
2133
+ ]);
2134
+ const exact = new Set(listing.split("\x00").filter((entry) => entry.length > 0));
2135
+ const nfc = new Set([...exact].map((entry) => entry.normalize("NFC")));
2136
+ const folded = stripFinalNewline(ignoreCase) === "true" ? new Set([...nfc].map(foldCase)) : null;
2137
+ return { exact, nfc, folded };
2138
+ }
2054
2139
  function computeFileHash(filePath) {
2055
2140
  return new Promise((resolve, reject) => {
2056
2141
  const hash = createHash("sha256");
@@ -2136,8 +2221,9 @@ function sortFilesByPath(files) {
2136
2221
  const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2137
2222
  return Object.fromEntries(sorted.map((key) => [key, files[key]]));
2138
2223
  }
2139
- async function generateLockfile(targetDir, generatorVersion, templateVersion) {
2224
+ async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
2140
2225
  const files = {};
2226
+ const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
2141
2227
  const componentRoots = COMPONENT_PATHS.map(([prefix]) => join4(targetDir, prefix));
2142
2228
  for (const componentRoot of componentRoots) {
2143
2229
  const exists = await fileExists(componentRoot);
@@ -2148,6 +2234,10 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion) {
2148
2234
  const allFiles = await collectFiles(componentRoot, targetDir, false);
2149
2235
  for (const absolutePath of allFiles) {
2150
2236
  const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
2237
+ if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
2238
+ debug("lockfile.entry_git_excluded", { path: relativePath });
2239
+ continue;
2240
+ }
2151
2241
  let hash;
2152
2242
  let size;
2153
2243
  try {
@@ -2208,12 +2298,12 @@ function preserveGeneratedAt(next, previous) {
2208
2298
  }
2209
2299
  return { ...next, generatedAt: previous.generatedAt };
2210
2300
  }
2211
- async function generateAndWriteLockfileForDir(targetDir) {
2301
+ async function generateAndWriteLockfileForDir(targetDir, options = {}) {
2212
2302
  try {
2213
2303
  const packageRoot = getPackageRoot();
2214
2304
  const manifest = await readJsonFile(join4(packageRoot, "templates", "manifest.json"));
2215
2305
  const { version: generatorVersion } = await readJsonFile(join4(packageRoot, "package.json"));
2216
- const generated = await generateLockfile(targetDir, generatorVersion, manifest.version);
2306
+ const generated = await generateLockfile(targetDir, generatorVersion, manifest.version, options);
2217
2307
  const previous = await readLockfile(targetDir);
2218
2308
  const lockfile = preserveGeneratedAt(generated, previous);
2219
2309
  await writeLockfile(targetDir, lockfile);
@@ -2853,7 +2943,7 @@ var package_default = {
2853
2943
  workspaces: [
2854
2944
  "packages/*"
2855
2945
  ],
2856
- version: "1.1.102",
2946
+ version: "1.1.103",
2857
2947
  description: "Batteries-included agent harness for Claude Code",
2858
2948
  type: "module",
2859
2949
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.102",
6
+ "version": "1.1.103",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -56,7 +56,7 @@ ecomode 활성 시 자동으로 concise 모드로 전환 (R013).
56
56
 
57
57
  ## 위임 모델 (R010)
58
58
 
59
- 오케스트레이터(메인 대화)는 파일 수정 금지 → 서브에이전트에 위임. Agent tool 호출 시 항상 `mode: "bypassPermissions"` 명시.
59
+ 오케스트레이터(메인 대화)는 파일 수정 금지 → 서브에이전트에 위임. CC < 2.1.212에서는 Agent 도구 호출 시 `mode: "bypassPermissions"`가 필요하므로 호환을 위해 명시합니다. 2.1.212+에서는 `mode`가 무시되고 서브에이전트가 부모 세션의 권한 모드를 상속합니다(frontmatter `permissionMode`로 조정 가능). 무인 실행 전 유효 모드는 R010 「Universal bypassPermissions」 절차로 실측합니다.
60
60
 
61
61
  ## 작업 완료 검증 (R020)
62
62