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.
- package/dist/cli/index.js +363 -26
- package/dist/index.js +95 -5
- package/package.json +1 -1
- package/templates/.claude/output-styles/korean-engineer.md +1 -1
- package/templates/.claude/rules/MUST-agent-design.md +2 -2
- package/templates/.claude/rules/MUST-agent-teams.md +13 -2
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +41 -29
- package/templates/.claude/rules/MUST-permissions.md +12 -9
- package/templates/.claude/skills/action-validator/SKILL.md +2 -2
- package/templates/.claude/skills/adaptive-harness/SKILL.md +2 -2
- package/templates/.claude/skills/adversarial-review/SKILL.md +1 -1
- package/templates/.claude/skills/agent-eval-framework/SKILL.md +2 -2
- package/templates/.claude/skills/de-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/deep-plan/SKILL.md +4 -4
- package/templates/.claude/skills/deep-verify/SKILL.md +3 -3
- package/templates/.claude/skills/design-shotgun/SKILL.md +1 -1
- package/templates/.claude/skills/dev-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/dev-review/SKILL.md +2 -2
- package/templates/.claude/skills/evaluator-optimizer/SKILL.md +1 -1
- package/templates/.claude/skills/fsd/SKILL.md +3 -4
- package/templates/.claude/skills/harness-eval/SKILL.md +1 -1
- package/templates/.claude/skills/harness-synthesizer/SKILL.md +1 -1
- package/templates/.claude/skills/homework/SKILL.md +3 -3
- package/templates/.claude/skills/idea/SKILL.md +4 -2
- package/templates/.claude/skills/instinct-extractor/SKILL.md +8 -8
- package/templates/.claude/skills/intent-detection/SKILL.md +1 -1
- package/templates/.claude/skills/multi-model-verification/SKILL.md +2 -2
- package/templates/.claude/skills/omcustom-release-notes/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +2 -2
- package/templates/.claude/skills/post-release-followup/SKILL.md +2 -2
- package/templates/.claude/skills/professor-triage/SKILL.md +10 -10
- package/templates/.claude/skills/profile/SKILL.md +4 -4
- package/templates/.claude/skills/qa-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/release-plan/SKILL.md +1 -1
- package/templates/.claude/skills/research/SKILL.md +8 -6
- package/templates/.claude/skills/result-aggregation/SKILL.md +1 -1
- package/templates/.claude/skills/roundtable-debate/SKILL.md +2 -2
- package/templates/.claude/skills/scout/SKILL.md +3 -8
- package/templates/.claude/skills/sec-agentshield-wrapper/SKILL.md +1 -1
- package/templates/.claude/skills/secretary-routing/SKILL.md +2 -2
- package/templates/.claude/skills/skill-extractor/SKILL.md +1 -1
- package/templates/.claude/skills/structured-dev-cycle/SKILL.md +1 -1
- package/templates/.claude/skills/task-decomposition/SKILL.md +1 -1
- package/templates/.claude/skills/worker-reviewer-pipeline/SKILL.md +5 -1
- package/templates/guides/claude-code/15-version-compatibility.md +1 -1
- package/templates/guides/deep-plan/phases.md +11 -9
- package/templates/guides/harness-engineering/README.md +2 -1
- package/templates/guides/professor-triage/README.md +1 -1
- package/templates/guides/professor-triage/phases.md +1 -1
- package/templates/manifest.json +1 -1
- 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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
21044
|
-
|
|
21045
|
-
|
|
21046
|
-
|
|
21047
|
-
|
|
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
|
-
|
|
21050
|
-
|
|
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
|
|
21394
|
+
async function unlinkQuietly(path) {
|
|
21058
21395
|
try {
|
|
21059
|
-
await unlink(
|
|
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.
|
|
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
|
@@ -56,7 +56,7 @@ ecomode 활성 시 자동으로 concise 모드로 전환 (R013).
|
|
|
56
56
|
|
|
57
57
|
## 위임 모델 (R010)
|
|
58
58
|
|
|
59
|
-
오케스트레이터(메인 대화)는 파일 수정 금지 → 서브에이전트에 위임. Agent
|
|
59
|
+
오케스트레이터(메인 대화)는 파일 수정 금지 → 서브에이전트에 위임. CC < 2.1.212에서는 Agent 도구 호출 시 `mode: "bypassPermissions"`가 필요하므로 호환을 위해 명시합니다. 2.1.212+에서는 `mode`가 무시되고 서브에이전트가 부모 세션의 권한 모드를 상속합니다(frontmatter `permissionMode`로 조정 가능). 무인 실행 전 유효 모드는 R010 「Universal bypassPermissions」 절차로 실측합니다.
|
|
60
60
|
|
|
61
61
|
## 작업 완료 검증 (R020)
|
|
62
62
|
|