pai-zero 0.13.0 → 0.13.1

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/bin/pai.js CHANGED
@@ -54,6 +54,13 @@ var init_logger = __esm({
54
54
  });
55
55
 
56
56
  // src/core/config.ts
57
+ var config_exports = {};
58
+ __export(config_exports, {
59
+ createDefaultConfig: () => createDefaultConfig,
60
+ loadConfig: () => loadConfig,
61
+ normalizeMode: () => normalizeMode,
62
+ saveConfig: () => saveConfig
63
+ });
57
64
  import path from "path";
58
65
  import { createRequire } from "module";
59
66
  import fs from "fs-extra";
@@ -3598,169 +3605,722 @@ var init_detector = __esm({
3598
3605
  }
3599
3606
  });
3600
3607
 
3601
- // src/stages/environment/doctor.ts
3602
- var doctor_exports = {};
3603
- __export(doctor_exports, {
3604
- runDoctor: () => runDoctor
3605
- });
3606
- import { join as join6 } from "path";
3607
- import { homedir } from "os";
3608
+ // src/core/roboco.ts
3609
+ import os2 from "os";
3610
+ import path5 from "path";
3608
3611
  import fs10 from "fs-extra";
3609
- async function runDoctor() {
3610
- section("PAI Doctor \u2014 \uD658\uACBD \uC9C4\uB2E8");
3611
- const checks = [];
3612
- const nodeVersion = process.version;
3613
- const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
3614
- checks.push({
3615
- label: "Node.js \uBC84\uC804",
3616
- ok: nodeMajor >= 20,
3617
- detail: `${nodeVersion} ${nodeMajor >= 20 ? "(>=20 OK)" : "(\uC5C5\uADF8\uB808\uC774\uB4DC \uD544\uC694)"}`,
3618
- fix: nodeMajor < 20 ? "Node.js 20 \uC774\uC0C1\uC73C\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694" : void 0
3619
- });
3620
- const claudeCheck = await checkCommand("claude", ["--version"]);
3621
- checks.push({
3622
- label: "Claude Code CLI",
3623
- ok: claudeCheck.ok,
3624
- detail: claudeCheck.detail,
3625
- fix: claudeCheck.ok ? void 0 : "npm install -g @anthropic-ai/claude-code"
3626
- });
3627
- const globalConfigPath = join6(homedir(), ".pai", "config.json");
3628
- const hasGlobalConfig = await fs10.pathExists(globalConfigPath);
3629
- checks.push({
3630
- label: "\uAE00\uB85C\uBC8C \uC124\uC815",
3631
- ok: true,
3632
- detail: hasGlobalConfig ? globalConfigPath : "\uBBF8\uC124\uC815 (\uAE30\uBCF8\uAC12 \uC0AC\uC6A9)"
3633
- });
3634
- let hasSdk = false;
3612
+ function robocoPath(cwd) {
3613
+ return path5.join(cwd, ROBOCO_PATH);
3614
+ }
3615
+ async function loadRobocoState(cwd) {
3616
+ const p = robocoPath(cwd);
3617
+ if (!await fs10.pathExists(p)) return freshState();
3635
3618
  try {
3636
- await import("@anthropic-ai/claude-agent-sdk");
3637
- hasSdk = true;
3619
+ const raw = await fs10.readJson(p);
3620
+ if (raw?.version !== "2.0") return freshState();
3621
+ return raw;
3638
3622
  } catch {
3639
- }
3640
- checks.push({
3641
- label: "Agent SDK",
3642
- ok: true,
3643
- // optional이므로 항상 OK
3644
- detail: hasSdk ? "\uC124\uCE58\uB428 (AI \uAE30\uB2A5 \uD65C\uC131\uD654)" : "\uBBF8\uC124\uCE58 (\uC815\uC801 \uBD84\uC11D \uBAA8\uB4DC)"
3645
- });
3646
- console.log("");
3647
- let passed = 0;
3648
- for (const check of checks) {
3649
- const icon = check.ok ? "\u2713" : "\u2717";
3650
- const pad = " ".repeat(Math.max(1, 20 - check.label.length));
3651
- if (check.ok) {
3652
- success(`${icon} ${check.label}${pad}${check.detail}`);
3653
- passed++;
3654
- } else {
3655
- error(`${icon} ${check.label}${pad}${check.detail}`);
3656
- if (check.fix) {
3657
- info(` \u2192 ${check.fix}`);
3658
- }
3659
- }
3660
- }
3661
- console.log("");
3662
- info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
3663
- if (passed < checks.length) {
3664
- process.exitCode = 1;
3623
+ return freshState();
3665
3624
  }
3666
3625
  }
3667
- async function checkCommand(cmd, args) {
3626
+ async function saveRobocoState(cwd, state) {
3627
+ const p = robocoPath(cwd);
3628
+ await fs10.ensureDir(path5.dirname(p));
3629
+ await fs10.writeJson(p, state, { spaces: 2 });
3630
+ }
3631
+ function freshState() {
3632
+ return {
3633
+ version: "2.0",
3634
+ currentStage: "environment",
3635
+ stageHistory: [],
3636
+ lock: null,
3637
+ gates: {}
3638
+ };
3639
+ }
3640
+ function isProcessAlive(pid) {
3668
3641
  try {
3669
- const { execa } = await import("execa");
3670
- const { stdout } = await execa(cmd, args, { timeout: 1e4 });
3671
- return { ok: true, detail: stdout.trim().split("\n")[0] ?? "ok" };
3642
+ process.kill(pid, 0);
3643
+ return true;
3672
3644
  } catch {
3673
- return { ok: false, detail: "not found" };
3645
+ return false;
3674
3646
  }
3675
3647
  }
3676
- var init_doctor = __esm({
3677
- "src/stages/environment/doctor.ts"() {
3678
- "use strict";
3679
- init_ui();
3680
- }
3681
- });
3682
-
3683
- // src/utils/github-fetch.ts
3684
- import path5 from "path";
3685
- import fs11 from "fs-extra";
3686
- async function httpGet(url, timeoutMs, accept) {
3687
- const controller = new AbortController();
3688
- const timer = setTimeout(() => controller.abort(), timeoutMs);
3689
- try {
3690
- return await fetch(url, {
3691
- signal: controller.signal,
3692
- headers: { "Accept": accept, "User-Agent": "pai-zero" }
3693
- });
3694
- } finally {
3695
- clearTimeout(timer);
3696
- }
3648
+ function isLockStale(lock, now2 = /* @__PURE__ */ new Date()) {
3649
+ if (!isProcessAlive(lock.pid)) return true;
3650
+ if (new Date(lock.expiresAt).getTime() < now2.getTime()) return true;
3651
+ return false;
3697
3652
  }
3698
- async function listDir(repo, ref, dirPath, timeoutMs) {
3699
- const url = `https://api.github.com/repos/${repo}/contents/${encodeURI(dirPath)}?ref=${encodeURIComponent(ref)}`;
3700
- const res = await httpGet(url, timeoutMs, "application/vnd.github+json");
3701
- if (!res.ok) {
3702
- throw new Error(`GitHub API ${res.status} ${res.statusText} (${url})`);
3703
- }
3704
- const data = await res.json();
3705
- if (!Array.isArray(data)) {
3706
- throw new Error(`Expected directory, got single entry at ${dirPath}`);
3653
+ async function acquireLock(cwd, stage, command, ttlMs = DEFAULT_TTL_MS) {
3654
+ const state = await loadRobocoState(cwd);
3655
+ if (state.lock) {
3656
+ if (!isLockStale(state.lock)) {
3657
+ throw new RobocoLockError(formatLockError(state.lock), state.lock);
3658
+ }
3707
3659
  }
3708
- return data;
3660
+ const now2 = /* @__PURE__ */ new Date();
3661
+ const newLock = {
3662
+ pid: process.pid,
3663
+ stage,
3664
+ command,
3665
+ acquiredAt: now2.toISOString(),
3666
+ expiresAt: new Date(now2.getTime() + ttlMs).toISOString(),
3667
+ host: os2.hostname()
3668
+ };
3669
+ state.lock = newLock;
3670
+ await saveRobocoState(cwd, state);
3671
+ return state;
3709
3672
  }
3710
- async function downloadFile(downloadUrl, destPath, timeoutMs) {
3711
- const res = await httpGet(downloadUrl, timeoutMs, "*/*");
3712
- if (!res.ok) {
3713
- throw new Error(`Download failed ${res.status} ${res.statusText} (${downloadUrl})`);
3673
+ async function releaseLock(cwd) {
3674
+ const state = await loadRobocoState(cwd);
3675
+ if (state.lock && state.lock.pid === process.pid) {
3676
+ state.lock = null;
3677
+ await saveRobocoState(cwd, state);
3714
3678
  }
3715
- const buf = Buffer.from(await res.arrayBuffer());
3716
- await fs11.ensureDir(path5.dirname(destPath));
3717
- await fs11.writeFile(destPath, buf);
3718
3679
  }
3719
- async function fetchGithubDir(opts) {
3720
- const {
3721
- repo,
3722
- ref = "main",
3723
- srcPath,
3724
- destDir,
3725
- overwrite = false,
3726
- timeoutMs = 1e4
3727
- } = opts;
3728
- const result = { written: [], skipped: [], errors: [] };
3729
- async function walk(currentSrc, currentDest) {
3730
- let entries;
3731
- try {
3732
- entries = await listDir(repo, ref, currentSrc, timeoutMs);
3733
- } catch (err) {
3734
- const msg = err instanceof Error ? err.message : String(err);
3735
- result.errors.push({ path: currentSrc, error: msg });
3736
- return;
3680
+ function formatLockError(lock) {
3681
+ const started = new Date(lock.acquiredAt);
3682
+ const mins = Math.round((Date.now() - started.getTime()) / 6e4);
3683
+ return [
3684
+ "\uB2E4\uB978 PAI \uBA85\uB839\uC774 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4",
3685
+ ` stage: ${lock.stage}`,
3686
+ ` command: ${lock.command}`,
3687
+ ` \uC2DC\uC791: ${mins}\uBD84 \uC804 (PID ${lock.pid}${lock.host ? ", host " + lock.host : ""})`,
3688
+ " \uB300\uAE30\uD558\uC9C0 \uC54A\uACE0 \uC989\uC2DC \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC644\uB8CC \uD6C4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694."
3689
+ ].join("\n");
3690
+ }
3691
+ async function markStageStart(cwd, stage, command) {
3692
+ const state = await loadRobocoState(cwd);
3693
+ state.currentStage = stage;
3694
+ state.stageHistory.push({
3695
+ stage,
3696
+ status: "in_progress",
3697
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3698
+ by: command
3699
+ });
3700
+ await saveRobocoState(cwd, state);
3701
+ }
3702
+ async function markStageEnd(cwd, stage, success2, reason) {
3703
+ const state = await loadRobocoState(cwd);
3704
+ for (let i = state.stageHistory.length - 1; i >= 0; i--) {
3705
+ const entry = state.stageHistory[i];
3706
+ if (entry.stage === stage && entry.status === "in_progress") {
3707
+ entry.status = success2 ? "done" : "failed";
3708
+ entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3709
+ if (reason) entry.reason = reason;
3710
+ break;
3737
3711
  }
3738
- for (const entry of entries) {
3739
- const relName = entry.name;
3740
- if (entry.type === "dir") {
3741
- await walk(`${currentSrc}/${relName}`, path5.join(currentDest, relName));
3742
- } else if (entry.type === "file") {
3743
- const destFile = path5.join(currentDest, relName);
3744
- if (!overwrite && await fs11.pathExists(destFile)) {
3745
- result.skipped.push(destFile);
3746
- continue;
3747
- }
3748
- if (!entry.download_url) {
3749
- result.errors.push({ path: entry.path, error: "no download_url" });
3750
- continue;
3751
- }
3752
- try {
3753
- await downloadFile(entry.download_url, destFile, timeoutMs);
3754
- result.written.push(destFile);
3755
- } catch (err) {
3756
- const msg = err instanceof Error ? err.message : String(err);
3757
- result.errors.push({ path: entry.path, error: msg });
3758
- }
3712
+ }
3713
+ await saveRobocoState(cwd, state);
3714
+ }
3715
+ async function saveGateResult(cwd, key, result) {
3716
+ const state = await loadRobocoState(cwd);
3717
+ state.gates[key] = result;
3718
+ await saveRobocoState(cwd, state);
3719
+ }
3720
+ async function withRoboco(cwd, stage, command, options, fn) {
3721
+ await acquireLock(cwd, stage, command, options.ttlMs);
3722
+ try {
3723
+ if (options.gate && !options.force) {
3724
+ const result = await options.gate(cwd);
3725
+ await saveGateResult(cwd, `${stage}.entry`, result);
3726
+ if (!result.passed && !options.skipGates) {
3727
+ await markStageEnd(cwd, stage, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${result.name}`);
3728
+ throw new GateFailedError(result);
3729
+ }
3730
+ if (!result.passed && options.skipGates) {
3759
3731
  }
3760
3732
  }
3733
+ await markStageStart(cwd, stage, command);
3734
+ const value = await fn();
3735
+ await markStageEnd(cwd, stage, true);
3736
+ return value;
3737
+ } catch (err) {
3738
+ if (!(err instanceof GateFailedError)) {
3739
+ const msg = err instanceof Error ? err.message : String(err);
3740
+ await markStageEnd(cwd, stage, false, msg).catch(() => {
3741
+ });
3742
+ }
3743
+ throw err;
3744
+ } finally {
3745
+ await releaseLock(cwd).catch(() => {
3746
+ });
3761
3747
  }
3762
- await walk(srcPath, destDir);
3763
- return result;
3748
+ }
3749
+ async function syncHandoff(cwd) {
3750
+ const handoffPath = path5.join(cwd, "handoff.md");
3751
+ if (!await fs10.pathExists(handoffPath)) return;
3752
+ const state = await loadRobocoState(cwd);
3753
+ const block = buildHandoffBlock(state);
3754
+ const content = await fs10.readFile(handoffPath, "utf8");
3755
+ const startIdx = content.indexOf(HANDOFF_START);
3756
+ const endIdx = content.indexOf(HANDOFF_END);
3757
+ let next;
3758
+ if (startIdx >= 0 && endIdx > startIdx) {
3759
+ next = content.slice(0, startIdx) + block + content.slice(endIdx + HANDOFF_END.length);
3760
+ } else {
3761
+ const sep = content.endsWith("\n") ? "\n" : "\n\n";
3762
+ next = content + sep + block + "\n";
3763
+ }
3764
+ await fs10.writeFile(handoffPath, next);
3765
+ }
3766
+ function buildHandoffBlock(state) {
3767
+ const lines = [HANDOFF_START, "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)", ""];
3768
+ const done = state.stageHistory.filter((h) => h.status === "done");
3769
+ const inProgress = state.stageHistory.filter((h) => h.status === "in_progress");
3770
+ const failed = state.stageHistory.filter((h) => h.status === "failed");
3771
+ if (inProgress.length > 0) {
3772
+ lines.push("### \uC9C4\uD589 \uC911");
3773
+ for (const h of inProgress) {
3774
+ lines.push(`- [ ] ${h.stage} (${h.by}, \uC2DC\uC791 ${fmtDate(h.startedAt)})`);
3775
+ }
3776
+ lines.push("");
3777
+ }
3778
+ if (failed.length > 0) {
3779
+ lines.push("### \uC2E4\uD328");
3780
+ for (const h of failed) {
3781
+ lines.push(`- [x] ${h.stage} \u2014 ${h.reason ?? "\uC6D0\uC778 \uBD88\uBA85"} (${fmtDate(h.startedAt)})`);
3782
+ }
3783
+ lines.push("");
3784
+ }
3785
+ if (done.length > 0) {
3786
+ lines.push("### \uC644\uB8CC");
3787
+ for (const h of done) {
3788
+ lines.push(`- [x] ${h.stage} (${h.by}, ${fmtDate(h.endedAt ?? h.startedAt)})`);
3789
+ }
3790
+ lines.push("");
3791
+ }
3792
+ lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
3793
+ lines.push(HANDOFF_END);
3794
+ return lines.join("\n");
3795
+ }
3796
+ function fmtDate(iso) {
3797
+ try {
3798
+ const d = new Date(iso);
3799
+ return d.toISOString().slice(0, 16).replace("T", " ");
3800
+ } catch {
3801
+ return iso;
3802
+ }
3803
+ }
3804
+ var ROBOCO_PATH, DEFAULT_TTL_MS, RobocoLockError, GateFailedError, HANDOFF_START, HANDOFF_END;
3805
+ var init_roboco = __esm({
3806
+ "src/core/roboco.ts"() {
3807
+ "use strict";
3808
+ ROBOCO_PATH = path5.join(".pai", "roboco.json");
3809
+ DEFAULT_TTL_MS = 10 * 60 * 1e3;
3810
+ RobocoLockError = class extends Error {
3811
+ constructor(message, holder) {
3812
+ super(message);
3813
+ this.holder = holder;
3814
+ this.name = "RobocoLockError";
3815
+ }
3816
+ holder;
3817
+ };
3818
+ GateFailedError = class extends Error {
3819
+ constructor(result) {
3820
+ super(`\uAC8C\uC774\uD2B8 \uC2E4\uD328 (${result.name}): ${result.violations.length}\uAC74 \uC704\uBC18`);
3821
+ this.result = result;
3822
+ this.name = "GateFailedError";
3823
+ }
3824
+ result;
3825
+ };
3826
+ HANDOFF_START = "<!-- roboco:start -->";
3827
+ HANDOFF_END = "<!-- roboco:end -->";
3828
+ }
3829
+ });
3830
+
3831
+ // src/core/gates.ts
3832
+ var gates_exports = {};
3833
+ __export(gates_exports, {
3834
+ STAGE_GATES: () => STAGE_GATES,
3835
+ countBusinessRules: () => countBusinessRules,
3836
+ countDomainObjects: () => countDomainObjects,
3837
+ countEndpoints: () => countEndpoints,
3838
+ designEntryGate: () => designEntryGate,
3839
+ evaluationEntryGate: () => evaluationEntryGate,
3840
+ executionEntryGate: () => executionEntryGate,
3841
+ validateEdges: () => validateEdges,
3842
+ validationEntryGate: () => validationEntryGate
3843
+ });
3844
+ import path6 from "path";
3845
+ import fs11 from "fs-extra";
3846
+ function now() {
3847
+ return (/* @__PURE__ */ new Date()).toISOString();
3848
+ }
3849
+ function makeResult(name, violations) {
3850
+ return {
3851
+ name,
3852
+ passed: violations.length === 0,
3853
+ checkedAt: now(),
3854
+ violations
3855
+ };
3856
+ }
3857
+ async function designEntryGate(cwd) {
3858
+ const violations = [];
3859
+ const claudeMd = path6.join(cwd, "CLAUDE.md");
3860
+ if (!await fs11.pathExists(claudeMd)) {
3861
+ violations.push({
3862
+ rule: "claude-md-exists",
3863
+ message: "CLAUDE.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
3864
+ location: "CLAUDE.md"
3865
+ });
3866
+ }
3867
+ const configJson = path6.join(cwd, ".pai", "config.json");
3868
+ if (!await fs11.pathExists(configJson)) {
3869
+ violations.push({
3870
+ rule: "pai-config-exists",
3871
+ message: "PAI \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
3872
+ location: ".pai/config.json"
3873
+ });
3874
+ }
3875
+ return makeResult("design.entry", violations);
3876
+ }
3877
+ async function executionEntryGate(cwd) {
3878
+ const violations = [];
3879
+ const openspec = path6.join(cwd, "docs", "openspec.md");
3880
+ const hasOpenspec = await fs11.pathExists(openspec);
3881
+ if (!hasOpenspec) {
3882
+ violations.push({
3883
+ rule: "openspec-exists",
3884
+ message: "docs/openspec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3885
+ location: "docs/openspec.md"
3886
+ });
3887
+ } else {
3888
+ const content = await fs11.readFile(openspec, "utf8");
3889
+ const endpointCount = countEndpoints(content);
3890
+ if (endpointCount < 1) {
3891
+ violations.push({
3892
+ rule: "openspec-endpoints-min",
3893
+ message: `openspec.md\uC5D0 \uC815\uC758\uB41C API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694. /pai design \uC2E4\uD589 \uAD8C\uC7A5.`,
3894
+ location: "docs/openspec.md \xA7 5. API \uC5D4\uB4DC\uD3EC\uC778\uD2B8"
3895
+ });
3896
+ }
3897
+ }
3898
+ const omc = path6.join(cwd, ".pai", "omc.md");
3899
+ if (await fs11.pathExists(omc)) {
3900
+ const content = await fs11.readFile(omc, "utf8");
3901
+ const domainCount = countDomainObjects(content);
3902
+ if (domainCount < 1) {
3903
+ violations.push({
3904
+ rule: "omc-domain-min",
3905
+ message: "omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
3906
+ location: ".pai/omc.md \xA7 \uB3C4\uBA54\uC778 \uAC1D\uCCB4"
3907
+ });
3908
+ }
3909
+ }
3910
+ const edgeViolations = await validateEdges(cwd);
3911
+ for (const ev of edgeViolations) {
3912
+ violations.push({
3913
+ rule: `edge-${ev.edge.from}-${ev.edge.to}`,
3914
+ message: ev.message,
3915
+ location: ev.edge.path
3916
+ });
3917
+ }
3918
+ return makeResult("execution.entry", violations);
3919
+ }
3920
+ async function validationEntryGate(cwd) {
3921
+ const violations = [];
3922
+ const srcDir = path6.join(cwd, "src");
3923
+ if (!await fs11.pathExists(srcDir)) {
3924
+ violations.push({
3925
+ rule: "src-dir-exists",
3926
+ message: "src/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3927
+ location: "src/"
3928
+ });
3929
+ } else {
3930
+ const codeFileCount = await countCodeFiles(srcDir);
3931
+ if (codeFileCount === 0) {
3932
+ violations.push({
3933
+ rule: "code-files-min",
3934
+ message: "src/ \uB0B4\uBD80\uC5D0 \uCF54\uB4DC \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
3935
+ location: "src/"
3936
+ });
3937
+ }
3938
+ }
3939
+ const edgeViolations = await validateEdges(cwd);
3940
+ for (const ev of edgeViolations) {
3941
+ violations.push({
3942
+ rule: `edge-${ev.edge.from}-${ev.edge.to}`,
3943
+ message: ev.message,
3944
+ location: ev.edge.path
3945
+ });
3946
+ }
3947
+ return makeResult("validation.entry", violations);
3948
+ }
3949
+ async function evaluationEntryGate(cwd) {
3950
+ const violations = [];
3951
+ const state = await loadRobocoState(cwd);
3952
+ const recentValidation = [...state.stageHistory].reverse().find((h) => h.stage === "validation");
3953
+ if (!recentValidation) {
3954
+ violations.push({
3955
+ rule: "validation-executed",
3956
+ message: "validation \uB2E8\uACC4\uAC00 \uD55C \uBC88\uB3C4 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai test`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
3957
+ });
3958
+ } else if (recentValidation.status !== "done") {
3959
+ violations.push({
3960
+ rule: "validation-passed",
3961
+ message: `\uCD5C\uADFC validation\uC774 ${recentValidation.status} \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD14C\uC2A4\uD2B8\uB97C \uC131\uACF5\uC2DC\uD0A8 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.`
3962
+ });
3963
+ } else {
3964
+ const endedAt = recentValidation.endedAt ?? recentValidation.startedAt;
3965
+ const age = Date.now() - new Date(endedAt).getTime();
3966
+ if (age > EVAL_GATE_WINDOW_MS) {
3967
+ const mins = Math.round(age / 6e4);
3968
+ violations.push({
3969
+ rule: "validation-recent",
3970
+ message: `\uCD5C\uADFC validation \uD1B5\uACFC\uAC00 ${mins}\uBD84 \uC804\uC785\uB2C8\uB2E4 (1\uC2DC\uAC04 \uC774\uB0B4\uC5EC\uC57C \uD568). --force\uB85C \uC6B0\uD68C \uAC00\uB2A5.`
3971
+ });
3972
+ }
3973
+ }
3974
+ return makeResult("evaluation.entry", violations);
3975
+ }
3976
+ function countEndpoints(openspecContent) {
3977
+ const lines = openspecContent.split("\n");
3978
+ let inTable = false;
3979
+ let count = 0;
3980
+ for (const rawLine of lines) {
3981
+ const line = rawLine.trim();
3982
+ if (/^##\s*5\.|^##\s*API|API 엔드포인트/i.test(line)) {
3983
+ inTable = true;
3984
+ continue;
3985
+ }
3986
+ if (!inTable) continue;
3987
+ if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
3988
+ inTable = false;
3989
+ continue;
3990
+ }
3991
+ if (/^\|\s*Method/i.test(line)) continue;
3992
+ if (/^\|\s*[-:]+\s*\|/.test(line)) continue;
3993
+ const m = line.match(/^\|\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\|\s*(\/\S+)\s*\|/i);
3994
+ if (m) count++;
3995
+ }
3996
+ return count;
3997
+ }
3998
+ function countDomainObjects(omcContent) {
3999
+ const lines = omcContent.split("\n");
4000
+ let count = 0;
4001
+ let inDomainSection = false;
4002
+ for (const rawLine of lines) {
4003
+ const line = rawLine.trim();
4004
+ if (/^##\s*도메인 객체|^##\s*Domain Objects/i.test(line)) {
4005
+ inDomainSection = true;
4006
+ continue;
4007
+ }
4008
+ if (!inDomainSection) continue;
4009
+ if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
4010
+ inDomainSection = false;
4011
+ continue;
4012
+ }
4013
+ if (/^###\s+\w/.test(line)) count++;
4014
+ }
4015
+ return count;
4016
+ }
4017
+ async function validateEdges(cwd) {
4018
+ const config = await loadConfig(cwd);
4019
+ if (!config?.edges || config.edges.length === 0) return [];
4020
+ const violations = [];
4021
+ for (const edge of config.edges) {
4022
+ if (edge.path) {
4023
+ const fullPath = path6.join(cwd, edge.path);
4024
+ if (!await fs11.pathExists(fullPath)) {
4025
+ violations.push({
4026
+ edge,
4027
+ message: `${edge.from}\u2192${edge.to} \uACC4\uC57D \uD30C\uC77C \uC5C6\uC74C: ${edge.path}`
4028
+ });
4029
+ continue;
4030
+ }
4031
+ }
4032
+ switch (edge.via) {
4033
+ case "specFile":
4034
+ case "endpoints": {
4035
+ const specPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, "docs", "openspec.md");
4036
+ if (await fs11.pathExists(specPath)) {
4037
+ const content = await fs11.readFile(specPath, "utf8");
4038
+ const count = countEndpoints(content);
4039
+ if (count === 0) {
4040
+ violations.push({
4041
+ edge,
4042
+ message: `${edge.from}\u2192${edge.to}: openspec.md\uC5D0 \uC815\uC758\uB41C \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
4043
+ });
4044
+ }
4045
+ }
4046
+ break;
4047
+ }
4048
+ case "domain": {
4049
+ const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
4050
+ if (await fs11.pathExists(omcPath)) {
4051
+ const content = await fs11.readFile(omcPath, "utf8");
4052
+ const count = countDomainObjects(content);
4053
+ if (count === 0) {
4054
+ violations.push({
4055
+ edge,
4056
+ message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
4057
+ });
4058
+ }
4059
+ }
4060
+ break;
4061
+ }
4062
+ case "businessRules": {
4063
+ const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
4064
+ if (await fs11.pathExists(omcPath)) {
4065
+ const content = await fs11.readFile(omcPath, "utf8");
4066
+ const ruleCount = countBusinessRules(content);
4067
+ if (ruleCount === 0) {
4068
+ violations.push({
4069
+ edge,
4070
+ message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uBE44\uC988\uB2C8\uC2A4 \uADDC\uCE59\uC774 0\uAC1C \u2014 harness \uAC80\uC99D \uB300\uC0C1 \uC5C6\uC74C`
4071
+ });
4072
+ }
4073
+ }
4074
+ break;
4075
+ }
4076
+ }
4077
+ }
4078
+ return violations;
4079
+ }
4080
+ function countBusinessRules(omcContent) {
4081
+ const lines = omcContent.split("\n");
4082
+ let inSection = false;
4083
+ let count = 0;
4084
+ for (const rawLine of lines) {
4085
+ const line = rawLine.trim();
4086
+ if (/^##\s*비즈니스 규칙|^##\s*Business Rules/i.test(line)) {
4087
+ inSection = true;
4088
+ continue;
4089
+ }
4090
+ if (!inSection) continue;
4091
+ if (/^##\s/.test(line) && !/비즈니스|Business/i.test(line)) {
4092
+ inSection = false;
4093
+ continue;
4094
+ }
4095
+ if (/^-\s+\*\*/.test(line) || /^-\s+[^\s]/.test(line)) count++;
4096
+ }
4097
+ return count;
4098
+ }
4099
+ async function countCodeFiles(srcDir) {
4100
+ const exts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt"]);
4101
+ let count = 0;
4102
+ async function walk(dir) {
4103
+ let entries;
4104
+ try {
4105
+ entries = await fs11.readdir(dir);
4106
+ } catch {
4107
+ return;
4108
+ }
4109
+ for (const name of entries) {
4110
+ if (name.startsWith(".") || name === "node_modules") continue;
4111
+ const full = path6.join(dir, name);
4112
+ const stat = await fs11.stat(full).catch(() => null);
4113
+ if (!stat) continue;
4114
+ if (stat.isDirectory()) {
4115
+ await walk(full);
4116
+ } else if (exts.has(path6.extname(name))) {
4117
+ count++;
4118
+ }
4119
+ }
4120
+ }
4121
+ await walk(srcDir);
4122
+ return count;
4123
+ }
4124
+ var EVAL_GATE_WINDOW_MS, STAGE_GATES;
4125
+ var init_gates = __esm({
4126
+ "src/core/gates.ts"() {
4127
+ "use strict";
4128
+ init_roboco();
4129
+ init_config();
4130
+ EVAL_GATE_WINDOW_MS = 60 * 60 * 1e3;
4131
+ STAGE_GATES = {
4132
+ environment: async () => makeResult("environment.entry", []),
4133
+ // 항상 통과
4134
+ design: designEntryGate,
4135
+ execution: executionEntryGate,
4136
+ validation: validationEntryGate,
4137
+ evaluation: evaluationEntryGate
4138
+ };
4139
+ }
4140
+ });
4141
+
4142
+ // src/stages/environment/doctor.ts
4143
+ var doctor_exports = {};
4144
+ __export(doctor_exports, {
4145
+ runDoctor: () => runDoctor
4146
+ });
4147
+ import { join as join6 } from "path";
4148
+ import { homedir } from "os";
4149
+ import fs12 from "fs-extra";
4150
+ async function runDoctor() {
4151
+ section("PAI Doctor \u2014 \uD658\uACBD \uC9C4\uB2E8");
4152
+ const checks = [];
4153
+ const nodeVersion = process.version;
4154
+ const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
4155
+ checks.push({
4156
+ label: "Node.js \uBC84\uC804",
4157
+ ok: nodeMajor >= 20,
4158
+ detail: `${nodeVersion} ${nodeMajor >= 20 ? "(>=20 OK)" : "(\uC5C5\uADF8\uB808\uC774\uB4DC \uD544\uC694)"}`,
4159
+ fix: nodeMajor < 20 ? "Node.js 20 \uC774\uC0C1\uC73C\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694" : void 0
4160
+ });
4161
+ const claudeCheck = await checkCommand("claude", ["--version"]);
4162
+ checks.push({
4163
+ label: "Claude Code CLI",
4164
+ ok: claudeCheck.ok,
4165
+ detail: claudeCheck.detail,
4166
+ fix: claudeCheck.ok ? void 0 : "npm install -g @anthropic-ai/claude-code"
4167
+ });
4168
+ const globalConfigPath = join6(homedir(), ".pai", "config.json");
4169
+ const hasGlobalConfig = await fs12.pathExists(globalConfigPath);
4170
+ checks.push({
4171
+ label: "\uAE00\uB85C\uBC8C \uC124\uC815",
4172
+ ok: true,
4173
+ detail: hasGlobalConfig ? globalConfigPath : "\uBBF8\uC124\uC815 (\uAE30\uBCF8\uAC12 \uC0AC\uC6A9)"
4174
+ });
4175
+ let hasSdk = false;
4176
+ try {
4177
+ await import("@anthropic-ai/claude-agent-sdk");
4178
+ hasSdk = true;
4179
+ } catch {
4180
+ }
4181
+ checks.push({
4182
+ label: "Agent SDK",
4183
+ ok: true,
4184
+ // optional이므로 항상 OK
4185
+ detail: hasSdk ? "\uC124\uCE58\uB428 (AI \uAE30\uB2A5 \uD65C\uC131\uD654)" : "\uBBF8\uC124\uCE58 (\uC815\uC801 \uBD84\uC11D \uBAA8\uB4DC)"
4186
+ });
4187
+ const { validateEdges: validateEdges2 } = await Promise.resolve().then(() => (init_gates(), gates_exports));
4188
+ const edgeViolations = await validateEdges2(process.cwd());
4189
+ if (edgeViolations.length > 0) {
4190
+ checks.push({
4191
+ label: "\uACC4\uC57D \uBB34\uACB0\uC131 (edges)",
4192
+ ok: false,
4193
+ detail: `${edgeViolations.length}\uAC74 \uC704\uBC18`,
4194
+ fix: edgeViolations.map((v) => v.message).join("; ")
4195
+ });
4196
+ } else {
4197
+ const { loadConfig: loadCfg } = await Promise.resolve().then(() => (init_config(), config_exports));
4198
+ const cfg = await loadCfg(process.cwd());
4199
+ const edgeCount = cfg?.edges?.length ?? 0;
4200
+ checks.push({
4201
+ label: "\uACC4\uC57D \uBB34\uACB0\uC131 (edges)",
4202
+ ok: true,
4203
+ detail: edgeCount > 0 ? `${edgeCount}\uAC1C \uC5E3\uC9C0 \uBAA8\uB450 \uC720\uD6A8` : "\uC5E3\uC9C0 \uC5C6\uC74C (\uC815\uC0C1)"
4204
+ });
4205
+ }
4206
+ console.log("");
4207
+ let passed = 0;
4208
+ for (const check of checks) {
4209
+ const icon = check.ok ? "\u2713" : "\u2717";
4210
+ const pad = " ".repeat(Math.max(1, 20 - check.label.length));
4211
+ if (check.ok) {
4212
+ success(`${icon} ${check.label}${pad}${check.detail}`);
4213
+ passed++;
4214
+ } else {
4215
+ error(`${icon} ${check.label}${pad}${check.detail}`);
4216
+ if (check.fix) {
4217
+ info(` \u2192 ${check.fix}`);
4218
+ }
4219
+ }
4220
+ }
4221
+ console.log("");
4222
+ info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
4223
+ if (passed < checks.length) {
4224
+ process.exitCode = 1;
4225
+ }
4226
+ }
4227
+ async function checkCommand(cmd, args) {
4228
+ try {
4229
+ const { execa } = await import("execa");
4230
+ const { stdout } = await execa(cmd, args, { timeout: 1e4 });
4231
+ return { ok: true, detail: stdout.trim().split("\n")[0] ?? "ok" };
4232
+ } catch {
4233
+ return { ok: false, detail: "not found" };
4234
+ }
4235
+ }
4236
+ var init_doctor = __esm({
4237
+ "src/stages/environment/doctor.ts"() {
4238
+ "use strict";
4239
+ init_ui();
4240
+ }
4241
+ });
4242
+
4243
+ // src/utils/github-fetch.ts
4244
+ import path7 from "path";
4245
+ import fs13 from "fs-extra";
4246
+ async function httpGet(url, timeoutMs, accept) {
4247
+ const controller = new AbortController();
4248
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4249
+ try {
4250
+ return await fetch(url, {
4251
+ signal: controller.signal,
4252
+ headers: { "Accept": accept, "User-Agent": "pai-zero" }
4253
+ });
4254
+ } finally {
4255
+ clearTimeout(timer);
4256
+ }
4257
+ }
4258
+ async function listDir(repo, ref, dirPath, timeoutMs) {
4259
+ const url = `https://api.github.com/repos/${repo}/contents/${encodeURI(dirPath)}?ref=${encodeURIComponent(ref)}`;
4260
+ const res = await httpGet(url, timeoutMs, "application/vnd.github+json");
4261
+ if (!res.ok) {
4262
+ throw new Error(`GitHub API ${res.status} ${res.statusText} (${url})`);
4263
+ }
4264
+ const data = await res.json();
4265
+ if (!Array.isArray(data)) {
4266
+ throw new Error(`Expected directory, got single entry at ${dirPath}`);
4267
+ }
4268
+ return data;
4269
+ }
4270
+ async function downloadFile(downloadUrl, destPath, timeoutMs) {
4271
+ const res = await httpGet(downloadUrl, timeoutMs, "*/*");
4272
+ if (!res.ok) {
4273
+ throw new Error(`Download failed ${res.status} ${res.statusText} (${downloadUrl})`);
4274
+ }
4275
+ const buf = Buffer.from(await res.arrayBuffer());
4276
+ await fs13.ensureDir(path7.dirname(destPath));
4277
+ await fs13.writeFile(destPath, buf);
4278
+ }
4279
+ async function fetchGithubDir(opts) {
4280
+ const {
4281
+ repo,
4282
+ ref = "main",
4283
+ srcPath,
4284
+ destDir,
4285
+ overwrite = false,
4286
+ timeoutMs = 1e4
4287
+ } = opts;
4288
+ const result = { written: [], skipped: [], errors: [] };
4289
+ async function walk(currentSrc, currentDest) {
4290
+ let entries;
4291
+ try {
4292
+ entries = await listDir(repo, ref, currentSrc, timeoutMs);
4293
+ } catch (err) {
4294
+ const msg = err instanceof Error ? err.message : String(err);
4295
+ result.errors.push({ path: currentSrc, error: msg });
4296
+ return;
4297
+ }
4298
+ for (const entry of entries) {
4299
+ const relName = entry.name;
4300
+ if (entry.type === "dir") {
4301
+ await walk(`${currentSrc}/${relName}`, path7.join(currentDest, relName));
4302
+ } else if (entry.type === "file") {
4303
+ const destFile = path7.join(currentDest, relName);
4304
+ if (!overwrite && await fs13.pathExists(destFile)) {
4305
+ result.skipped.push(destFile);
4306
+ continue;
4307
+ }
4308
+ if (!entry.download_url) {
4309
+ result.errors.push({ path: entry.path, error: "no download_url" });
4310
+ continue;
4311
+ }
4312
+ try {
4313
+ await downloadFile(entry.download_url, destFile, timeoutMs);
4314
+ result.written.push(destFile);
4315
+ } catch (err) {
4316
+ const msg = err instanceof Error ? err.message : String(err);
4317
+ result.errors.push({ path: entry.path, error: msg });
4318
+ }
4319
+ }
4320
+ }
4321
+ }
4322
+ await walk(srcPath, destDir);
4323
+ return result;
3764
4324
  }
3765
4325
  var init_github_fetch = __esm({
3766
4326
  "src/utils/github-fetch.ts"() {
@@ -3773,8 +4333,8 @@ var fetch_cmd_exports = {};
3773
4333
  __export(fetch_cmd_exports, {
3774
4334
  fetchCommand: () => fetchCommand
3775
4335
  });
3776
- import path6 from "path";
3777
- import fs12 from "fs-extra";
4336
+ import path8 from "path";
4337
+ import fs14 from "fs-extra";
3778
4338
  import chalk4 from "chalk";
3779
4339
  async function fetchCommand(cwd, recipeKey, options) {
3780
4340
  if (options.list) {
@@ -3812,7 +4372,7 @@ async function fetchCommand(cwd, recipeKey, options) {
3812
4372
  const installed = [];
3813
4373
  for (const key of keys) {
3814
4374
  const recipe = RECIPES[key];
3815
- const targetDir = path6.join(cwd, recipe.target);
4375
+ const targetDir = path8.join(cwd, recipe.target);
3816
4376
  console.log("");
3817
4377
  console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
3818
4378
  const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
@@ -3834,7 +4394,7 @@ async function fetchCommand(cwd, recipeKey, options) {
3834
4394
  if (result.written.length > 0) {
3835
4395
  success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
3836
4396
  for (const f of result.written) {
3837
- console.log(chalk4.gray(` ${path6.relative(cwd, f)}`));
4397
+ console.log(chalk4.gray(` ${path8.relative(cwd, f)}`));
3838
4398
  }
3839
4399
  }
3840
4400
  if (result.skipped.length > 0) {
@@ -3870,10 +4430,10 @@ async function fetchCommand(cwd, recipeKey, options) {
3870
4430
  }
3871
4431
  async function appendEnvKeys(cwd, recipe) {
3872
4432
  if (recipe.envKeys.length === 0) return;
3873
- const envPath = path6.join(cwd, ".env.local");
4433
+ const envPath = path8.join(cwd, ".env.local");
3874
4434
  let content = "";
3875
- if (await fs12.pathExists(envPath)) {
3876
- content = await fs12.readFile(envPath, "utf8");
4435
+ if (await fs14.pathExists(envPath)) {
4436
+ content = await fs14.readFile(envPath, "utf8");
3877
4437
  }
3878
4438
  const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
3879
4439
  if (missingKeys.length === 0) return;
@@ -3886,13 +4446,13 @@ async function appendEnvKeys(cwd, recipe) {
3886
4446
  if (ek.hint) lines.push(`# ${ek.hint}`);
3887
4447
  lines.push(`${ek.key}=${ek.default ?? ""}`);
3888
4448
  }
3889
- await fs12.ensureFile(envPath);
3890
- await fs12.appendFile(envPath, lines.join("\n") + "\n");
4449
+ await fs14.ensureFile(envPath);
4450
+ await fs14.appendFile(envPath, lines.join("\n") + "\n");
3891
4451
  }
3892
4452
  async function upsertRecipesSkill(cwd, installedKeys) {
3893
- const skillDir = path6.join(cwd, ".claude", "skills", "recipes");
3894
- await fs12.ensureDir(skillDir);
3895
- const skillPath = path6.join(skillDir, "SKILL.md");
4453
+ const skillDir = path8.join(cwd, ".claude", "skills", "recipes");
4454
+ await fs14.ensureDir(skillDir);
4455
+ const skillPath = path8.join(skillDir, "SKILL.md");
3896
4456
  const recipes = installedKeys.map((k) => RECIPES[k]);
3897
4457
  const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
3898
4458
  const body = [
@@ -3931,10 +4491,10 @@ async function upsertRecipesSkill(cwd, installedKeys) {
3931
4491
  "5. \uAC00\uC774\uB4DC \uC21C\uC11C\uC5D0 \uB530\uB77C \uAD6C\uD604 \u2014 \uB808\uC2DC\uD53C \uADDC\uCE59\uC744 \uC808\uB300 \uC6B0\uD68C\uD558\uC9C0 \uB9D0 \uAC83",
3932
4492
  ""
3933
4493
  ].join("\n");
3934
- await fs12.writeFile(skillPath, body);
4494
+ await fs14.writeFile(skillPath, body);
3935
4495
  }
3936
4496
  async function upsertClaudeMdBlock(cwd, installedKeys) {
3937
- const claudeMdPath = path6.join(cwd, "CLAUDE.md");
4497
+ const claudeMdPath = path8.join(cwd, "CLAUDE.md");
3938
4498
  const BLOCK_START = "<!-- pai:recipes:start -->";
3939
4499
  const BLOCK_END = "<!-- pai:recipes:end -->";
3940
4500
  const lines = [];
@@ -3954,8 +4514,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
3954
4514
  lines.push(BLOCK_END);
3955
4515
  const block = lines.join("\n");
3956
4516
  let content = "";
3957
- if (await fs12.pathExists(claudeMdPath)) {
3958
- content = await fs12.readFile(claudeMdPath, "utf8");
4517
+ if (await fs14.pathExists(claudeMdPath)) {
4518
+ content = await fs14.readFile(claudeMdPath, "utf8");
3959
4519
  }
3960
4520
  const startIdx = content.indexOf(BLOCK_START);
3961
4521
  const endIdx = content.indexOf(BLOCK_END);
@@ -3968,8 +4528,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
3968
4528
  if (content.length > 0) content += "\n";
3969
4529
  content += block + "\n";
3970
4530
  }
3971
- await fs12.ensureFile(claudeMdPath);
3972
- await fs12.writeFile(claudeMdPath, content);
4531
+ await fs14.ensureFile(claudeMdPath);
4532
+ await fs14.writeFile(claudeMdPath, content);
3973
4533
  }
3974
4534
  var init_fetch_cmd = __esm({
3975
4535
  "src/cli/commands/fetch.cmd.ts"() {
@@ -4203,7 +4763,7 @@ __export(analyzer_exports2, {
4203
4763
  analyzeRepository: () => analyzeRepository
4204
4764
  });
4205
4765
  import { join as join7 } from "path";
4206
- import fs13 from "fs-extra";
4766
+ import fs15 from "fs-extra";
4207
4767
  async function analyzeRepository(repoPath) {
4208
4768
  try {
4209
4769
  return await aiAnalysis(repoPath);
@@ -4264,14 +4824,14 @@ async function checkTestCoverage(repoPath) {
4264
4824
  ".nycrc"
4265
4825
  ];
4266
4826
  for (const f of testConfigs) {
4267
- const found = await fs13.pathExists(join7(repoPath, f));
4827
+ const found = await fs15.pathExists(join7(repoPath, f));
4268
4828
  findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4269
4829
  if (found) score += 20;
4270
4830
  }
4271
4831
  const testDirs = ["tests", "test", "__tests__", "spec"];
4272
4832
  let hasTestDir = false;
4273
4833
  for (const d of testDirs) {
4274
- if (await fs13.pathExists(join7(repoPath, d))) {
4834
+ if (await fs15.pathExists(join7(repoPath, d))) {
4275
4835
  findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
4276
4836
  hasTestDir = true;
4277
4837
  score += 30;
@@ -4295,7 +4855,7 @@ async function checkCiCd(repoPath) {
4295
4855
  { path: ".circleci", label: "CircleCI" }
4296
4856
  ];
4297
4857
  for (const { path: path10, label } of ciConfigs) {
4298
- const found = await fs13.pathExists(join7(repoPath, path10));
4858
+ const found = await fs15.pathExists(join7(repoPath, path10));
4299
4859
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4300
4860
  if (found) score += 40;
4301
4861
  }
@@ -4314,7 +4874,7 @@ async function checkHooks(repoPath) {
4314
4874
  { path: ".claude/settings.json", label: "Claude Code settings" }
4315
4875
  ];
4316
4876
  for (const { path: path10, label } of hookConfigs) {
4317
- const found = await fs13.pathExists(join7(repoPath, path10));
4877
+ const found = await fs15.pathExists(join7(repoPath, path10));
4318
4878
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4319
4879
  if (found) score += 20;
4320
4880
  }
@@ -4332,7 +4892,7 @@ async function checkRepoStructure(repoPath) {
4332
4892
  { path: ".gitignore", label: ".gitignore" }
4333
4893
  ];
4334
4894
  for (const { path: path10, label } of structureChecks) {
4335
- const found = await fs13.pathExists(join7(repoPath, path10));
4895
+ const found = await fs15.pathExists(join7(repoPath, path10));
4336
4896
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4337
4897
  if (found) score += 25;
4338
4898
  }
@@ -4349,7 +4909,7 @@ async function checkDocumentation(repoPath) {
4349
4909
  { path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
4350
4910
  ];
4351
4911
  for (const { path: path10, label, points } of docChecks) {
4352
- const found = await fs13.pathExists(join7(repoPath, path10));
4912
+ const found = await fs15.pathExists(join7(repoPath, path10));
4353
4913
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4354
4914
  if (found) score += points;
4355
4915
  }
@@ -4368,7 +4928,7 @@ async function checkHarnessEngineering(repoPath) {
4368
4928
  { path: ".pai/config.json", label: "PAI config", points: 10 }
4369
4929
  ];
4370
4930
  for (const { path: path10, label, points } of harnessChecks) {
4371
- const found = await fs13.pathExists(join7(repoPath, path10));
4931
+ const found = await fs15.pathExists(join7(repoPath, path10));
4372
4932
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4373
4933
  if (found) score += points;
4374
4934
  }
@@ -4653,821 +5213,398 @@ function buildDetailedReport(result, projectName) {
4653
5213
  }
4654
5214
  lines.push("");
4655
5215
  }
4656
- if (cat.grade === "A" || cat.grade === "B") {
4657
- lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
4658
- } else if (cat.grade === "C") {
4659
- lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
4660
- } else if (cat.grade === "D") {
4661
- lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
4662
- } else {
4663
- lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
4664
- }
4665
- lines.push("");
4666
- if (cat.recommendations.length > 0) {
4667
- lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
4668
- lines.push("");
4669
- for (const r of cat.recommendations) {
4670
- const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
4671
- lines.push(`- ${icon} ${r.message}`);
4672
- lines.push(` - \u2192 ${r.action}`);
4673
- }
4674
- lines.push("");
4675
- }
4676
- lines.push("---");
4677
- lines.push("");
4678
- }
4679
- lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
4680
- lines.push("");
4681
- const critical = result.categories.filter((c2) => c2.grade === "F");
4682
- const warning = result.categories.filter((c2) => c2.grade === "D");
4683
- const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
4684
- if (critical.length > 0) {
4685
- lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
4686
- lines.push("");
4687
- lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
4688
- lines.push("|------|----------|------|------|");
4689
- for (const c2 of critical) {
4690
- const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
4691
- lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
4692
- }
4693
- lines.push("");
4694
- }
4695
- if (warning.length > 0) {
4696
- lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
4697
- lines.push("");
4698
- lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
4699
- lines.push("|------|----------|------|------|");
4700
- for (const c2 of warning) {
4701
- const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
4702
- lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
4703
- }
4704
- lines.push("");
4705
- }
4706
- if (ok.length > 0) {
4707
- lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
4708
- lines.push("");
4709
- for (const c2 of ok) {
4710
- lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
4711
- }
4712
- lines.push("");
4713
- }
4714
- lines.push("---");
4715
- lines.push("");
4716
- lines.push("## \uACB0\uB860");
4717
- lines.push("");
4718
- lines.push(result.summary);
4719
- lines.push("");
4720
- if (result.totalGrade === "F" || result.totalGrade === "D") {
4721
- const immediateActions = critical.length + warning.length;
4722
- lines.push(`\uC989\uC2DC \uAC1C\uC120 \uAC00\uB2A5\uD55C ${immediateActions}\uAC1C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uBA74 \uC810\uC218\uB97C \uD06C\uAC8C \uB04C\uC5B4\uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
4723
- } else if (result.totalGrade === "C") {
4724
- lines.push("\uAE30\uBCF8 \uAD6C\uC870\uAC00 \uC798 \uAC16\uCD94\uC5B4\uC838 \uC788\uC73C\uBA70, \uB2E8\uAE30 \uAC1C\uC120 \uD56D\uBAA9\uC744 \uB9C8\uBB34\uB9AC\uD558\uBA74 B\uB4F1\uAE09 \uB2EC\uC131\uC774 \uAC00\uB2A5\uD569\uB2C8\uB2E4.");
4725
- } else {
4726
- lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uC798 \uAD6C\uC131\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC9C0\uC18D\uC801\uC778 \uD488\uC9C8 \uAD00\uB9AC\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.");
4727
- }
4728
- lines.push("");
4729
- lines.push("---");
4730
- lines.push("");
4731
- lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
4732
- return lines.join("\n") + "\n";
4733
- }
4734
- var GRADE_COLORS;
4735
- var init_reporter = __esm({
4736
- "src/stages/evaluation/reporter.ts"() {
4737
- "use strict";
4738
- GRADE_COLORS = {
4739
- A: chalk5.hex("#6BCB77"),
4740
- B: chalk5.hex("#7B93DB"),
4741
- C: chalk5.hex("#E2B340"),
4742
- D: chalk5.hex("#E06C75"),
4743
- F: chalk5.hex("#CC4444")
4744
- };
4745
- }
4746
- });
4747
-
4748
- // src/utils/shell-cd.ts
4749
- var shell_cd_exports = {};
4750
- __export(shell_cd_exports, {
4751
- installShellHelper: () => installShellHelper,
4752
- requestCdAfter: () => requestCdAfter
4753
- });
4754
- import { join as join8 } from "path";
4755
- import { homedir as homedir2 } from "os";
4756
- import fs14 from "fs-extra";
4757
- async function requestCdAfter(targetDir) {
4758
- await fs14.ensureDir(PAI_DIR);
4759
- await fs14.writeFile(CD_FILE, targetDir);
4760
- }
4761
- async function installShellHelper() {
4762
- await fs14.ensureDir(PAI_DIR);
4763
- if (isWindows) {
4764
- return installPowerShellHelper();
4765
- }
4766
- return installBashHelper();
4767
- }
4768
- async function installBashHelper() {
4769
- await fs14.writeFile(HELPER_FILE_SH, BASH_HELPER);
4770
- const rcFile = getShellRcPath();
4771
- const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
4772
- if (await fs14.pathExists(rcFile)) {
4773
- const content = await fs14.readFile(rcFile, "utf8");
4774
- if (content.includes("shell-helper.sh")) {
4775
- return true;
4776
- }
4777
- await fs14.appendFile(rcFile, `
4778
- # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
4779
- ${sourceLine}
4780
- `);
4781
- return false;
4782
- }
4783
- await fs14.writeFile(rcFile, `${sourceLine}
4784
- `);
4785
- return false;
4786
- }
4787
- async function installPowerShellHelper() {
4788
- await fs14.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
4789
- const rcFile = getShellRcPath();
4790
- const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
4791
- await fs14.ensureDir(join8(rcFile, ".."));
4792
- if (await fs14.pathExists(rcFile)) {
4793
- const content = await fs14.readFile(rcFile, "utf8");
4794
- if (content.includes("shell-helper.ps1")) {
4795
- return true;
4796
- }
4797
- await fs14.appendFile(rcFile, `
4798
- # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
4799
- ${sourceLine}
4800
- `);
4801
- return false;
4802
- }
4803
- await fs14.writeFile(rcFile, `${sourceLine}
4804
- `);
4805
- return false;
4806
- }
4807
- var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
4808
- var init_shell_cd = __esm({
4809
- "src/utils/shell-cd.ts"() {
4810
- "use strict";
4811
- init_platform();
4812
- PAI_DIR = join8(homedir2(), ".pai");
4813
- CD_FILE = join8(PAI_DIR, ".cd-after");
4814
- HELPER_FILE_SH = join8(PAI_DIR, "shell-helper.sh");
4815
- HELPER_FILE_PS1 = join8(PAI_DIR, "shell-helper.ps1");
4816
- BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
4817
- pai() {
4818
- local cd_target="$HOME/.pai/.cd-after"
4819
- rm -f "$cd_target"
4820
- PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
4821
- local exit_code=$?
4822
- if [ -f "$cd_target" ]; then
4823
- local dir
4824
- dir="$(cat "$cd_target")"
4825
- rm -f "$cd_target"
4826
- if [ -n "$dir" ] && [ -d "$dir" ]; then
4827
- cd "$dir" || true
4828
- echo " \u2192 cd $dir"
4829
- fi
4830
- fi
4831
- return $exit_code
4832
- }
4833
- `;
4834
- POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
4835
- function pai {
4836
- $cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
4837
- Remove-Item $cdTarget -ErrorAction SilentlyContinue
4838
- $env:PAI_CD_AFTER = $cdTarget
4839
- & npx pai-zero @args
4840
- $exitCode = $LASTEXITCODE
4841
- if (Test-Path $cdTarget) {
4842
- $dir = Get-Content $cdTarget -Raw
4843
- Remove-Item $cdTarget -ErrorAction SilentlyContinue
4844
- if ($dir -and (Test-Path $dir)) {
4845
- Set-Location $dir
4846
- Write-Host " -> cd $dir"
4847
- }
4848
- }
4849
- return $exitCode
4850
- }
4851
- `;
4852
- }
4853
- });
4854
-
4855
- // src/utils/claude-settings.ts
4856
- var claude_settings_exports = {};
4857
- __export(claude_settings_exports, {
4858
- ClaudeSettingsError: () => ClaudeSettingsError,
4859
- buildSkeleton: () => buildSkeleton,
4860
- enableOmcPlugin: () => enableOmcPlugin,
4861
- getClaudeSettingsPath: () => getClaudeSettingsPath,
4862
- isAlreadyEnabled: () => isAlreadyEnabled,
4863
- mergeOmcIntoSettings: () => mergeOmcIntoSettings
4864
- });
4865
- import os2 from "os";
4866
- import path7 from "path";
4867
- import fs15 from "fs-extra";
4868
- function getClaudeSettingsPath(homeDir = os2.homedir()) {
4869
- return path7.join(homeDir, ".claude", "settings.json");
4870
- }
4871
- function parseJsonWithBom(raw) {
4872
- const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
4873
- return JSON.parse(stripped);
4874
- }
4875
- function timestampSuffix() {
4876
- return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
4877
- }
4878
- async function enableOmcPlugin(options = {}) {
4879
- const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
4880
- const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
4881
- const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
4882
- const wantBackup = options.backup ?? true;
4883
- const settingsPath = getClaudeSettingsPath();
4884
- await fs15.ensureDir(path7.dirname(settingsPath));
4885
- if (!await fs15.pathExists(settingsPath)) {
4886
- const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
4887
- await fs15.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
4888
- return { action: "created", settingsPath };
4889
- }
4890
- const raw = await fs15.readFile(settingsPath, "utf8");
4891
- let parsed;
4892
- try {
4893
- const value = parseJsonWithBom(raw);
4894
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4895
- throw new Error("settings.json is not a JSON object");
4896
- }
4897
- parsed = value;
4898
- } catch (err) {
4899
- const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
4900
- await fs15.copy(settingsPath, backupPath2);
4901
- throw new ClaudeSettingsError(
4902
- `settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
4903
- backupPath2
4904
- );
4905
- }
4906
- if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
4907
- return { action: "already-enabled", settingsPath };
4908
- }
4909
- let backupPath;
4910
- if (wantBackup) {
4911
- backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
4912
- await fs15.copy(settingsPath, backupPath);
4913
- }
4914
- const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
4915
- await fs15.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
4916
- return { action: "added", settingsPath, backupPath };
4917
- }
4918
- function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
4919
- return {
4920
- extraKnownMarketplaces: {
4921
- [marketplaceId]: {
4922
- source: { source: "git", url: marketplaceUrl }
4923
- }
4924
- },
4925
- enabledPlugins: {
4926
- [pluginId]: true
4927
- }
4928
- };
4929
- }
4930
- function isAlreadyEnabled(settings, marketplaceId, pluginId) {
4931
- const markets = settings["extraKnownMarketplaces"];
4932
- const enabled = settings["enabledPlugins"];
4933
- const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
4934
- const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
4935
- return hasMarket && hasPlugin;
4936
- }
4937
- function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
4938
- const next = { ...settings };
4939
- const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
4940
- existingMarkets[marketplaceId] = {
4941
- source: { source: "git", url: marketplaceUrl }
4942
- };
4943
- next["extraKnownMarketplaces"] = existingMarkets;
4944
- const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
4945
- existingPlugins[pluginId] = true;
4946
- next["enabledPlugins"] = existingPlugins;
4947
- return next;
4948
- }
4949
- var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
4950
- var init_claude_settings = __esm({
4951
- "src/utils/claude-settings.ts"() {
4952
- "use strict";
4953
- init_platform();
4954
- DEFAULT_MARKETPLACE_ID = "omc";
4955
- DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
4956
- DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
4957
- ClaudeSettingsError = class extends Error {
4958
- constructor(message, backupPath) {
4959
- super(message);
4960
- this.backupPath = backupPath;
4961
- this.name = "ClaudeSettingsError";
5216
+ if (cat.grade === "A" || cat.grade === "B") {
5217
+ lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
5218
+ } else if (cat.grade === "C") {
5219
+ lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
5220
+ } else if (cat.grade === "D") {
5221
+ lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
5222
+ } else {
5223
+ lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
5224
+ }
5225
+ lines.push("");
5226
+ if (cat.recommendations.length > 0) {
5227
+ lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
5228
+ lines.push("");
5229
+ for (const r of cat.recommendations) {
5230
+ const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
5231
+ lines.push(`- ${icon} ${r.message}`);
5232
+ lines.push(` - \u2192 ${r.action}`);
4962
5233
  }
4963
- backupPath;
4964
- };
4965
- }
4966
- });
4967
-
4968
- // src/stages/evaluation/cache.ts
4969
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
4970
- import { join as join9 } from "path";
4971
- import { createHash } from "crypto";
4972
- function computeRepoHash(repoPath) {
4973
- const hash = createHash("sha256");
4974
- for (const file of FILES_TO_HASH) {
4975
- const fullPath = join9(repoPath, file);
4976
- try {
4977
- const content = readFileSync(fullPath);
4978
- hash.update(`${file}:${content.length}`);
4979
- } catch {
4980
- hash.update(`${file}:missing`);
5234
+ lines.push("");
4981
5235
  }
5236
+ lines.push("---");
5237
+ lines.push("");
4982
5238
  }
4983
- return hash.digest("hex").slice(0, 16);
4984
- }
4985
- function getCachePath(repoPath) {
4986
- return join9(repoPath, CACHE_DIR, CACHE_FILE);
4987
- }
4988
- function loadCache(repoPath) {
4989
- try {
4990
- const data = readFileSync(getCachePath(repoPath), "utf-8");
4991
- return JSON.parse(data);
4992
- } catch {
4993
- return { version: 1, entries: {} };
5239
+ lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
5240
+ lines.push("");
5241
+ const critical = result.categories.filter((c2) => c2.grade === "F");
5242
+ const warning = result.categories.filter((c2) => c2.grade === "D");
5243
+ const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
5244
+ if (critical.length > 0) {
5245
+ lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
5246
+ lines.push("");
5247
+ lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
5248
+ lines.push("|------|----------|------|------|");
5249
+ for (const c2 of critical) {
5250
+ const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
5251
+ lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
5252
+ }
5253
+ lines.push("");
4994
5254
  }
4995
- }
4996
- function saveCache(repoPath, store) {
4997
- const cacheDir = join9(repoPath, CACHE_DIR, "cache");
4998
- if (!existsSync(cacheDir)) {
4999
- mkdirSync(cacheDir, { recursive: true });
5255
+ if (warning.length > 0) {
5256
+ lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
5257
+ lines.push("");
5258
+ lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
5259
+ lines.push("|------|----------|------|------|");
5260
+ for (const c2 of warning) {
5261
+ const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
5262
+ lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
5263
+ }
5264
+ lines.push("");
5000
5265
  }
5001
- writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
5002
- }
5003
- function getCachedResult(repoPath) {
5004
- const store = loadCache(repoPath);
5005
- const repoHash = computeRepoHash(repoPath);
5006
- const entry = store.entries[repoHash];
5007
- if (!entry) return null;
5008
- if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
5009
- return entry.llmOutput;
5010
- }
5011
- function setCachedResult(repoPath, llmOutput) {
5012
- const store = loadCache(repoPath);
5013
- const repoHash = computeRepoHash(repoPath);
5014
- const now2 = Date.now();
5015
- for (const [key, entry] of Object.entries(store.entries)) {
5016
- if (now2 - entry.timestamp > CACHE_TTL_MS) {
5017
- delete store.entries[key];
5266
+ if (ok.length > 0) {
5267
+ lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
5268
+ lines.push("");
5269
+ for (const c2 of ok) {
5270
+ lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
5018
5271
  }
5272
+ lines.push("");
5019
5273
  }
5020
- store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
5021
- saveCache(repoPath, store);
5274
+ lines.push("---");
5275
+ lines.push("");
5276
+ lines.push("## \uACB0\uB860");
5277
+ lines.push("");
5278
+ lines.push(result.summary);
5279
+ lines.push("");
5280
+ if (result.totalGrade === "F" || result.totalGrade === "D") {
5281
+ const immediateActions = critical.length + warning.length;
5282
+ lines.push(`\uC989\uC2DC \uAC1C\uC120 \uAC00\uB2A5\uD55C ${immediateActions}\uAC1C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uBA74 \uC810\uC218\uB97C \uD06C\uAC8C \uB04C\uC5B4\uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
5283
+ } else if (result.totalGrade === "C") {
5284
+ lines.push("\uAE30\uBCF8 \uAD6C\uC870\uAC00 \uC798 \uAC16\uCD94\uC5B4\uC838 \uC788\uC73C\uBA70, \uB2E8\uAE30 \uAC1C\uC120 \uD56D\uBAA9\uC744 \uB9C8\uBB34\uB9AC\uD558\uBA74 B\uB4F1\uAE09 \uB2EC\uC131\uC774 \uAC00\uB2A5\uD569\uB2C8\uB2E4.");
5285
+ } else {
5286
+ lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uC798 \uAD6C\uC131\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC9C0\uC18D\uC801\uC778 \uD488\uC9C8 \uAD00\uB9AC\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.");
5287
+ }
5288
+ lines.push("");
5289
+ lines.push("---");
5290
+ lines.push("");
5291
+ lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
5292
+ return lines.join("\n") + "\n";
5022
5293
  }
5023
- var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
5024
- var init_cache = __esm({
5025
- "src/stages/evaluation/cache.ts"() {
5294
+ var GRADE_COLORS;
5295
+ var init_reporter = __esm({
5296
+ "src/stages/evaluation/reporter.ts"() {
5026
5297
  "use strict";
5027
- CACHE_DIR = ".pai";
5028
- CACHE_FILE = "cache/evaluation.json";
5029
- CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5030
- FILES_TO_HASH = [
5031
- "package.json",
5032
- "pyproject.toml",
5033
- "go.mod",
5034
- "Cargo.toml",
5035
- "tsconfig.json",
5036
- "jest.config.ts",
5037
- "vitest.config.ts",
5038
- ".github/workflows",
5039
- ".gitlab-ci.yml",
5040
- ".husky",
5041
- ".lintstagedrc",
5042
- "CLAUDE.md",
5043
- "AGENTS.md",
5044
- ".cursorrules",
5045
- "README.md",
5046
- "CONTRIBUTING.md"
5047
- ];
5298
+ GRADE_COLORS = {
5299
+ A: chalk5.hex("#6BCB77"),
5300
+ B: chalk5.hex("#7B93DB"),
5301
+ C: chalk5.hex("#E2B340"),
5302
+ D: chalk5.hex("#E06C75"),
5303
+ F: chalk5.hex("#CC4444")
5304
+ };
5048
5305
  }
5049
5306
  });
5050
5307
 
5051
- // src/core/roboco.ts
5052
- import os3 from "os";
5053
- import path8 from "path";
5308
+ // src/utils/shell-cd.ts
5309
+ var shell_cd_exports = {};
5310
+ __export(shell_cd_exports, {
5311
+ installShellHelper: () => installShellHelper,
5312
+ requestCdAfter: () => requestCdAfter
5313
+ });
5314
+ import { join as join8 } from "path";
5315
+ import { homedir as homedir2 } from "os";
5054
5316
  import fs16 from "fs-extra";
5055
- function robocoPath(cwd) {
5056
- return path8.join(cwd, ROBOCO_PATH);
5317
+ async function requestCdAfter(targetDir) {
5318
+ await fs16.ensureDir(PAI_DIR);
5319
+ await fs16.writeFile(CD_FILE, targetDir);
5057
5320
  }
5058
- async function loadRobocoState(cwd) {
5059
- const p = robocoPath(cwd);
5060
- if (!await fs16.pathExists(p)) return freshState();
5061
- try {
5062
- const raw = await fs16.readJson(p);
5063
- if (raw?.version !== "2.0") return freshState();
5064
- return raw;
5065
- } catch {
5066
- return freshState();
5321
+ async function installShellHelper() {
5322
+ await fs16.ensureDir(PAI_DIR);
5323
+ if (isWindows) {
5324
+ return installPowerShellHelper();
5067
5325
  }
5326
+ return installBashHelper();
5068
5327
  }
5069
- async function saveRobocoState(cwd, state) {
5070
- const p = robocoPath(cwd);
5071
- await fs16.ensureDir(path8.dirname(p));
5072
- await fs16.writeJson(p, state, { spaces: 2 });
5073
- }
5074
- function freshState() {
5075
- return {
5076
- version: "2.0",
5077
- currentStage: "environment",
5078
- stageHistory: [],
5079
- lock: null,
5080
- gates: {}
5081
- };
5082
- }
5083
- function isProcessAlive(pid) {
5084
- try {
5085
- process.kill(pid, 0);
5086
- return true;
5087
- } catch {
5328
+ async function installBashHelper() {
5329
+ await fs16.writeFile(HELPER_FILE_SH, BASH_HELPER);
5330
+ const rcFile = getShellRcPath();
5331
+ const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
5332
+ if (await fs16.pathExists(rcFile)) {
5333
+ const content = await fs16.readFile(rcFile, "utf8");
5334
+ if (content.includes("shell-helper.sh")) {
5335
+ return true;
5336
+ }
5337
+ await fs16.appendFile(rcFile, `
5338
+ # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5339
+ ${sourceLine}
5340
+ `);
5088
5341
  return false;
5089
5342
  }
5343
+ await fs16.writeFile(rcFile, `${sourceLine}
5344
+ `);
5345
+ return false;
5090
5346
  }
5091
- function isLockStale(lock, now2 = /* @__PURE__ */ new Date()) {
5092
- if (!isProcessAlive(lock.pid)) return true;
5093
- if (new Date(lock.expiresAt).getTime() < now2.getTime()) return true;
5347
+ async function installPowerShellHelper() {
5348
+ await fs16.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
5349
+ const rcFile = getShellRcPath();
5350
+ const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
5351
+ await fs16.ensureDir(join8(rcFile, ".."));
5352
+ if (await fs16.pathExists(rcFile)) {
5353
+ const content = await fs16.readFile(rcFile, "utf8");
5354
+ if (content.includes("shell-helper.ps1")) {
5355
+ return true;
5356
+ }
5357
+ await fs16.appendFile(rcFile, `
5358
+ # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5359
+ ${sourceLine}
5360
+ `);
5361
+ return false;
5362
+ }
5363
+ await fs16.writeFile(rcFile, `${sourceLine}
5364
+ `);
5094
5365
  return false;
5095
5366
  }
5096
- async function acquireLock(cwd, stage, command, ttlMs = DEFAULT_TTL_MS) {
5097
- const state = await loadRobocoState(cwd);
5098
- if (state.lock) {
5099
- if (!isLockStale(state.lock)) {
5100
- throw new RobocoLockError(formatLockError(state.lock), state.lock);
5367
+ var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
5368
+ var init_shell_cd = __esm({
5369
+ "src/utils/shell-cd.ts"() {
5370
+ "use strict";
5371
+ init_platform();
5372
+ PAI_DIR = join8(homedir2(), ".pai");
5373
+ CD_FILE = join8(PAI_DIR, ".cd-after");
5374
+ HELPER_FILE_SH = join8(PAI_DIR, "shell-helper.sh");
5375
+ HELPER_FILE_PS1 = join8(PAI_DIR, "shell-helper.ps1");
5376
+ BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
5377
+ pai() {
5378
+ local cd_target="$HOME/.pai/.cd-after"
5379
+ rm -f "$cd_target"
5380
+ PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
5381
+ local exit_code=$?
5382
+ if [ -f "$cd_target" ]; then
5383
+ local dir
5384
+ dir="$(cat "$cd_target")"
5385
+ rm -f "$cd_target"
5386
+ if [ -n "$dir" ] && [ -d "$dir" ]; then
5387
+ cd "$dir" || true
5388
+ echo " \u2192 cd $dir"
5389
+ fi
5390
+ fi
5391
+ return $exit_code
5392
+ }
5393
+ `;
5394
+ POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
5395
+ function pai {
5396
+ $cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
5397
+ Remove-Item $cdTarget -ErrorAction SilentlyContinue
5398
+ $env:PAI_CD_AFTER = $cdTarget
5399
+ & npx pai-zero @args
5400
+ $exitCode = $LASTEXITCODE
5401
+ if (Test-Path $cdTarget) {
5402
+ $dir = Get-Content $cdTarget -Raw
5403
+ Remove-Item $cdTarget -ErrorAction SilentlyContinue
5404
+ if ($dir -and (Test-Path $dir)) {
5405
+ Set-Location $dir
5406
+ Write-Host " -> cd $dir"
5101
5407
  }
5102
5408
  }
5103
- const now2 = /* @__PURE__ */ new Date();
5104
- const newLock = {
5105
- pid: process.pid,
5106
- stage,
5107
- command,
5108
- acquiredAt: now2.toISOString(),
5109
- expiresAt: new Date(now2.getTime() + ttlMs).toISOString(),
5110
- host: os3.hostname()
5111
- };
5112
- state.lock = newLock;
5113
- await saveRobocoState(cwd, state);
5114
- return state;
5409
+ return $exitCode
5115
5410
  }
5116
- async function releaseLock(cwd) {
5117
- const state = await loadRobocoState(cwd);
5118
- if (state.lock && state.lock.pid === process.pid) {
5119
- state.lock = null;
5120
- await saveRobocoState(cwd, state);
5411
+ `;
5121
5412
  }
5413
+ });
5414
+
5415
+ // src/utils/claude-settings.ts
5416
+ var claude_settings_exports = {};
5417
+ __export(claude_settings_exports, {
5418
+ ClaudeSettingsError: () => ClaudeSettingsError,
5419
+ buildSkeleton: () => buildSkeleton,
5420
+ enableOmcPlugin: () => enableOmcPlugin,
5421
+ getClaudeSettingsPath: () => getClaudeSettingsPath,
5422
+ isAlreadyEnabled: () => isAlreadyEnabled,
5423
+ mergeOmcIntoSettings: () => mergeOmcIntoSettings
5424
+ });
5425
+ import os3 from "os";
5426
+ import path9 from "path";
5427
+ import fs17 from "fs-extra";
5428
+ function getClaudeSettingsPath(homeDir = os3.homedir()) {
5429
+ return path9.join(homeDir, ".claude", "settings.json");
5122
5430
  }
5123
- function formatLockError(lock) {
5124
- const started = new Date(lock.acquiredAt);
5125
- const mins = Math.round((Date.now() - started.getTime()) / 6e4);
5126
- return [
5127
- "\uB2E4\uB978 PAI \uBA85\uB839\uC774 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4",
5128
- ` stage: ${lock.stage}`,
5129
- ` command: ${lock.command}`,
5130
- ` \uC2DC\uC791: ${mins}\uBD84 \uC804 (PID ${lock.pid}${lock.host ? ", host " + lock.host : ""})`,
5131
- " \uB300\uAE30\uD558\uC9C0 \uC54A\uACE0 \uC989\uC2DC \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC644\uB8CC \uD6C4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694."
5132
- ].join("\n");
5431
+ function parseJsonWithBom(raw) {
5432
+ const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
5433
+ return JSON.parse(stripped);
5133
5434
  }
5134
- async function markStageStart(cwd, stage, command) {
5135
- const state = await loadRobocoState(cwd);
5136
- state.currentStage = stage;
5137
- state.stageHistory.push({
5138
- stage,
5139
- status: "in_progress",
5140
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5141
- by: command
5142
- });
5143
- await saveRobocoState(cwd, state);
5435
+ function timestampSuffix() {
5436
+ return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
5144
5437
  }
5145
- async function markStageEnd(cwd, stage, success2, reason) {
5146
- const state = await loadRobocoState(cwd);
5147
- for (let i = state.stageHistory.length - 1; i >= 0; i--) {
5148
- const entry = state.stageHistory[i];
5149
- if (entry.stage === stage && entry.status === "in_progress") {
5150
- entry.status = success2 ? "done" : "failed";
5151
- entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
5152
- if (reason) entry.reason = reason;
5153
- break;
5154
- }
5438
+ async function enableOmcPlugin(options = {}) {
5439
+ const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
5440
+ const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
5441
+ const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
5442
+ const wantBackup = options.backup ?? true;
5443
+ const settingsPath = getClaudeSettingsPath();
5444
+ await fs17.ensureDir(path9.dirname(settingsPath));
5445
+ if (!await fs17.pathExists(settingsPath)) {
5446
+ const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
5447
+ await fs17.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
5448
+ return { action: "created", settingsPath };
5155
5449
  }
5156
- await saveRobocoState(cwd, state);
5157
- }
5158
- async function saveGateResult(cwd, key, result) {
5159
- const state = await loadRobocoState(cwd);
5160
- state.gates[key] = result;
5161
- await saveRobocoState(cwd, state);
5162
- }
5163
- async function withRoboco(cwd, stage, command, options, fn) {
5164
- await acquireLock(cwd, stage, command, options.ttlMs);
5450
+ const raw = await fs17.readFile(settingsPath, "utf8");
5451
+ let parsed;
5165
5452
  try {
5166
- if (options.gate && !options.force) {
5167
- const result = await options.gate(cwd);
5168
- await saveGateResult(cwd, `${stage}.entry`, result);
5169
- if (!result.passed && !options.skipGates) {
5170
- await markStageEnd(cwd, stage, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${result.name}`);
5171
- throw new GateFailedError(result);
5172
- }
5173
- if (!result.passed && options.skipGates) {
5174
- }
5453
+ const value = parseJsonWithBom(raw);
5454
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5455
+ throw new Error("settings.json is not a JSON object");
5175
5456
  }
5176
- await markStageStart(cwd, stage, command);
5177
- const value = await fn();
5178
- await markStageEnd(cwd, stage, true);
5179
- return value;
5457
+ parsed = value;
5180
5458
  } catch (err) {
5181
- if (!(err instanceof GateFailedError)) {
5182
- const msg = err instanceof Error ? err.message : String(err);
5183
- await markStageEnd(cwd, stage, false, msg).catch(() => {
5184
- });
5185
- }
5186
- throw err;
5187
- } finally {
5188
- await releaseLock(cwd).catch(() => {
5189
- });
5190
- }
5191
- }
5192
- async function syncHandoff(cwd) {
5193
- const handoffPath = path8.join(cwd, "handoff.md");
5194
- if (!await fs16.pathExists(handoffPath)) return;
5195
- const state = await loadRobocoState(cwd);
5196
- const block = buildHandoffBlock(state);
5197
- const content = await fs16.readFile(handoffPath, "utf8");
5198
- const startIdx = content.indexOf(HANDOFF_START);
5199
- const endIdx = content.indexOf(HANDOFF_END);
5200
- let next;
5201
- if (startIdx >= 0 && endIdx > startIdx) {
5202
- next = content.slice(0, startIdx) + block + content.slice(endIdx + HANDOFF_END.length);
5203
- } else {
5204
- const sep = content.endsWith("\n") ? "\n" : "\n\n";
5205
- next = content + sep + block + "\n";
5459
+ const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
5460
+ await fs17.copy(settingsPath, backupPath2);
5461
+ throw new ClaudeSettingsError(
5462
+ `settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
5463
+ backupPath2
5464
+ );
5206
5465
  }
5207
- await fs16.writeFile(handoffPath, next);
5208
- }
5209
- function buildHandoffBlock(state) {
5210
- const lines = [HANDOFF_START, "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)", ""];
5211
- const done = state.stageHistory.filter((h) => h.status === "done");
5212
- const inProgress = state.stageHistory.filter((h) => h.status === "in_progress");
5213
- const failed = state.stageHistory.filter((h) => h.status === "failed");
5214
- if (inProgress.length > 0) {
5215
- lines.push("### \uC9C4\uD589 \uC911");
5216
- for (const h of inProgress) {
5217
- lines.push(`- [ ] ${h.stage} (${h.by}, \uC2DC\uC791 ${fmtDate(h.startedAt)})`);
5218
- }
5219
- lines.push("");
5466
+ if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
5467
+ return { action: "already-enabled", settingsPath };
5220
5468
  }
5221
- if (failed.length > 0) {
5222
- lines.push("### \uC2E4\uD328");
5223
- for (const h of failed) {
5224
- lines.push(`- [x] ${h.stage} \u2014 ${h.reason ?? "\uC6D0\uC778 \uBD88\uBA85"} (${fmtDate(h.startedAt)})`);
5225
- }
5226
- lines.push("");
5469
+ let backupPath;
5470
+ if (wantBackup) {
5471
+ backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
5472
+ await fs17.copy(settingsPath, backupPath);
5227
5473
  }
5228
- if (done.length > 0) {
5229
- lines.push("### \uC644\uB8CC");
5230
- for (const h of done) {
5231
- lines.push(`- [x] ${h.stage} (${h.by}, ${fmtDate(h.endedAt ?? h.startedAt)})`);
5474
+ const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
5475
+ await fs17.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
5476
+ return { action: "added", settingsPath, backupPath };
5477
+ }
5478
+ function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
5479
+ return {
5480
+ extraKnownMarketplaces: {
5481
+ [marketplaceId]: {
5482
+ source: { source: "git", url: marketplaceUrl }
5483
+ }
5484
+ },
5485
+ enabledPlugins: {
5486
+ [pluginId]: true
5232
5487
  }
5233
- lines.push("");
5234
- }
5235
- lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
5236
- lines.push(HANDOFF_END);
5237
- return lines.join("\n");
5488
+ };
5238
5489
  }
5239
- function fmtDate(iso) {
5240
- try {
5241
- const d = new Date(iso);
5242
- return d.toISOString().slice(0, 16).replace("T", " ");
5243
- } catch {
5244
- return iso;
5245
- }
5490
+ function isAlreadyEnabled(settings, marketplaceId, pluginId) {
5491
+ const markets = settings["extraKnownMarketplaces"];
5492
+ const enabled = settings["enabledPlugins"];
5493
+ const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
5494
+ const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
5495
+ return hasMarket && hasPlugin;
5246
5496
  }
5247
- var ROBOCO_PATH, DEFAULT_TTL_MS, RobocoLockError, GateFailedError, HANDOFF_START, HANDOFF_END;
5248
- var init_roboco = __esm({
5249
- "src/core/roboco.ts"() {
5497
+ function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
5498
+ const next = { ...settings };
5499
+ const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
5500
+ existingMarkets[marketplaceId] = {
5501
+ source: { source: "git", url: marketplaceUrl }
5502
+ };
5503
+ next["extraKnownMarketplaces"] = existingMarkets;
5504
+ const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
5505
+ existingPlugins[pluginId] = true;
5506
+ next["enabledPlugins"] = existingPlugins;
5507
+ return next;
5508
+ }
5509
+ var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
5510
+ var init_claude_settings = __esm({
5511
+ "src/utils/claude-settings.ts"() {
5250
5512
  "use strict";
5251
- ROBOCO_PATH = path8.join(".pai", "roboco.json");
5252
- DEFAULT_TTL_MS = 10 * 60 * 1e3;
5253
- RobocoLockError = class extends Error {
5254
- constructor(message, holder) {
5513
+ init_platform();
5514
+ DEFAULT_MARKETPLACE_ID = "omc";
5515
+ DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
5516
+ DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
5517
+ ClaudeSettingsError = class extends Error {
5518
+ constructor(message, backupPath) {
5255
5519
  super(message);
5256
- this.holder = holder;
5257
- this.name = "RobocoLockError";
5258
- }
5259
- holder;
5260
- };
5261
- GateFailedError = class extends Error {
5262
- constructor(result) {
5263
- super(`\uAC8C\uC774\uD2B8 \uC2E4\uD328 (${result.name}): ${result.violations.length}\uAC74 \uC704\uBC18`);
5264
- this.result = result;
5265
- this.name = "GateFailedError";
5520
+ this.backupPath = backupPath;
5521
+ this.name = "ClaudeSettingsError";
5266
5522
  }
5267
- result;
5523
+ backupPath;
5268
5524
  };
5269
- HANDOFF_START = "<!-- roboco:start -->";
5270
- HANDOFF_END = "<!-- roboco:end -->";
5271
5525
  }
5272
5526
  });
5273
5527
 
5274
- // src/core/gates.ts
5275
- import path9 from "path";
5276
- import fs17 from "fs-extra";
5277
- function now() {
5278
- return (/* @__PURE__ */ new Date()).toISOString();
5279
- }
5280
- function makeResult(name, violations) {
5281
- return {
5282
- name,
5283
- passed: violations.length === 0,
5284
- checkedAt: now(),
5285
- violations
5286
- };
5287
- }
5288
- async function designEntryGate(cwd) {
5289
- const violations = [];
5290
- const claudeMd = path9.join(cwd, "CLAUDE.md");
5291
- if (!await fs17.pathExists(claudeMd)) {
5292
- violations.push({
5293
- rule: "claude-md-exists",
5294
- message: "CLAUDE.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
5295
- location: "CLAUDE.md"
5296
- });
5297
- }
5298
- const configJson = path9.join(cwd, ".pai", "config.json");
5299
- if (!await fs17.pathExists(configJson)) {
5300
- violations.push({
5301
- rule: "pai-config-exists",
5302
- message: "PAI \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
5303
- location: ".pai/config.json"
5304
- });
5305
- }
5306
- return makeResult("design.entry", violations);
5307
- }
5308
- async function executionEntryGate(cwd) {
5309
- const violations = [];
5310
- const openspec = path9.join(cwd, "docs", "openspec.md");
5311
- const hasOpenspec = await fs17.pathExists(openspec);
5312
- if (!hasOpenspec) {
5313
- violations.push({
5314
- rule: "openspec-exists",
5315
- message: "docs/openspec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
5316
- location: "docs/openspec.md"
5317
- });
5318
- } else {
5319
- const content = await fs17.readFile(openspec, "utf8");
5320
- const endpointCount = countEndpoints(content);
5321
- if (endpointCount < 1) {
5322
- violations.push({
5323
- rule: "openspec-endpoints-min",
5324
- message: `openspec.md\uC5D0 \uC815\uC758\uB41C API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694 (\uD604\uC7AC: 0). /pai design \uC2E4\uD589 \uAD8C\uC7A5.`,
5325
- location: "docs/openspec.md \xA7 5. API \uC5D4\uB4DC\uD3EC\uC778\uD2B8"
5326
- });
5327
- }
5328
- }
5329
- const omc = path9.join(cwd, ".pai", "omc.md");
5330
- if (await fs17.pathExists(omc)) {
5331
- const content = await fs17.readFile(omc, "utf8");
5332
- const domainCount = countDomainObjects(content);
5333
- if (domainCount < 1) {
5334
- violations.push({
5335
- rule: "omc-domain-min",
5336
- message: "omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
5337
- location: ".pai/omc.md \xA7 \uB3C4\uBA54\uC778 \uAC1D\uCCB4"
5338
- });
5528
+ // src/stages/evaluation/cache.ts
5529
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
5530
+ import { join as join9 } from "path";
5531
+ import { createHash } from "crypto";
5532
+ function computeRepoHash(repoPath) {
5533
+ const hash = createHash("sha256");
5534
+ for (const file of FILES_TO_HASH) {
5535
+ const fullPath = join9(repoPath, file);
5536
+ try {
5537
+ const content = readFileSync(fullPath);
5538
+ hash.update(`${file}:${content.length}`);
5539
+ } catch {
5540
+ hash.update(`${file}:missing`);
5339
5541
  }
5340
5542
  }
5341
- return makeResult("execution.entry", violations);
5543
+ return hash.digest("hex").slice(0, 16);
5342
5544
  }
5343
- async function validationEntryGate(cwd) {
5344
- const violations = [];
5345
- const srcDir = path9.join(cwd, "src");
5346
- if (!await fs17.pathExists(srcDir)) {
5347
- violations.push({
5348
- rule: "src-dir-exists",
5349
- message: "src/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
5350
- location: "src/"
5351
- });
5352
- } else {
5353
- const codeFileCount = await countCodeFiles(srcDir);
5354
- if (codeFileCount === 0) {
5355
- violations.push({
5356
- rule: "code-files-min",
5357
- message: "src/ \uB0B4\uBD80\uC5D0 \uCF54\uB4DC \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
5358
- location: "src/"
5359
- });
5360
- }
5361
- }
5362
- return makeResult("validation.entry", violations);
5545
+ function getCachePath(repoPath) {
5546
+ return join9(repoPath, CACHE_DIR, CACHE_FILE);
5363
5547
  }
5364
- async function evaluationEntryGate(cwd) {
5365
- const violations = [];
5366
- const state = await loadRobocoState(cwd);
5367
- const recentValidation = [...state.stageHistory].reverse().find((h) => h.stage === "validation");
5368
- if (!recentValidation) {
5369
- violations.push({
5370
- rule: "validation-executed",
5371
- message: "validation \uB2E8\uACC4\uAC00 \uD55C \uBC88\uB3C4 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai test`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
5372
- });
5373
- } else if (recentValidation.status !== "done") {
5374
- violations.push({
5375
- rule: "validation-passed",
5376
- message: `\uCD5C\uADFC validation\uC774 ${recentValidation.status} \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD14C\uC2A4\uD2B8\uB97C \uC131\uACF5\uC2DC\uD0A8 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.`
5377
- });
5378
- } else {
5379
- const endedAt = recentValidation.endedAt ?? recentValidation.startedAt;
5380
- const age = Date.now() - new Date(endedAt).getTime();
5381
- if (age > EVAL_GATE_WINDOW_MS) {
5382
- const mins = Math.round(age / 6e4);
5383
- violations.push({
5384
- rule: "validation-recent",
5385
- message: `\uCD5C\uADFC validation \uD1B5\uACFC\uAC00 ${mins}\uBD84 \uC804\uC785\uB2C8\uB2E4 (1\uC2DC\uAC04 \uC774\uB0B4\uC5EC\uC57C \uD568). --force\uB85C \uC6B0\uD68C \uAC00\uB2A5.`
5386
- });
5387
- }
5548
+ function loadCache(repoPath) {
5549
+ try {
5550
+ const data = readFileSync(getCachePath(repoPath), "utf-8");
5551
+ return JSON.parse(data);
5552
+ } catch {
5553
+ return { version: 1, entries: {} };
5388
5554
  }
5389
- return makeResult("evaluation.entry", violations);
5390
5555
  }
5391
- function countEndpoints(openspecContent) {
5392
- const lines = openspecContent.split("\n");
5393
- let inTable = false;
5394
- let count = 0;
5395
- for (const rawLine of lines) {
5396
- const line = rawLine.trim();
5397
- if (/^##\s*5\.|^##\s*API|API 엔드포인트/i.test(line)) {
5398
- inTable = true;
5399
- continue;
5400
- }
5401
- if (!inTable) continue;
5402
- if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
5403
- inTable = false;
5404
- continue;
5405
- }
5406
- if (/^\|\s*Method/i.test(line)) continue;
5407
- if (/^\|\s*[-:]+\s*\|/.test(line)) continue;
5408
- const m = line.match(/^\|\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\|\s*(\/\S+)\s*\|/i);
5409
- if (m) count++;
5556
+ function saveCache(repoPath, store) {
5557
+ const cacheDir = join9(repoPath, CACHE_DIR, "cache");
5558
+ if (!existsSync(cacheDir)) {
5559
+ mkdirSync(cacheDir, { recursive: true });
5410
5560
  }
5411
- return count;
5561
+ writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
5412
5562
  }
5413
- function countDomainObjects(omcContent) {
5414
- const lines = omcContent.split("\n");
5415
- let count = 0;
5416
- let inDomainSection = false;
5417
- for (const rawLine of lines) {
5418
- const line = rawLine.trim();
5419
- if (/^##\s*도메인 객체|^##\s*Domain Objects/i.test(line)) {
5420
- inDomainSection = true;
5421
- continue;
5422
- }
5423
- if (!inDomainSection) continue;
5424
- if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
5425
- inDomainSection = false;
5426
- continue;
5427
- }
5428
- if (/^###\s+\w/.test(line)) count++;
5429
- }
5430
- return count;
5563
+ function getCachedResult(repoPath) {
5564
+ const store = loadCache(repoPath);
5565
+ const repoHash = computeRepoHash(repoPath);
5566
+ const entry = store.entries[repoHash];
5567
+ if (!entry) return null;
5568
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
5569
+ return entry.llmOutput;
5431
5570
  }
5432
- async function countCodeFiles(srcDir) {
5433
- const exts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt"]);
5434
- let count = 0;
5435
- async function walk(dir) {
5436
- let entries;
5437
- try {
5438
- entries = await fs17.readdir(dir);
5439
- } catch {
5440
- return;
5441
- }
5442
- for (const name of entries) {
5443
- if (name.startsWith(".") || name === "node_modules") continue;
5444
- const full = path9.join(dir, name);
5445
- const stat = await fs17.stat(full).catch(() => null);
5446
- if (!stat) continue;
5447
- if (stat.isDirectory()) {
5448
- await walk(full);
5449
- } else if (exts.has(path9.extname(name))) {
5450
- count++;
5451
- }
5571
+ function setCachedResult(repoPath, llmOutput) {
5572
+ const store = loadCache(repoPath);
5573
+ const repoHash = computeRepoHash(repoPath);
5574
+ const now2 = Date.now();
5575
+ for (const [key, entry] of Object.entries(store.entries)) {
5576
+ if (now2 - entry.timestamp > CACHE_TTL_MS) {
5577
+ delete store.entries[key];
5452
5578
  }
5453
5579
  }
5454
- await walk(srcDir);
5455
- return count;
5580
+ store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
5581
+ saveCache(repoPath, store);
5456
5582
  }
5457
- var EVAL_GATE_WINDOW_MS, STAGE_GATES;
5458
- var init_gates = __esm({
5459
- "src/core/gates.ts"() {
5583
+ var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
5584
+ var init_cache = __esm({
5585
+ "src/stages/evaluation/cache.ts"() {
5460
5586
  "use strict";
5461
- init_roboco();
5462
- EVAL_GATE_WINDOW_MS = 60 * 60 * 1e3;
5463
- STAGE_GATES = {
5464
- environment: async () => makeResult("environment.entry", []),
5465
- // 항상 통과
5466
- design: designEntryGate,
5467
- execution: executionEntryGate,
5468
- validation: validationEntryGate,
5469
- evaluation: evaluationEntryGate
5470
- };
5587
+ CACHE_DIR = ".pai";
5588
+ CACHE_FILE = "cache/evaluation.json";
5589
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5590
+ FILES_TO_HASH = [
5591
+ "package.json",
5592
+ "pyproject.toml",
5593
+ "go.mod",
5594
+ "Cargo.toml",
5595
+ "tsconfig.json",
5596
+ "jest.config.ts",
5597
+ "vitest.config.ts",
5598
+ ".github/workflows",
5599
+ ".gitlab-ci.yml",
5600
+ ".husky",
5601
+ ".lintstagedrc",
5602
+ "CLAUDE.md",
5603
+ "AGENTS.md",
5604
+ ".cursorrules",
5605
+ "README.md",
5606
+ "CONTRIBUTING.md"
5607
+ ];
5471
5608
  }
5472
5609
  });
5473
5610