pai-zero 0.13.0 → 0.13.2

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,2070 +3605,2200 @@ 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;
3764
3748
  }
3765
- var init_github_fetch = __esm({
3766
- "src/utils/github-fetch.ts"() {
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"() {
3767
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 -->";
3768
3828
  }
3769
3829
  });
3770
3830
 
3771
- // src/cli/commands/fetch.cmd.ts
3772
- var fetch_cmd_exports = {};
3773
- __export(fetch_cmd_exports, {
3774
- fetchCommand: () => fetchCommand
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
3775
3843
  });
3776
3844
  import path6 from "path";
3777
- import fs12 from "fs-extra";
3778
- import chalk4 from "chalk";
3779
- async function fetchCommand(cwd, recipeKey, options) {
3780
- if (options.list) {
3781
- section("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB808\uC2DC\uD53C");
3782
- for (const key of listRecipeKeys()) {
3783
- const r = RECIPES[key];
3784
- console.log(` ${colors.accent(key.padEnd(14))} ${r.label}`);
3785
- console.log(` ${colors.dim(" " + r.description)}`);
3786
- console.log(` ${colors.dim(" \uC18C\uC2A4: github.com/" + r.source.repo + "/tree/" + r.source.ref + "/" + r.source.path)}`);
3787
- console.log("");
3788
- }
3789
- hint("\uC0AC\uC6A9: pai fetch <key> \uB610\uB294 pai fetch --all");
3790
- return;
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
+ });
3791
3866
  }
3792
- let keys;
3793
- if (options.all) {
3794
- keys = listRecipeKeys();
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
+ });
3795
3887
  } else {
3796
- if (!recipeKey) {
3797
- error("\uB808\uC2DC\uD53C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
3798
- hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
3799
- hint("\uBAA9\uB85D: pai fetch --list");
3800
- process.exitCode = 1;
3801
- return;
3802
- }
3803
- if (!getRecipe(recipeKey)) {
3804
- error(`\uC54C \uC218 \uC5C6\uB294 \uB808\uC2DC\uD53C: ${recipeKey}`);
3805
- hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
3806
- process.exitCode = 1;
3807
- return;
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
+ });
3808
3896
  }
3809
- keys = [recipeKey.toLowerCase()];
3810
3897
  }
3811
- section("\uB808\uC2DC\uD53C \uB2E4\uC6B4\uB85C\uB4DC");
3812
- const installed = [];
3813
- for (const key of keys) {
3814
- const recipe = RECIPES[key];
3815
- const targetDir = path6.join(cwd, recipe.target);
3816
- console.log("");
3817
- console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
3818
- const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
3819
- return fetchGithubDir({
3820
- repo: recipe.source.repo,
3821
- ref: recipe.source.ref,
3822
- srcPath: recipe.source.path,
3823
- destDir: targetDir,
3824
- overwrite: options.overwrite ?? false
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"
3825
3907
  });
3826
- });
3827
- if (result.errors.length > 0) {
3828
- error(`\uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${result.errors[0].error}`);
3829
- for (const e of result.errors.slice(1)) {
3830
- console.log(chalk4.gray(` ${e.path}: ${e.error}`));
3831
- }
3832
- continue;
3833
- }
3834
- if (result.written.length > 0) {
3835
- success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
3836
- for (const f of result.written) {
3837
- console.log(chalk4.gray(` ${path6.relative(cwd, f)}`));
3838
- }
3839
- }
3840
- if (result.skipped.length > 0) {
3841
- info(`${result.skipped.length}\uAC1C \uD30C\uC77C \uAC74\uB108\uB700 (\uC774\uBBF8 \uC874\uC7AC \u2014 \uB36E\uC5B4\uC4F0\uAE30: --overwrite)`);
3842
3908
  }
3843
- await appendEnvKeys(cwd, recipe);
3844
- installed.push(key);
3845
3909
  }
3846
- if (installed.length === 0) {
3847
- return;
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
+ });
3848
3917
  }
3849
- await upsertRecipesSkill(cwd, installed);
3850
- await upsertClaudeMdBlock(cwd, installed);
3851
- console.log("");
3852
- success(`\uB808\uC2DC\uD53C ${installed.length}\uAC1C \uC124\uCE58 \uC644\uB8CC: ${installed.join(", ")}`);
3853
- console.log("");
3854
- console.log(colors.accent(" \uB2E4\uC74C \uB2E8\uACC4"));
3855
- console.log(colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3856
- console.log(` ${colors.success("1.")} ${colors.dim(".env.local \uD30C\uC77C\uC744 \uC5F4\uC5B4 \uD658\uACBD\uBCC0\uC218 \uAC12\uC744 \uCC44\uC6B0\uC138\uC694")}`);
3857
- for (const key of installed) {
3858
- const recipe = RECIPES[key];
3859
- for (const ek of recipe.envKeys) {
3860
- console.log(` ${chalk4.cyan(ek.key.padEnd(22))} ${colors.dim(ek.hint ?? "")}`);
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
+ });
3861
3937
  }
3862
3938
  }
3863
- console.log("");
3864
- console.log(` ${colors.success("2.")} ${colors.dim("Claude Code\uC5D0\uC11C \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC \uC790\uB3D9\uC73C\uB85C \uB808\uC2DC\uD53C \uBB38\uC11C\uB97C \uCC38\uC870\uD569\uB2C8\uB2E4")}`);
3865
- for (const key of installed) {
3866
- const recipe = RECIPES[key];
3867
- console.log(` ${chalk4.cyan(recipe.target + "/")}`);
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
+ });
3868
3946
  }
3869
- console.log("");
3947
+ return makeResult("validation.entry", violations);
3870
3948
  }
3871
- async function appendEnvKeys(cwd, recipe) {
3872
- if (recipe.envKeys.length === 0) return;
3873
- const envPath = path6.join(cwd, ".env.local");
3874
- let content = "";
3875
- if (await fs12.pathExists(envPath)) {
3876
- content = await fs12.readFile(envPath, "utf8");
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
+ }
3877
3973
  }
3878
- const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
3879
- if (missingKeys.length === 0) return;
3880
- const lines = [];
3881
- if (content.length > 0 && !content.endsWith("\n")) lines.push("");
3882
- lines.push("");
3883
- lines.push(`# \u2500\u2500 ${recipe.label} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
3884
- lines.push(`# \uAC00\uC774\uB4DC: ${recipe.target}/guideline.md`);
3885
- for (const ek of missingKeys) {
3886
- if (ek.hint) lines.push(`# ${ek.hint}`);
3887
- lines.push(`${ek.key}=${ek.default ?? ""}`);
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++;
3888
3995
  }
3889
- await fs12.ensureFile(envPath);
3890
- await fs12.appendFile(envPath, lines.join("\n") + "\n");
3996
+ return count;
3891
3997
  }
3892
- 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");
3896
- const recipes = installedKeys.map((k) => RECIPES[k]);
3897
- const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
3898
- const body = [
3899
- "---",
3900
- "name: recipes",
3901
- `description: "\uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C \u2014 ${recipes.map((r) => r.label).join(", ")}"`,
3902
- "---",
3903
- "",
3904
- "# \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C",
3905
- "",
3906
- "\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uB2E4\uC74C \uC0AC\uB0B4 \uC5F0\uB3D9 \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
3907
- "\uAD00\uB828 \uC694\uCCAD\uC744 \uBC1B\uC73C\uBA74 \uD574\uB2F9 \uACBD\uB85C\uC758 `guideline.md`\uB97C **\uBC18\uB4DC\uC2DC \uBA3C\uC800 \uC77D\uACE0**",
3908
- "\uC9C0\uCE68\uC5D0 \uB9DE\uAC8C \uAD6C\uD604\uD558\uC138\uC694.",
3909
- "",
3910
- "## \uD2B8\uB9AC\uAC70",
3911
- "",
3912
- "- " + triggers,
3913
- "",
3914
- "## \uC124\uCE58\uB41C \uB808\uC2DC\uD53C",
3915
- "",
3916
- ...recipes.map((r) => [
3917
- `### ${r.label}`,
3918
- "",
3919
- `- \uACBD\uB85C: \`${r.target}/\``,
3920
- `- \uC8FC\uC694 \uBB38\uC11C: \`${r.target}/guideline.md\``,
3921
- `- \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`,
3922
- ""
3923
- ].join("\n")),
3924
- "",
3925
- "## \uC791\uC5C5 \uC21C\uC11C",
3926
- "",
3927
- "1. \uC0AC\uC6A9\uC790\uC758 \uC694\uCCAD\uC774 \uC704 \uD2B8\uB9AC\uAC70 \uC911 \uD558\uB098\uC5D0 \uD574\uB2F9\uD558\uB294\uC9C0 \uD310\uB2E8",
3928
- "2. \uD574\uB2F9 \uB808\uC2DC\uD53C\uC758 `guideline.md`\uB97C Read \uB3C4\uAD6C\uB85C \uC77D\uAE30",
3929
- "3. \uC0D8\uD50C \uCF54\uB4DC(`*.html`, `*.ts` \uB4F1)\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uD655\uC778",
3930
- "4. `.env.local`\uC5D0 \uAD00\uB828 \uD658\uACBD\uBCC0\uC218\uAC00 \uCC44\uC6CC\uC838 \uC788\uB294\uC9C0 \uD655\uC778",
3931
- "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
- ""
3933
- ].join("\n");
3934
- await fs12.writeFile(skillPath, body);
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;
3935
4016
  }
3936
- async function upsertClaudeMdBlock(cwd, installedKeys) {
3937
- const claudeMdPath = path6.join(cwd, "CLAUDE.md");
3938
- const BLOCK_START = "<!-- pai:recipes:start -->";
3939
- const BLOCK_END = "<!-- pai:recipes:end -->";
3940
- const lines = [];
3941
- lines.push(BLOCK_START);
3942
- lines.push("## \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9");
3943
- lines.push("");
3944
- lines.push("\uB2E4\uC74C \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC **\uBC18\uB4DC\uC2DC** \uD574\uB2F9 \uACBD\uB85C\uC758");
3945
- lines.push("`guideline.md`\uB97C \uBA3C\uC800 \uC77D\uACE0 \uC9C0\uCE68\uC744 \uB530\uB974\uC138\uC694:");
3946
- lines.push("");
3947
- for (const key of installedKeys) {
3948
- const r = RECIPES[key];
3949
- lines.push(`- **${r.label}** \u2014 \`${r.target}/\``);
3950
- lines.push(` \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`);
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
+ }
3951
4077
  }
3952
- lines.push("");
3953
- lines.push("\uD658\uACBD\uBCC0\uC218 \uAC12\uC740 `.env.local`\uC5D0 \uCC44\uC6CC\uC838 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
3954
- lines.push(BLOCK_END);
3955
- const block = lines.join("\n");
3956
- let content = "";
3957
- if (await fs12.pathExists(claudeMdPath)) {
3958
- content = await fs12.readFile(claudeMdPath, "utf8");
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++;
3959
4096
  }
3960
- const startIdx = content.indexOf(BLOCK_START);
3961
- const endIdx = content.indexOf(BLOCK_END);
3962
- if (startIdx >= 0 && endIdx > startIdx) {
3963
- const before = content.slice(0, startIdx);
3964
- const after = content.slice(endIdx + BLOCK_END.length);
3965
- content = before + block + after;
3966
- } else {
3967
- if (content.length > 0 && !content.endsWith("\n")) content += "\n";
3968
- if (content.length > 0) content += "\n";
3969
- content += block + "\n";
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
+ }
3970
4120
  }
3971
- await fs12.ensureFile(claudeMdPath);
3972
- await fs12.writeFile(claudeMdPath, content);
4121
+ await walk(srcDir);
4122
+ return count;
3973
4123
  }
3974
- var init_fetch_cmd = __esm({
3975
- "src/cli/commands/fetch.cmd.ts"() {
4124
+ var EVAL_GATE_WINDOW_MS, STAGE_GATES;
4125
+ var init_gates = __esm({
4126
+ "src/core/gates.ts"() {
3976
4127
  "use strict";
3977
- init_ui();
3978
- init_logger();
3979
- init_recipes();
3980
- init_github_fetch();
3981
- init_progress();
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
+ };
3982
4139
  }
3983
4140
  });
3984
4141
 
3985
- // src/stages/environment/index.ts
3986
- var environmentStage;
3987
- var init_environment = __esm({
3988
- "src/stages/environment/index.ts"() {
3989
- "use strict";
3990
- init_analyzer();
3991
- init_interviewer();
3992
- init_generator();
3993
- init_installer();
3994
- init_registry();
3995
- init_claude_commands();
3996
- init_detector();
3997
- init_config();
3998
- init_progress();
3999
- init_ui();
4000
- init_analyzer();
4001
- init_interviewer();
4002
- init_doctor();
4003
- environmentStage = {
4004
- name: "environment",
4005
- canSkip(input) {
4006
- return input.config.plugins.length > 0;
4007
- },
4008
- async run(input) {
4009
- const start = Date.now();
4010
- const artifacts = [];
4011
- const errors = [];
4012
- try {
4013
- console.log("");
4014
- const analysis = await withSpinner("\uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D \uC911...", async () => {
4015
- const result = await analyzeProject(input.cwd);
4016
- await sleep(500);
4017
- return result;
4018
- });
4019
- console.log("");
4020
- if (analysis.stack.languages.length > 0) {
4021
- success(`\uC2A4\uD0DD: ${analysis.stack.languages.join(", ")}`);
4022
- }
4023
- if (analysis.stack.frameworks.length > 0) {
4024
- success(`\uD504\uB808\uC784\uC6CC\uD06C: ${analysis.stack.frameworks.join(", ")}`);
4025
- }
4026
- success(`Git: ${analysis.git.isGitRepo ? `${analysis.git.repoName ?? "local"}` : "\uBBF8\uCD08\uAE30\uD654"}`);
4027
- const interview = await runInterview(analysis, input.cwd, input.config.projectName);
4028
- console.log("");
4029
- const configFiles = [
4030
- "CLAUDE.md",
4031
- ".claude/settings.json",
4032
- "docs/vibe-coding/01-intent.md",
4033
- "docs/vibe-coding/02-requirements.md",
4034
- "docs/vibe-coding/03-research.md",
4035
- "docs/vibe-coding/04-plan.md",
4036
- "docs/vibe-coding/05-implement.md"
4037
- ];
4038
- const generated = await withFileSpinner("\uC124\uC815 \uD30C\uC77C \uC0DD\uC131 \uC911...", configFiles, async () => {
4039
- const files = await generateFiles(input.cwd, analysis, interview);
4040
- await sleep(400);
4041
- return files;
4042
- });
4043
- artifacts.push(...generated);
4044
- const basePlugins = ["github", "openspec", "roboco"];
4045
- const pluginKeys = [.../* @__PURE__ */ new Set([...basePlugins, ...interview.extraTools])];
4046
- const provCtx = {
4047
- cwd: input.cwd,
4048
- projectName: interview.projectName,
4049
- mode: interview.mode,
4050
- envEntries: {
4051
- PAI_PROJECT_NAME: interview.projectName,
4052
- PAI_MODE: interview.mode
4053
- },
4054
- analysis,
4055
- mcp: interview.mcp
4056
- };
4057
- if (interview.authMethods.includes("custom")) {
4058
- provCtx.envEntries["OAUTH_CLIENT_ID"] = interview.customAuth?.clientId || "YOUR_CLIENT_ID_HERE";
4059
- provCtx.envEntries["OAUTH_CLIENT_SECRET"] = interview.customAuth?.clientSecret || "YOUR_CLIENT_SECRET_HERE";
4060
- provCtx.envEntries["OAUTH_REDIRECT_URI"] = "http://localhost:3000/auth/callback";
4061
- }
4062
- const pluginFileNames = [
4063
- ".gitignore",
4064
- "src/",
4065
- "docs/",
4066
- "tests/",
4067
- "public/",
4068
- "docs/openspec.md",
4069
- ".pai/roboco.json",
4070
- ...interview.extraTools.includes("omc") ? [".pai/omc.md", ".omc/"] : [],
4071
- ...interview.extraTools.includes("vercel") ? ["vercel.json"] : [],
4072
- ...interview.extraTools.includes("supabase") ? ["supabase/config.toml"] : [],
4073
- ...interview.extraTools.includes("gstack") ? [".pai/gstack.json"] : [],
4074
- ...interview.extraTools.includes("harness") ? [".pai/harness.json"] : [],
4075
- ...interview.extraTools.includes("mcp") ? ["mcp-server/", ".mcp.json"] : [],
4076
- ".env.local"
4077
- ];
4078
- console.log("");
4079
- await withFileSpinner("\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870 \uC124\uCE58 \uC911...", pluginFileNames, async () => {
4080
- await runProvisioners(pluginKeys, provCtx);
4081
- await sleep(500);
4082
- });
4083
- const cmdFiles = [
4084
- "SKILL.md",
4085
- "info.md",
4086
- "init.md",
4087
- "status.md",
4088
- "doctor.md",
4089
- "design.md",
4090
- "validate.md",
4091
- "evaluate.md",
4092
- "install.md"
4093
- ];
4094
- console.log("");
4095
- await withFileSpinner("\uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC124\uCE58 \uC911...", cmdFiles, async () => {
4096
- await provisionClaudeCommands(input.cwd);
4097
- await sleep(400);
4098
- });
4099
- console.log("");
4100
- const installResults = await installTools(interview.tools, input.cwd);
4101
- printInstallReport(installResults, interview.tools);
4102
- if (interview.recipes && interview.recipes.length > 0) {
4103
- console.log("");
4104
- const { fetchCommand: fetchCommand2 } = await Promise.resolve().then(() => (init_fetch_cmd(), fetch_cmd_exports));
4105
- for (const recipeKey of interview.recipes) {
4106
- try {
4107
- await fetchCommand2(input.cwd, recipeKey, {});
4108
- } catch (err) {
4109
- const msg = err instanceof Error ? err.message : String(err);
4110
- warn(`\uB808\uC2DC\uD53C '${recipeKey}' \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 \u2014 ${msg}`);
4111
- hint(`\uB098\uC911\uC5D0 \uC218\uB3D9: pai fetch ${recipeKey}`);
4112
- }
4113
- }
4114
- }
4115
- console.log("");
4116
- await withSpinner("\uC124\uC815 \uC800\uC7A5 \uC911...", async () => {
4117
- const config = createDefaultConfig(interview.projectName, interview.mode);
4118
- config.plugins = pluginKeys;
4119
- config.edges = deriveEdges(pluginKeys);
4120
- if (interview.mcp) config.mcp = interview.mcp;
4121
- await saveConfig(input.cwd, config);
4122
- await sleep(300);
4123
- });
4124
- return {
4125
- stage: "environment",
4126
- status: "success",
4127
- data: { analysis, interview, installResults },
4128
- artifacts,
4129
- duration: Date.now() - start,
4130
- errors
4131
- };
4132
- } catch (err) {
4133
- const msg = err instanceof Error ? err.message : String(err);
4134
- errors.push({ code: "ENV_SETUP_FAILED", message: msg, recoverable: true });
4135
- return {
4136
- stage: "environment",
4137
- status: "failed",
4138
- data: {},
4139
- artifacts,
4140
- duration: Date.now() - start,
4141
- errors
4142
- };
4143
- }
4144
- }
4145
- };
4146
- }
4147
- });
4148
-
4149
- // src/stages/evaluation/prompts/analyze.ts
4150
- var analyze_exports = {};
4151
- __export(analyze_exports, {
4152
- buildAnalysisPrompt: () => buildAnalysisPrompt
4142
+ // src/stages/environment/doctor.ts
4143
+ var doctor_exports = {};
4144
+ __export(doctor_exports, {
4145
+ runDoctor: () => runDoctor
4153
4146
  });
4154
- function buildAnalysisPrompt(repoPath) {
4155
- return `Analyze the repository at "${repoPath}" for vibe coding readiness.
4156
-
4157
- Use only Read, Glob, and Grep tools. Complete in ~20 tool calls.
4158
- Focus on config files, not source code.
4159
-
4160
- Evaluate these 6 categories:
4161
-
4162
- ### Must-Have (60% weight)
4163
- 1. **\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0**: test framework config, test dirs, coverage thresholds
4164
- - 0=none, 20=config only, 40=minimal files, 60=moderate, 80=good, 100=comprehensive
4165
- 2. **CI/CD**: pipeline configs, quality gates, multi-stage
4166
- - 0=none, 20=exists but minimal, 40=basic, 60=tests+lint, 80=tests+lint+build, 100=comprehensive
4167
- 3. **\uD6C5 \uAE30\uBC18 \uAC80\uC99D**: git hooks, lint-staged, AI agent hooks (.claude/settings.json)
4168
- - 0=none, 20=framework only, 40=single hook, 60=lint+format, 80=lint+test+format, 100=comprehensive+AI
4169
-
4170
- ### Nice-to-Have (40% weight)
4171
- 4. **\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870**: dir organization, deps management, env config
4172
- - 0=flat/chaotic, 20=minimal, 40=basic, 60=reasonable, 80=well-organized, 100=exemplary
4173
- 5. **\uBB38\uC11C\uD654 \uC218\uC900**: README, CONTRIBUTING, API docs, architecture docs
4174
- - 0=none, 20=minimal, 40=basic, 60=good README, 80=README+API, 100=comprehensive
4175
- 6. **\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1**: CLAUDE.md, AGENTS.md, .claude/ config, skills, commands
4176
- - 0=none, 20=basic, 40=one AI config, 60=context+config, 80=context+safety+skills, 100=comprehensive
4177
-
4178
- Output ONLY a JSON block:
4179
- \`\`\`json
4180
- {
4181
- "categories": [
4182
- {
4183
- "name": "\uCE74\uD14C\uACE0\uB9AC\uBA85 (Korean)",
4184
- "tier": "must" | "nice",
4185
- "score": 0-100,
4186
- "recommendations": [{ "severity": "critical"|"warning"|"info", "message": "...", "action": "..." }],
4187
- "rawFindings": [{ "item": "filename or concept", "found": true|false, "details": "..." }]
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
+ }
4188
4219
  }
4189
- ],
4190
- "summary": "2-3 sentence Korean summary"
4220
+ }
4221
+ console.log("");
4222
+ info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
4223
+ if (passed < checks.length) {
4224
+ process.exitCode = 1;
4225
+ }
4191
4226
  }
4192
- \`\`\``;
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
+ }
4193
4235
  }
4194
- var init_analyze = __esm({
4195
- "src/stages/evaluation/prompts/analyze.ts"() {
4236
+ var init_doctor = __esm({
4237
+ "src/stages/environment/doctor.ts"() {
4196
4238
  "use strict";
4239
+ init_ui();
4197
4240
  }
4198
4241
  });
4199
4242
 
4200
- // src/stages/evaluation/analyzer.ts
4201
- var analyzer_exports2 = {};
4202
- __export(analyzer_exports2, {
4203
- analyzeRepository: () => analyzeRepository
4204
- });
4205
- import { join as join7 } from "path";
4243
+ // src/utils/github-fetch.ts
4244
+ import path7 from "path";
4206
4245
  import fs13 from "fs-extra";
4207
- async function analyzeRepository(repoPath) {
4246
+ async function httpGet(url, timeoutMs, accept) {
4247
+ const controller = new AbortController();
4248
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4208
4249
  try {
4209
- return await aiAnalysis(repoPath);
4210
- } catch {
4211
- return staticAnalysis(repoPath);
4250
+ return await fetch(url, {
4251
+ signal: controller.signal,
4252
+ headers: { "Accept": accept, "User-Agent": "pai-zero" }
4253
+ });
4254
+ } finally {
4255
+ clearTimeout(timer);
4212
4256
  }
4213
4257
  }
4214
- async function aiAnalysis(repoPath) {
4215
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
4216
- const { buildAnalysisPrompt: buildAnalysisPrompt2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
4217
- const prompt = buildAnalysisPrompt2(repoPath);
4218
- let resultText = "";
4219
- for await (const message of query({
4220
- prompt,
4221
- options: {
4222
- maxTurns: 200,
4223
- systemPrompt: "You are a vibe coding readiness analyzer. Analyze the repository and output JSON.",
4224
- allowedTools: ["Read", "Glob", "Grep"],
4225
- permissionMode: "plan"
4226
- }
4227
- })) {
4228
- if ("result" in message) {
4229
- resultText = message.result;
4230
- }
4231
- }
4232
- return parseJsonFromText(resultText);
4233
- }
4234
- function parseJsonFromText(text) {
4235
- const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
4236
- if (jsonMatch?.[1]) {
4237
- return JSON.parse(jsonMatch[1]);
4238
- }
4239
- return JSON.parse(text);
4240
- }
4241
- async function staticAnalysis(repoPath) {
4242
- const categories = [];
4243
- categories.push(await checkTestCoverage(repoPath));
4244
- categories.push(await checkCiCd(repoPath));
4245
- categories.push(await checkHooks(repoPath));
4246
- categories.push(await checkRepoStructure(repoPath));
4247
- categories.push(await checkDocumentation(repoPath));
4248
- categories.push(await checkHarnessEngineering(repoPath));
4249
- return {
4250
- categories,
4251
- summary: `\uC815\uC801 \uBD84\uC11D \uC644\uB8CC. ${categories.filter((c2) => c2.score >= 60).length}/6 \uCE74\uD14C\uACE0\uB9AC \uD1B5\uACFC.`
4252
- };
4253
- }
4254
- async function checkTestCoverage(repoPath) {
4255
- const findings = [];
4256
- let score = 0;
4257
- const testConfigs = [
4258
- "jest.config.ts",
4259
- "jest.config.js",
4260
- "vitest.config.ts",
4261
- "vitest.config.js",
4262
- "pytest.ini",
4263
- "phpunit.xml",
4264
- ".nycrc"
4265
- ];
4266
- for (const f of testConfigs) {
4267
- const found = await fs13.pathExists(join7(repoPath, f));
4268
- findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4269
- if (found) score += 20;
4270
- }
4271
- const testDirs = ["tests", "test", "__tests__", "spec"];
4272
- let hasTestDir = false;
4273
- for (const d of testDirs) {
4274
- if (await fs13.pathExists(join7(repoPath, d))) {
4275
- findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
4276
- hasTestDir = true;
4277
- score += 30;
4278
- break;
4279
- }
4280
- }
4281
- if (!hasTestDir) {
4282
- findings.push({ item: "test directory", found: false, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC5C6\uC74C" });
4283
- }
4284
- score = Math.min(100, score);
4285
- const recommendations = score < 60 ? [{ severity: "critical", message: "\uD14C\uC2A4\uD2B8 \uD658\uACBD \uBBF8\uAD6C\uC131", action: "\uD14C\uC2A4\uD2B8 \uD504\uB808\uC784\uC6CC\uD06C\uC640 \uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC\uB97C \uCD94\uAC00\uD558\uC138\uC694" }] : [];
4286
- return { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", score, recommendations, rawFindings: findings };
4287
- }
4288
- async function checkCiCd(repoPath) {
4289
- const findings = [];
4290
- let score = 0;
4291
- const ciConfigs = [
4292
- { path: ".github/workflows", label: "GitHub Actions" },
4293
- { path: ".gitlab-ci.yml", label: "GitLab CI" },
4294
- { path: "Jenkinsfile", label: "Jenkins" },
4295
- { path: ".circleci", label: "CircleCI" }
4296
- ];
4297
- for (const { path: path10, label } of ciConfigs) {
4298
- const found = await fs13.pathExists(join7(repoPath, path10));
4299
- findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4300
- if (found) score += 40;
4301
- }
4302
- score = Math.min(100, score);
4303
- const recommendations = score === 0 ? [{ severity: "critical", message: "CI/CD \uBBF8\uAD6C\uC131", action: "GitHub Actions \uB610\uB294 \uB2E4\uB978 CI \uD30C\uC774\uD504\uB77C\uC778\uC744 \uC124\uC815\uD558\uC138\uC694" }] : [];
4304
- return { name: "CI/CD", tier: "must", score, recommendations, rawFindings: findings };
4305
- }
4306
- async function checkHooks(repoPath) {
4307
- const findings = [];
4308
- let score = 0;
4309
- const hookConfigs = [
4310
- { path: ".husky", label: "Husky" },
4311
- { path: ".lintstagedrc", label: "lint-staged" },
4312
- { path: ".lintstagedrc.json", label: "lint-staged (json)" },
4313
- { path: "commitlint.config.js", label: "commitlint" },
4314
- { path: ".claude/settings.json", label: "Claude Code settings" }
4315
- ];
4316
- for (const { path: path10, label } of hookConfigs) {
4317
- const found = await fs13.pathExists(join7(repoPath, path10));
4318
- findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4319
- if (found) score += 20;
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})`);
4320
4263
  }
4321
- score = Math.min(100, score);
4322
- const recommendations = score < 40 ? [{ severity: "warning", message: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D \uBD80\uC871", action: "Husky + lint-staged\uB97C \uC124\uC815\uD558\uC5EC \uCEE4\uBC0B \uC804 \uAC80\uC99D\uC744 \uCD94\uAC00\uD558\uC138\uC694" }] : [];
4323
- return { name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", score, recommendations, rawFindings: findings };
4324
- }
4325
- async function checkRepoStructure(repoPath) {
4326
- const findings = [];
4327
- let score = 0;
4328
- const structureChecks = [
4329
- { path: "src", label: "src/ \uB514\uB809\uD1A0\uB9AC" },
4330
- { path: "package.json", label: "\uC758\uC874\uC131 \uAD00\uB9AC" },
4331
- { path: ".env.example", label: "\uD658\uACBD\uBCC0\uC218 \uC608\uC2DC" },
4332
- { path: ".gitignore", label: ".gitignore" }
4333
- ];
4334
- for (const { path: path10, label } of structureChecks) {
4335
- const found = await fs13.pathExists(join7(repoPath, path10));
4336
- findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4337
- if (found) score += 25;
4264
+ const data = await res.json();
4265
+ if (!Array.isArray(data)) {
4266
+ throw new Error(`Expected directory, got single entry at ${dirPath}`);
4338
4267
  }
4339
- score = Math.min(100, score);
4340
- return { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", score, recommendations: [], rawFindings: findings };
4268
+ return data;
4341
4269
  }
4342
- async function checkDocumentation(repoPath) {
4343
- const findings = [];
4344
- let score = 0;
4345
- const docChecks = [
4346
- { path: "README.md", label: "README.md", points: 30 },
4347
- { path: "CONTRIBUTING.md", label: "CONTRIBUTING.md", points: 20 },
4348
- { path: "docs", label: "docs/ \uB514\uB809\uD1A0\uB9AC", points: 25 },
4349
- { path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
4350
- ];
4351
- for (const { path: path10, label, points } of docChecks) {
4352
- const found = await fs13.pathExists(join7(repoPath, path10));
4353
- findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4354
- if (found) score += points;
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})`);
4355
4274
  }
4356
- score = Math.min(100, score);
4357
- return { name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", score, recommendations: [], rawFindings: findings };
4275
+ const buf = Buffer.from(await res.arrayBuffer());
4276
+ await fs13.ensureDir(path7.dirname(destPath));
4277
+ await fs13.writeFile(destPath, buf);
4358
4278
  }
4359
- async function checkHarnessEngineering(repoPath) {
4360
- const findings = [];
4361
- let score = 0;
4362
- const harnessChecks = [
4363
- { path: "CLAUDE.md", label: "CLAUDE.md", points: 25 },
4364
- { path: "AGENTS.md", label: "AGENTS.md", points: 15 },
4365
- { path: ".claude/settings.json", label: ".claude/settings.json", points: 20 },
4366
- { path: ".claude/skills", label: ".claude/skills/", points: 20 },
4367
- { path: ".claude/commands", label: ".claude/commands/", points: 10 },
4368
- { path: ".pai/config.json", label: "PAI config", points: 10 }
4369
- ];
4370
- for (const { path: path10, label, points } of harnessChecks) {
4371
- const found = await fs13.pathExists(join7(repoPath, path10));
4372
- findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4373
- if (found) score += points;
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
+ }
4374
4321
  }
4375
- score = Math.min(100, score);
4376
- return { name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", score, recommendations: [], rawFindings: findings };
4322
+ await walk(srcPath, destDir);
4323
+ return result;
4377
4324
  }
4378
- var init_analyzer2 = __esm({
4379
- "src/stages/evaluation/analyzer.ts"() {
4380
- "use strict";
4381
- }
4382
- });
4383
-
4384
- // src/core/types/stage.ts
4385
- var STAGE_ORDER;
4386
- var init_stage = __esm({
4387
- "src/core/types/stage.ts"() {
4325
+ var init_github_fetch = __esm({
4326
+ "src/utils/github-fetch.ts"() {
4388
4327
  "use strict";
4389
- STAGE_ORDER = [
4390
- "environment",
4391
- "design",
4392
- "execution",
4393
- "validation",
4394
- "evaluation"
4395
- ];
4396
4328
  }
4397
4329
  });
4398
4330
 
4399
- // src/core/types/scoring.ts
4400
- function gradeFromScore(score) {
4401
- if (score >= 90) return "A";
4402
- if (score >= 80) return "B";
4403
- if (score >= 70) return "C";
4404
- if (score >= 50) return "D";
4405
- return "F";
4406
- }
4407
- var DEFAULT_CATEGORY_WEIGHTS;
4408
- var init_scoring = __esm({
4409
- "src/core/types/scoring.ts"() {
4410
- "use strict";
4411
- DEFAULT_CATEGORY_WEIGHTS = [
4412
- { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", weight: 0.2 },
4413
- { name: "CI/CD", tier: "must", weight: 0.2 },
4414
- { name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", weight: 0.2 },
4415
- { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", weight: 0.133 },
4416
- { name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", weight: 0.133 },
4417
- { name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", weight: 0.134 }
4418
- ];
4419
- }
4331
+ // src/cli/commands/fetch.cmd.ts
4332
+ var fetch_cmd_exports = {};
4333
+ __export(fetch_cmd_exports, {
4334
+ fetchCommand: () => fetchCommand
4420
4335
  });
4421
-
4422
- // src/core/types/index.ts
4423
- var init_types = __esm({
4424
- "src/core/types/index.ts"() {
4425
- "use strict";
4426
- init_stage();
4427
- init_scoring();
4336
+ import path8 from "path";
4337
+ import fs14 from "fs-extra";
4338
+ import chalk4 from "chalk";
4339
+ async function fetchCommand(cwd, recipeKey, options) {
4340
+ if (options.list) {
4341
+ section("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB808\uC2DC\uD53C");
4342
+ for (const key of listRecipeKeys()) {
4343
+ const r = RECIPES[key];
4344
+ console.log(` ${colors.accent(key.padEnd(14))} ${r.label}`);
4345
+ console.log(` ${colors.dim(" " + r.description)}`);
4346
+ console.log(` ${colors.dim(" \uC18C\uC2A4: github.com/" + r.source.repo + "/tree/" + r.source.ref + "/" + r.source.path)}`);
4347
+ console.log("");
4348
+ }
4349
+ hint("\uC0AC\uC6A9: pai fetch <key> \uB610\uB294 pai fetch --all");
4350
+ return;
4428
4351
  }
4429
- });
4430
-
4431
- // src/stages/evaluation/scorer.ts
4432
- var scorer_exports = {};
4433
- __export(scorer_exports, {
4434
- computeResult: () => computeResult
4435
- });
4436
- function computeResult(llmOutput, customWeights) {
4437
- const weights = customWeights ?? DEFAULT_CATEGORY_WEIGHTS;
4438
- const categories = llmOutput.categories.map((cat) => ({
4439
- name: cat.name,
4440
- tier: cat.tier,
4441
- score: Math.round(Math.max(0, Math.min(100, cat.score))),
4442
- grade: gradeFromScore(Math.max(0, Math.min(100, cat.score))),
4443
- recommendations: cat.recommendations,
4444
- rawFindings: cat.rawFindings
4445
- }));
4446
- const totalScore = computeWeightedAverage(categories, weights);
4447
- let totalGrade = gradeFromScore(totalScore);
4448
- const { penaltyApplied, penaltyReason } = checkPenalty(categories);
4449
- if (penaltyApplied && gradeRank(totalGrade) > gradeRank("C")) {
4450
- totalGrade = "C";
4451
- }
4452
- return {
4453
- categories,
4454
- totalScore,
4455
- totalGrade,
4456
- summary: llmOutput.summary,
4457
- penaltyApplied,
4458
- penaltyReason
4459
- };
4460
- }
4461
- function computeWeightedAverage(categories, weights) {
4462
- let weightedSum = 0;
4463
- let totalWeight = 0;
4464
- for (const cat of categories) {
4465
- const config = weights.find((w) => w.name === cat.name);
4466
- const weight = config?.weight ?? (cat.tier === "must" ? 0.2 : 0.133);
4467
- weightedSum += cat.score * weight;
4468
- totalWeight += weight;
4352
+ let keys;
4353
+ if (options.all) {
4354
+ keys = listRecipeKeys();
4355
+ } else {
4356
+ if (!recipeKey) {
4357
+ error("\uB808\uC2DC\uD53C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
4358
+ hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
4359
+ hint("\uBAA9\uB85D: pai fetch --list");
4360
+ process.exitCode = 1;
4361
+ return;
4362
+ }
4363
+ if (!getRecipe(recipeKey)) {
4364
+ error(`\uC54C \uC218 \uC5C6\uB294 \uB808\uC2DC\uD53C: ${recipeKey}`);
4365
+ hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
4366
+ process.exitCode = 1;
4367
+ return;
4368
+ }
4369
+ keys = [recipeKey.toLowerCase()];
4469
4370
  }
4470
- if (totalWeight === 0) return 0;
4471
- return Math.round(weightedSum / totalWeight * 100) / 100;
4472
- }
4473
- function checkPenalty(categories) {
4474
- const failedMust = categories.filter(
4475
- (c2) => c2.tier === "must" && c2.grade === "F"
4476
- );
4477
- if (failedMust.length > 0) {
4478
- const names = failedMust.map((c2) => c2.name).join(", ");
4479
- return {
4480
- penaltyApplied: true,
4481
- penaltyReason: `\uD544\uC218 \uCE74\uD14C\uACE0\uB9AC F \uB4F1\uAE09: ${names} \u2192 \uC804\uCCB4 \uB4F1\uAE09 \uCD5C\uB300 C\uB85C \uC81C\uD55C`
4482
- };
4371
+ section("\uB808\uC2DC\uD53C \uB2E4\uC6B4\uB85C\uB4DC");
4372
+ const installed = [];
4373
+ for (const key of keys) {
4374
+ const recipe = RECIPES[key];
4375
+ const targetDir = path8.join(cwd, recipe.target);
4376
+ console.log("");
4377
+ console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
4378
+ const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
4379
+ return fetchGithubDir({
4380
+ repo: recipe.source.repo,
4381
+ ref: recipe.source.ref,
4382
+ srcPath: recipe.source.path,
4383
+ destDir: targetDir,
4384
+ overwrite: options.overwrite ?? false
4385
+ });
4386
+ });
4387
+ if (result.errors.length > 0) {
4388
+ error(`\uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${result.errors[0].error}`);
4389
+ for (const e of result.errors.slice(1)) {
4390
+ console.log(chalk4.gray(` ${e.path}: ${e.error}`));
4391
+ }
4392
+ continue;
4393
+ }
4394
+ if (result.written.length > 0) {
4395
+ success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
4396
+ for (const f of result.written) {
4397
+ console.log(chalk4.gray(` ${path8.relative(cwd, f)}`));
4398
+ }
4399
+ }
4400
+ if (result.skipped.length > 0) {
4401
+ info(`${result.skipped.length}\uAC1C \uD30C\uC77C \uAC74\uB108\uB700 (\uC774\uBBF8 \uC874\uC7AC \u2014 \uB36E\uC5B4\uC4F0\uAE30: --overwrite)`);
4402
+ }
4403
+ await appendEnvKeys(cwd, recipe);
4404
+ installed.push(key);
4483
4405
  }
4484
- return { penaltyApplied: false };
4485
- }
4486
- function gradeRank(grade) {
4487
- const ranks = { A: 4, B: 3, C: 2, D: 1, F: 0 };
4488
- return ranks[grade];
4489
- }
4490
- var init_scorer = __esm({
4491
- "src/stages/evaluation/scorer.ts"() {
4492
- "use strict";
4493
- init_types();
4406
+ if (installed.length === 0) {
4407
+ return;
4494
4408
  }
4495
- });
4496
-
4497
- // src/stages/evaluation/reporter.ts
4498
- var reporter_exports = {};
4499
- __export(reporter_exports, {
4500
- buildDetailedReport: () => buildDetailedReport,
4501
- buildMarkdownReport: () => buildMarkdownReport,
4502
- printReport: () => printReport,
4503
- printVerboseFindings: () => printVerboseFindings
4504
- });
4505
- import chalk5 from "chalk";
4506
- function printReport(result) {
4409
+ await upsertRecipesSkill(cwd, installed);
4410
+ await upsertClaudeMdBlock(cwd, installed);
4507
4411
  console.log("");
4508
- console.log(chalk5.hex("#7B93DB")(" PAI Evaluation Report"));
4509
- console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4412
+ success(`\uB808\uC2DC\uD53C ${installed.length}\uAC1C \uC124\uCE58 \uC644\uB8CC: ${installed.join(", ")}`);
4510
4413
  console.log("");
4511
- const gradeColor = GRADE_COLORS[result.totalGrade];
4512
- console.log(` \uC885\uD569 \uC810\uC218: ${gradeColor(String(result.totalScore))} / 100 \uB4F1\uAE09: ${gradeColor(result.totalGrade)}`);
4513
- if (result.penaltyApplied) {
4514
- console.log(chalk5.red(` \u26A0 ${result.penaltyReason}`));
4414
+ console.log(colors.accent(" \uB2E4\uC74C \uB2E8\uACC4"));
4415
+ console.log(colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4416
+ console.log(` ${colors.success("1.")} ${colors.dim(".env.local \uD30C\uC77C\uC744 \uC5F4\uC5B4 \uD658\uACBD\uBCC0\uC218 \uAC12\uC744 \uCC44\uC6B0\uC138\uC694")}`);
4417
+ for (const key of installed) {
4418
+ const recipe = RECIPES[key];
4419
+ for (const ek of recipe.envKeys) {
4420
+ console.log(` ${chalk4.cyan(ek.key.padEnd(22))} ${colors.dim(ek.hint ?? "")}`);
4421
+ }
4515
4422
  }
4516
4423
  console.log("");
4517
- console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4518
- for (const cat of result.categories) {
4519
- const color = GRADE_COLORS[cat.grade];
4520
- const tierLabel = cat.tier === "must" ? chalk5.hex("#E06C75")("[\uD544\uC218]") : chalk5.gray("[\uC120\uD0DD]");
4521
- const bar = renderBar(cat.score);
4522
- console.log(` ${tierLabel} ${cat.name.padEnd(16)} ${bar} ${color(`${cat.score}`).padStart(12)} ${color(cat.grade)}`);
4424
+ console.log(` ${colors.success("2.")} ${colors.dim("Claude Code\uC5D0\uC11C \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC \uC790\uB3D9\uC73C\uB85C \uB808\uC2DC\uD53C \uBB38\uC11C\uB97C \uCC38\uC870\uD569\uB2C8\uB2E4")}`);
4425
+ for (const key of installed) {
4426
+ const recipe = RECIPES[key];
4427
+ console.log(` ${chalk4.cyan(recipe.target + "/")}`);
4523
4428
  }
4524
4429
  console.log("");
4525
- console.log(chalk5.gray(` ${result.summary}`));
4526
- console.log("");
4527
- }
4528
- function printVerboseFindings(result) {
4529
- for (const cat of result.categories) {
4530
- console.log("");
4531
- console.log(chalk5.bold(` ${cat.name} (${cat.grade}, ${cat.score}\uC810)`));
4532
- for (const f of cat.rawFindings) {
4533
- const icon = f.found ? chalk5.green("\u2713") : chalk5.red("\u2717");
4534
- console.log(` ${icon} ${f.item} \u2014 ${f.details}`);
4535
- }
4536
- for (const r of cat.recommendations) {
4537
- const severity = r.severity === "critical" ? chalk5.red("!") : r.severity === "warning" ? chalk5.yellow("!") : chalk5.gray("i");
4538
- console.log(` ${severity} ${r.message}`);
4539
- console.log(chalk5.gray(` \u2192 ${r.action}`));
4540
- }
4541
- }
4542
4430
  }
4543
- function renderBar(score) {
4544
- const width = 20;
4545
- const filled = Math.round(score / 100 * width);
4546
- const empty = width - filled;
4547
- const color = score >= 80 ? chalk5.hex("#6BCB77") : score >= 60 ? chalk5.hex("#E2B340") : chalk5.hex("#E06C75");
4548
- return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
4549
- }
4550
- function buildMarkdownReport(result) {
4551
- const lines = [
4552
- "# PAI Evaluation Report",
4553
- "",
4554
- `> \uC0DD\uC131\uC77C\uC2DC: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}`,
4555
- "",
4556
- `## \uC885\uD569 \uC810\uC218: ${result.totalScore}/100 \u2014 \uB4F1\uAE09 ${result.totalGrade}`,
4557
- ""
4558
- ];
4559
- if (result.penaltyApplied) {
4560
- lines.push(`> \u26A0 ${result.penaltyReason}`);
4561
- lines.push("");
4562
- }
4563
- lines.push("## \uCE74\uD14C\uACE0\uB9AC\uBCC4 \uC0C1\uC138");
4564
- lines.push("");
4565
- lines.push("| \uCE74\uD14C\uACE0\uB9AC | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 |");
4566
- lines.push("|---------|------|------|------|");
4567
- for (const cat of result.categories) {
4568
- const tier = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
4569
- lines.push(`| ${cat.name} | ${tier} | ${cat.score} | ${cat.grade} |`);
4431
+ async function appendEnvKeys(cwd, recipe) {
4432
+ if (recipe.envKeys.length === 0) return;
4433
+ const envPath = path8.join(cwd, ".env.local");
4434
+ let content = "";
4435
+ if (await fs14.pathExists(envPath)) {
4436
+ content = await fs14.readFile(envPath, "utf8");
4570
4437
  }
4438
+ const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
4439
+ if (missingKeys.length === 0) return;
4440
+ const lines = [];
4441
+ if (content.length > 0 && !content.endsWith("\n")) lines.push("");
4571
4442
  lines.push("");
4572
- lines.push("## \uAD8C\uACE0\uC0AC\uD56D");
4573
- lines.push("");
4574
- const allRecs = result.categories.flatMap(
4575
- (c2) => c2.recommendations.map((r) => ({ ...r, category: c2.name }))
4576
- );
4577
- if (allRecs.length === 0) {
4578
- lines.push("\uBAA8\uB4E0 \uD56D\uBAA9\uC774 \uC591\uD638\uD569\uB2C8\uB2E4!");
4579
- } else {
4580
- for (const r of allRecs) {
4581
- const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
4582
- lines.push(`- ${icon} **${r.category}**: ${r.message}`);
4583
- lines.push(` - ${r.action}`);
4584
- }
4443
+ lines.push(`# \u2500\u2500 ${recipe.label} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
4444
+ lines.push(`# \uAC00\uC774\uB4DC: ${recipe.target}/guideline.md`);
4445
+ for (const ek of missingKeys) {
4446
+ if (ek.hint) lines.push(`# ${ek.hint}`);
4447
+ lines.push(`${ek.key}=${ek.default ?? ""}`);
4585
4448
  }
4586
- lines.push("");
4587
- lines.push(`## \uC694\uC57D`);
4588
- lines.push("");
4589
- lines.push(result.summary);
4590
- lines.push("");
4591
- lines.push("---");
4592
- lines.push("*Generated by PAI (Plugin-based AI)*");
4593
- return lines.join("\n") + "\n";
4449
+ await fs14.ensureFile(envPath);
4450
+ await fs14.appendFile(envPath, lines.join("\n") + "\n");
4594
4451
  }
4595
- function buildDetailedReport(result, projectName) {
4596
- const now2 = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
4597
- const mustCats = result.categories.filter((c2) => c2.tier === "must");
4598
- const niceCats = result.categories.filter((c2) => c2.tier === "nice");
4599
- const lines = [
4600
- `# ${projectName} \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uBD84\uC11D \uB9AC\uD3EC\uD2B8`,
4452
+ async function upsertRecipesSkill(cwd, installedKeys) {
4453
+ const skillDir = path8.join(cwd, ".claude", "skills", "recipes");
4454
+ await fs14.ensureDir(skillDir);
4455
+ const skillPath = path8.join(skillDir, "SKILL.md");
4456
+ const recipes = installedKeys.map((k) => RECIPES[k]);
4457
+ const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
4458
+ const body = [
4459
+ "---",
4460
+ "name: recipes",
4461
+ `description: "\uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C \u2014 ${recipes.map((r) => r.label).join(", ")}"`,
4462
+ "---",
4601
4463
  "",
4602
- `> \uC2A4\uCE94 \uC77C\uC2DC: ${now2}`,
4603
- `> \uBD84\uC11D \uB300\uC0C1: ${projectName}`,
4464
+ "# \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C",
4604
4465
  "",
4605
- "---",
4466
+ "\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uB2E4\uC74C \uC0AC\uB0B4 \uC5F0\uB3D9 \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
4467
+ "\uAD00\uB828 \uC694\uCCAD\uC744 \uBC1B\uC73C\uBA74 \uD574\uB2F9 \uACBD\uB85C\uC758 `guideline.md`\uB97C **\uBC18\uB4DC\uC2DC \uBA3C\uC800 \uC77D\uACE0**",
4468
+ "\uC9C0\uCE68\uC5D0 \uB9DE\uAC8C \uAD6C\uD604\uD558\uC138\uC694.",
4606
4469
  "",
4607
- "## \uC694\uC57D",
4470
+ "## \uD2B8\uB9AC\uAC70",
4608
4471
  "",
4609
- `${projectName}\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uC885\uD569 \uC810\uC218\uB294 **${result.totalScore}/100 (${result.totalGrade}\uB4F1\uAE09)**\uC785\uB2C8\uB2E4.`,
4610
- ""
4611
- ];
4612
- if (result.penaltyApplied) {
4613
- lines.push(`> \u26A0 ${result.penaltyReason}`);
4614
- lines.push("");
4615
- }
4616
- if (result.totalGrade === "A") {
4617
- lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uD65C\uC6A9\uD560 \uC900\uBE44\uAC00 \uC798 \uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
4618
- } else if (result.totalGrade === "B") {
4619
- lines.push("\uB300\uBD80\uBD84 \uC900\uBE44\uB418\uC5B4 \uC788\uC73C\uBA70, \uC77C\uBD80 \uAC1C\uC120\uC73C\uB85C \uCD5C\uC801\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC744 \uAD6C\uCD95\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
4620
- } else if (result.totalGrade === "C") {
4621
- lines.push("\uAE30\uBCF8\uC801\uC778 \uAD6C\uC870\uB294 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098, \uBC14\uC774\uBE0C \uCF54\uB529\uC758 \uC7A0\uC7AC\uB825\uC744 \uCDA9\uBD84\uD788 \uD65C\uC6A9\uD558\uB824\uBA74 \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
4622
- } else if (result.totalGrade === "D") {
4623
- lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uBCF8\uACA9\uC801\uC73C\uB85C \uD65C\uC6A9\uD558\uAE30\uC5D0\uB294 \uC0C1\uB2F9\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.");
4624
- } else {
4625
- lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uAC70\uC758 \uAD6C\uC131\uB418\uC9C0 \uC54A\uC740 \uC0C1\uD0DC\uB85C, \uAE30\uBCF8 \uC124\uC815\uBD80\uD130 \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.");
4626
- }
4627
- lines.push("");
4628
- lines.push("---");
4629
- lines.push("");
4630
- lines.push("## \uD56D\uBAA9\uBCC4 \uD604\uD669");
4631
- lines.push("");
4632
- lines.push("| \uD56D\uBAA9 | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 | \uAC00\uC911\uCE58 |");
4633
- lines.push("|------|------|------|------|--------|");
4634
- for (const cat of result.categories) {
4635
- const tier = cat.tier === "must" ? "**\uD544\uC218**" : "\uC120\uD0DD";
4636
- lines.push(`| ${cat.name} | ${tier} | ${cat.score}/100 | ${cat.grade} | ${cat.tier === "must" ? "20%" : "13.3%"} |`);
4637
- }
4638
- lines.push("");
4639
- lines.push("---");
4472
+ "- " + triggers,
4473
+ "",
4474
+ "## \uC124\uCE58\uB41C \uB808\uC2DC\uD53C",
4475
+ "",
4476
+ ...recipes.map((r) => [
4477
+ `### ${r.label}`,
4478
+ "",
4479
+ `- \uACBD\uB85C: \`${r.target}/\``,
4480
+ `- \uC8FC\uC694 \uBB38\uC11C: \`${r.target}/guideline.md\``,
4481
+ `- \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`,
4482
+ ""
4483
+ ].join("\n")),
4484
+ "",
4485
+ "## \uC791\uC5C5 \uC21C\uC11C",
4486
+ "",
4487
+ "1. \uC0AC\uC6A9\uC790\uC758 \uC694\uCCAD\uC774 \uC704 \uD2B8\uB9AC\uAC70 \uC911 \uD558\uB098\uC5D0 \uD574\uB2F9\uD558\uB294\uC9C0 \uD310\uB2E8",
4488
+ "2. \uD574\uB2F9 \uB808\uC2DC\uD53C\uC758 `guideline.md`\uB97C Read \uB3C4\uAD6C\uB85C \uC77D\uAE30",
4489
+ "3. \uC0D8\uD50C \uCF54\uB4DC(`*.html`, `*.ts` \uB4F1)\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uD655\uC778",
4490
+ "4. `.env.local`\uC5D0 \uAD00\uB828 \uD658\uACBD\uBCC0\uC218\uAC00 \uCC44\uC6CC\uC838 \uC788\uB294\uC9C0 \uD655\uC778",
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",
4492
+ ""
4493
+ ].join("\n");
4494
+ await fs14.writeFile(skillPath, body);
4495
+ }
4496
+ async function upsertClaudeMdBlock(cwd, installedKeys) {
4497
+ const claudeMdPath = path8.join(cwd, "CLAUDE.md");
4498
+ const BLOCK_START = "<!-- pai:recipes:start -->";
4499
+ const BLOCK_END = "<!-- pai:recipes:end -->";
4500
+ const lines = [];
4501
+ lines.push(BLOCK_START);
4502
+ lines.push("## \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9");
4640
4503
  lines.push("");
4641
- lines.push("## \uD56D\uBAA9\uBCC4 \uC0C1\uC138 \uBD84\uC11D");
4504
+ lines.push("\uB2E4\uC74C \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC **\uBC18\uB4DC\uC2DC** \uD574\uB2F9 \uACBD\uB85C\uC758");
4505
+ lines.push("`guideline.md`\uB97C \uBA3C\uC800 \uC77D\uACE0 \uC9C0\uCE68\uC744 \uB530\uB974\uC138\uC694:");
4642
4506
  lines.push("");
4643
- for (const cat of result.categories) {
4644
- const tierLabel = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
4645
- lines.push(`### ${cat.name} (${tierLabel}, \uAC00\uC911\uCE58 ${cat.tier === "must" ? "20%" : "13.3%"}) \u2014 ${cat.grade}\uB4F1\uAE09`);
4646
- lines.push("");
4647
- if (cat.rawFindings.length > 0) {
4648
- lines.push("**\uC810\uAC80 \uACB0\uACFC:**");
4649
- lines.push("");
4650
- for (const f of cat.rawFindings) {
4651
- const icon = f.found ? "\u2705" : "\u274C";
4652
- lines.push(`- ${icon} ${f.item} \u2014 ${f.details}`);
4653
- }
4654
- lines.push("");
4655
- }
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("");
4507
+ for (const key of installedKeys) {
4508
+ const r = RECIPES[key];
4509
+ lines.push(`- **${r.label}** \u2014 \`${r.target}/\``);
4510
+ lines.push(` \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`);
4678
4511
  }
4679
- lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
4680
4512
  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("");
4513
+ lines.push("\uD658\uACBD\uBCC0\uC218 \uAC12\uC740 `.env.local`\uC5D0 \uCC44\uC6CC\uC838 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
4514
+ lines.push(BLOCK_END);
4515
+ const block = lines.join("\n");
4516
+ let content = "";
4517
+ if (await fs14.pathExists(claudeMdPath)) {
4518
+ content = await fs14.readFile(claudeMdPath, "utf8");
4713
4519
  }
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.");
4520
+ const startIdx = content.indexOf(BLOCK_START);
4521
+ const endIdx = content.indexOf(BLOCK_END);
4522
+ if (startIdx >= 0 && endIdx > startIdx) {
4523
+ const before = content.slice(0, startIdx);
4524
+ const after = content.slice(endIdx + BLOCK_END.length);
4525
+ content = before + block + after;
4725
4526
  } 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;
4527
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
4528
+ if (content.length > 0) content += "\n";
4529
+ content += block + "\n";
4802
4530
  }
4803
- await fs14.writeFile(rcFile, `${sourceLine}
4804
- `);
4805
- return false;
4531
+ await fs14.ensureFile(claudeMdPath);
4532
+ await fs14.writeFile(claudeMdPath, content);
4806
4533
  }
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"() {
4534
+ var init_fetch_cmd = __esm({
4535
+ "src/cli/commands/fetch.cmd.ts"() {
4810
4536
  "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
- `;
4537
+ init_ui();
4538
+ init_logger();
4539
+ init_recipes();
4540
+ init_github_fetch();
4541
+ init_progress();
4852
4542
  }
4853
4543
  });
4854
4544
 
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"() {
4545
+ // src/stages/environment/index.ts
4546
+ var environmentStage;
4547
+ var init_environment = __esm({
4548
+ "src/stages/environment/index.ts"() {
4952
4549
  "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";
4550
+ init_analyzer();
4551
+ init_interviewer();
4552
+ init_generator();
4553
+ init_installer();
4554
+ init_registry();
4555
+ init_claude_commands();
4556
+ init_detector();
4557
+ init_config();
4558
+ init_progress();
4559
+ init_ui();
4560
+ init_analyzer();
4561
+ init_interviewer();
4562
+ init_doctor();
4563
+ environmentStage = {
4564
+ name: "environment",
4565
+ canSkip(input) {
4566
+ return input.config.plugins.length > 0;
4567
+ },
4568
+ async run(input) {
4569
+ const start = Date.now();
4570
+ const artifacts = [];
4571
+ const errors = [];
4572
+ try {
4573
+ console.log("");
4574
+ const analysis = await withSpinner("\uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D \uC911...", async () => {
4575
+ const result = await analyzeProject(input.cwd);
4576
+ await sleep(500);
4577
+ return result;
4578
+ });
4579
+ console.log("");
4580
+ if (analysis.stack.languages.length > 0) {
4581
+ success(`\uC2A4\uD0DD: ${analysis.stack.languages.join(", ")}`);
4582
+ }
4583
+ if (analysis.stack.frameworks.length > 0) {
4584
+ success(`\uD504\uB808\uC784\uC6CC\uD06C: ${analysis.stack.frameworks.join(", ")}`);
4585
+ }
4586
+ success(`Git: ${analysis.git.isGitRepo ? `${analysis.git.repoName ?? "local"}` : "\uBBF8\uCD08\uAE30\uD654"}`);
4587
+ const interview = await runInterview(analysis, input.cwd, input.config.projectName);
4588
+ console.log("");
4589
+ const configFiles = [
4590
+ "CLAUDE.md",
4591
+ ".claude/settings.json",
4592
+ "docs/vibe-coding/01-intent.md",
4593
+ "docs/vibe-coding/02-requirements.md",
4594
+ "docs/vibe-coding/03-research.md",
4595
+ "docs/vibe-coding/04-plan.md",
4596
+ "docs/vibe-coding/05-implement.md"
4597
+ ];
4598
+ const generated = await withFileSpinner("\uC124\uC815 \uD30C\uC77C \uC0DD\uC131 \uC911...", configFiles, async () => {
4599
+ const files = await generateFiles(input.cwd, analysis, interview);
4600
+ await sleep(400);
4601
+ return files;
4602
+ });
4603
+ artifacts.push(...generated);
4604
+ const basePlugins = ["github", "openspec", "roboco"];
4605
+ const pluginKeys = [.../* @__PURE__ */ new Set([...basePlugins, ...interview.extraTools])];
4606
+ const provCtx = {
4607
+ cwd: input.cwd,
4608
+ projectName: interview.projectName,
4609
+ mode: interview.mode,
4610
+ envEntries: {
4611
+ PAI_PROJECT_NAME: interview.projectName,
4612
+ PAI_MODE: interview.mode
4613
+ },
4614
+ analysis,
4615
+ mcp: interview.mcp
4616
+ };
4617
+ if (interview.authMethods.includes("custom")) {
4618
+ provCtx.envEntries["OAUTH_CLIENT_ID"] = interview.customAuth?.clientId || "YOUR_CLIENT_ID_HERE";
4619
+ provCtx.envEntries["OAUTH_CLIENT_SECRET"] = interview.customAuth?.clientSecret || "YOUR_CLIENT_SECRET_HERE";
4620
+ provCtx.envEntries["OAUTH_REDIRECT_URI"] = "http://localhost:3000/auth/callback";
4621
+ }
4622
+ const pluginFileNames = [
4623
+ ".gitignore",
4624
+ "src/",
4625
+ "docs/",
4626
+ "tests/",
4627
+ "public/",
4628
+ "docs/openspec.md",
4629
+ ".pai/roboco.json",
4630
+ ...interview.extraTools.includes("omc") ? [".pai/omc.md", ".omc/"] : [],
4631
+ ...interview.extraTools.includes("vercel") ? ["vercel.json"] : [],
4632
+ ...interview.extraTools.includes("supabase") ? ["supabase/config.toml"] : [],
4633
+ ...interview.extraTools.includes("gstack") ? [".pai/gstack.json"] : [],
4634
+ ...interview.extraTools.includes("harness") ? [".pai/harness.json"] : [],
4635
+ ...interview.extraTools.includes("mcp") ? ["mcp-server/", ".mcp.json"] : [],
4636
+ ".env.local"
4637
+ ];
4638
+ console.log("");
4639
+ await withFileSpinner("\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870 \uC124\uCE58 \uC911...", pluginFileNames, async () => {
4640
+ await runProvisioners(pluginKeys, provCtx);
4641
+ await sleep(500);
4642
+ });
4643
+ const cmdFiles = [
4644
+ "SKILL.md",
4645
+ "info.md",
4646
+ "init.md",
4647
+ "status.md",
4648
+ "doctor.md",
4649
+ "design.md",
4650
+ "validate.md",
4651
+ "evaluate.md",
4652
+ "install.md"
4653
+ ];
4654
+ console.log("");
4655
+ await withFileSpinner("\uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC124\uCE58 \uC911...", cmdFiles, async () => {
4656
+ await provisionClaudeCommands(input.cwd);
4657
+ await sleep(400);
4658
+ });
4659
+ console.log("");
4660
+ const installResults = await installTools(interview.tools, input.cwd);
4661
+ printInstallReport(installResults, interview.tools);
4662
+ if (interview.recipes && interview.recipes.length > 0) {
4663
+ console.log("");
4664
+ const { fetchCommand: fetchCommand2 } = await Promise.resolve().then(() => (init_fetch_cmd(), fetch_cmd_exports));
4665
+ for (const recipeKey of interview.recipes) {
4666
+ try {
4667
+ await fetchCommand2(input.cwd, recipeKey, {});
4668
+ } catch (err) {
4669
+ const msg = err instanceof Error ? err.message : String(err);
4670
+ warn(`\uB808\uC2DC\uD53C '${recipeKey}' \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 \u2014 ${msg}`);
4671
+ hint(`\uB098\uC911\uC5D0 \uC218\uB3D9: pai fetch ${recipeKey}`);
4672
+ }
4673
+ }
4674
+ }
4675
+ console.log("");
4676
+ await withSpinner("\uC124\uC815 \uC800\uC7A5 \uC911...", async () => {
4677
+ const config = createDefaultConfig(interview.projectName, interview.mode);
4678
+ config.plugins = pluginKeys;
4679
+ config.edges = deriveEdges(pluginKeys);
4680
+ if (interview.mcp) config.mcp = interview.mcp;
4681
+ await saveConfig(input.cwd, config);
4682
+ await sleep(300);
4683
+ });
4684
+ return {
4685
+ stage: "environment",
4686
+ status: "success",
4687
+ data: { analysis, interview, installResults },
4688
+ artifacts,
4689
+ duration: Date.now() - start,
4690
+ errors
4691
+ };
4692
+ } catch (err) {
4693
+ const msg = err instanceof Error ? err.message : String(err);
4694
+ errors.push({ code: "ENV_SETUP_FAILED", message: msg, recoverable: true });
4695
+ return {
4696
+ stage: "environment",
4697
+ status: "failed",
4698
+ data: {},
4699
+ artifacts,
4700
+ duration: Date.now() - start,
4701
+ errors
4702
+ };
4703
+ }
4962
4704
  }
4963
- backupPath;
4964
4705
  };
4965
4706
  }
4966
4707
  });
4967
4708
 
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);
4709
+ // src/stages/validation/runner.ts
4710
+ import { join as join7 } from "path";
4711
+ import fs15 from "fs-extra";
4712
+ async function runTests(cwd) {
4713
+ const start = Date.now();
4714
+ const gstackPath = join7(cwd, ".pai", "gstack.json");
4715
+ let runner = "npm test";
4716
+ if (await fs15.pathExists(gstackPath)) {
4976
4717
  try {
4977
- const content = readFileSync(fullPath);
4978
- hash.update(`${file}:${content.length}`);
4718
+ const config = await fs15.readJson(gstackPath);
4719
+ if (config.testRunner === "vitest") runner = "npx vitest run";
4720
+ else if (config.testRunner === "jest") runner = "npx jest";
4721
+ else if (config.testRunner === "mocha") runner = "npx mocha";
4979
4722
  } catch {
4980
- hash.update(`${file}:missing`);
4981
4723
  }
4982
4724
  }
4983
- return hash.digest("hex").slice(0, 16);
4984
- }
4985
- function getCachePath(repoPath) {
4986
- return join9(repoPath, CACHE_DIR, CACHE_FILE);
4725
+ const pkgPath = join7(cwd, "package.json");
4726
+ if (await fs15.pathExists(pkgPath)) {
4727
+ try {
4728
+ const pkg5 = await fs15.readJson(pkgPath);
4729
+ if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
4730
+ return {
4731
+ runner,
4732
+ passed: false,
4733
+ output: "\uD14C\uC2A4\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC815\uC758\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. package.json\uC758 scripts.test\uB97C \uC124\uC815\uD558\uC138\uC694.",
4734
+ duration: Date.now() - start
4735
+ };
4736
+ }
4737
+ } catch {
4738
+ }
4739
+ }
4740
+ try {
4741
+ const { execa } = await import("execa");
4742
+ const { stdout, stderr } = await execa("npm", ["test"], {
4743
+ cwd,
4744
+ timeout: 12e4,
4745
+ env: { ...process.env, CI: "true" }
4746
+ });
4747
+ return {
4748
+ runner,
4749
+ passed: true,
4750
+ output: stdout || stderr,
4751
+ duration: Date.now() - start
4752
+ };
4753
+ } catch (err) {
4754
+ const output = err instanceof Error ? err.message : String(err);
4755
+ return {
4756
+ runner,
4757
+ passed: false,
4758
+ output,
4759
+ duration: Date.now() - start
4760
+ };
4761
+ }
4987
4762
  }
4988
- function loadCache(repoPath) {
4763
+ var init_runner = __esm({
4764
+ "src/stages/validation/runner.ts"() {
4765
+ "use strict";
4766
+ }
4767
+ });
4768
+
4769
+ // src/stages/validation/harness.ts
4770
+ import { join as join8 } from "path";
4771
+ import fs16 from "fs-extra";
4772
+ async function runHarnessCheck(cwd) {
4773
+ const harnessPath = join8(cwd, ".pai", "harness.json");
4774
+ if (!await fs16.pathExists(harnessPath)) {
4775
+ return { enabled: false, specFile: null, rules: [], checks: [] };
4776
+ }
4777
+ let config;
4989
4778
  try {
4990
- const data = readFileSync(getCachePath(repoPath), "utf-8");
4991
- return JSON.parse(data);
4779
+ config = await fs16.readJson(harnessPath);
4992
4780
  } catch {
4993
- return { version: 1, entries: {} };
4781
+ return { enabled: false, specFile: null, rules: [], checks: [] };
4994
4782
  }
4995
- }
4996
- function saveCache(repoPath, store) {
4997
- const cacheDir = join9(repoPath, CACHE_DIR, "cache");
4998
- if (!existsSync(cacheDir)) {
4999
- mkdirSync(cacheDir, { recursive: true });
4783
+ const specFile = config.specFile ?? "docs/openspec.md";
4784
+ const rules = config.rules ?? [];
4785
+ const checks = [];
4786
+ if (rules.includes("spec-implementation-match")) {
4787
+ const specExists = await fs16.pathExists(join8(cwd, specFile));
4788
+ const srcExists = await fs16.pathExists(join8(cwd, "src"));
4789
+ checks.push({
4790
+ rule: "spec-implementation-match",
4791
+ passed: specExists && srcExists,
4792
+ detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
4793
+ });
5000
4794
  }
5001
- writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
4795
+ if (rules.includes("api-contract-test")) {
4796
+ const testDir = await fs16.pathExists(join8(cwd, "tests"));
4797
+ const testDir2 = await fs16.pathExists(join8(cwd, "test"));
4798
+ checks.push({
4799
+ rule: "api-contract-test",
4800
+ passed: testDir || testDir2,
4801
+ detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
4802
+ });
4803
+ }
4804
+ return { enabled: true, specFile, rules, checks };
5002
4805
  }
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;
4806
+ var init_harness = __esm({
4807
+ "src/stages/validation/harness.ts"() {
4808
+ "use strict";
4809
+ }
4810
+ });
4811
+
4812
+ // src/cli/commands/validate.cmd.ts
4813
+ var validate_cmd_exports = {};
4814
+ __export(validate_cmd_exports, {
4815
+ handleRobocoCliError: () => handleRobocoError,
4816
+ validateCommand: () => validateCommand
4817
+ });
4818
+ async function validateCommand(cwd, options = {}) {
4819
+ try {
4820
+ await withRoboco(
4821
+ cwd,
4822
+ "validation",
4823
+ "pai test",
4824
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
4825
+ async () => {
4826
+ section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
4827
+ const testResult = await runTests(cwd);
4828
+ if (testResult.passed) {
4829
+ success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
4830
+ } else {
4831
+ error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
4832
+ info(testResult.output.slice(0, 300));
4833
+ }
4834
+ section("\uD558\uB124\uC2A4 \uAC80\uC99D");
4835
+ const harness = await runHarnessCheck(cwd);
4836
+ if (!harness.enabled) {
4837
+ info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
4838
+ info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
4839
+ } else {
4840
+ for (const check of harness.checks) {
4841
+ if (check.passed) {
4842
+ success(`${check.rule}: ${check.detail}`);
4843
+ } else {
4844
+ warn(`${check.rule}: ${check.detail}`);
4845
+ }
4846
+ }
4847
+ }
4848
+ const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
4849
+ console.log("");
4850
+ if (allPassed) {
4851
+ success("\uAC80\uC99D \uD1B5\uACFC!");
4852
+ } else if (!testResult.passed) {
4853
+ error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
4854
+ throw new Error("tests-failed");
4855
+ } else {
4856
+ warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
4857
+ }
4858
+ }
4859
+ );
4860
+ await syncHandoff(cwd).catch(() => {
4861
+ });
4862
+ } catch (err) {
4863
+ handleRobocoError(err);
4864
+ }
5010
4865
  }
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];
4866
+ function handleRobocoError(err) {
4867
+ if (err instanceof RobocoLockError) {
4868
+ error(err.message);
4869
+ process.exitCode = 1;
4870
+ return;
4871
+ }
4872
+ if (err instanceof GateFailedError) {
4873
+ error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
4874
+ for (const v of err.result.violations) {
4875
+ warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
4876
+ }
4877
+ hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
4878
+ process.exitCode = 1;
4879
+ return;
4880
+ }
4881
+ if (err instanceof Error) {
4882
+ if (err.message === "tests-failed") {
4883
+ process.exitCode = 1;
4884
+ return;
5018
4885
  }
4886
+ error(err.message);
4887
+ process.exitCode = 1;
4888
+ return;
5019
4889
  }
5020
- store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
5021
- saveCache(repoPath, store);
4890
+ throw err;
4891
+ }
4892
+ var init_validate_cmd = __esm({
4893
+ "src/cli/commands/validate.cmd.ts"() {
4894
+ "use strict";
4895
+ init_ui();
4896
+ init_runner();
4897
+ init_harness();
4898
+ init_roboco();
4899
+ init_gates();
4900
+ }
4901
+ });
4902
+
4903
+ // src/stages/evaluation/prompts/analyze.ts
4904
+ var analyze_exports = {};
4905
+ __export(analyze_exports, {
4906
+ buildAnalysisPrompt: () => buildAnalysisPrompt
4907
+ });
4908
+ function buildAnalysisPrompt(repoPath) {
4909
+ return `Analyze the repository at "${repoPath}" for vibe coding readiness.
4910
+
4911
+ Use only Read, Glob, and Grep tools. Complete in ~20 tool calls.
4912
+ Focus on config files, not source code.
4913
+
4914
+ Evaluate these 6 categories:
4915
+
4916
+ ### Must-Have (60% weight)
4917
+ 1. **\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0**: test framework config, test dirs, coverage thresholds
4918
+ - 0=none, 20=config only, 40=minimal files, 60=moderate, 80=good, 100=comprehensive
4919
+ 2. **CI/CD**: pipeline configs, quality gates, multi-stage
4920
+ - 0=none, 20=exists but minimal, 40=basic, 60=tests+lint, 80=tests+lint+build, 100=comprehensive
4921
+ 3. **\uD6C5 \uAE30\uBC18 \uAC80\uC99D**: git hooks, lint-staged, AI agent hooks (.claude/settings.json)
4922
+ - 0=none, 20=framework only, 40=single hook, 60=lint+format, 80=lint+test+format, 100=comprehensive+AI
4923
+
4924
+ ### Nice-to-Have (40% weight)
4925
+ 4. **\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870**: dir organization, deps management, env config
4926
+ - 0=flat/chaotic, 20=minimal, 40=basic, 60=reasonable, 80=well-organized, 100=exemplary
4927
+ 5. **\uBB38\uC11C\uD654 \uC218\uC900**: README, CONTRIBUTING, API docs, architecture docs
4928
+ - 0=none, 20=minimal, 40=basic, 60=good README, 80=README+API, 100=comprehensive
4929
+ 6. **\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1**: CLAUDE.md, AGENTS.md, .claude/ config, skills, commands
4930
+ - 0=none, 20=basic, 40=one AI config, 60=context+config, 80=context+safety+skills, 100=comprehensive
4931
+
4932
+ Output ONLY a JSON block:
4933
+ \`\`\`json
4934
+ {
4935
+ "categories": [
4936
+ {
4937
+ "name": "\uCE74\uD14C\uACE0\uB9AC\uBA85 (Korean)",
4938
+ "tier": "must" | "nice",
4939
+ "score": 0-100,
4940
+ "recommendations": [{ "severity": "critical"|"warning"|"info", "message": "...", "action": "..." }],
4941
+ "rawFindings": [{ "item": "filename or concept", "found": true|false, "details": "..." }]
4942
+ }
4943
+ ],
4944
+ "summary": "2-3 sentence Korean summary"
5022
4945
  }
5023
- var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
5024
- var init_cache = __esm({
5025
- "src/stages/evaluation/cache.ts"() {
4946
+ \`\`\``;
4947
+ }
4948
+ var init_analyze = __esm({
4949
+ "src/stages/evaluation/prompts/analyze.ts"() {
5026
4950
  "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
- ];
5048
4951
  }
5049
4952
  });
5050
4953
 
5051
- // src/core/roboco.ts
5052
- import os3 from "os";
5053
- import path8 from "path";
5054
- import fs16 from "fs-extra";
5055
- function robocoPath(cwd) {
5056
- return path8.join(cwd, ROBOCO_PATH);
5057
- }
5058
- async function loadRobocoState(cwd) {
5059
- const p = robocoPath(cwd);
5060
- if (!await fs16.pathExists(p)) return freshState();
4954
+ // src/stages/evaluation/analyzer.ts
4955
+ var analyzer_exports2 = {};
4956
+ __export(analyzer_exports2, {
4957
+ analyzeRepository: () => analyzeRepository
4958
+ });
4959
+ import { join as join9 } from "path";
4960
+ import fs17 from "fs-extra";
4961
+ async function analyzeRepository(repoPath) {
5061
4962
  try {
5062
- const raw = await fs16.readJson(p);
5063
- if (raw?.version !== "2.0") return freshState();
5064
- return raw;
4963
+ return await aiAnalysis(repoPath);
5065
4964
  } catch {
5066
- return freshState();
4965
+ return staticAnalysis(repoPath);
5067
4966
  }
5068
4967
  }
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 });
4968
+ async function aiAnalysis(repoPath) {
4969
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
4970
+ const { buildAnalysisPrompt: buildAnalysisPrompt2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
4971
+ const prompt = buildAnalysisPrompt2(repoPath);
4972
+ let resultText = "";
4973
+ for await (const message of query({
4974
+ prompt,
4975
+ options: {
4976
+ maxTurns: 200,
4977
+ systemPrompt: "You are a vibe coding readiness analyzer. Analyze the repository and output JSON.",
4978
+ allowedTools: ["Read", "Glob", "Grep"],
4979
+ permissionMode: "plan"
4980
+ }
4981
+ })) {
4982
+ if ("result" in message) {
4983
+ resultText = message.result;
4984
+ }
4985
+ }
4986
+ return parseJsonFromText(resultText);
5073
4987
  }
5074
- function freshState() {
4988
+ function parseJsonFromText(text) {
4989
+ const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
4990
+ if (jsonMatch?.[1]) {
4991
+ return JSON.parse(jsonMatch[1]);
4992
+ }
4993
+ return JSON.parse(text);
4994
+ }
4995
+ async function staticAnalysis(repoPath) {
4996
+ const categories = [];
4997
+ categories.push(await checkTestCoverage(repoPath));
4998
+ categories.push(await checkCiCd(repoPath));
4999
+ categories.push(await checkHooks(repoPath));
5000
+ categories.push(await checkRepoStructure(repoPath));
5001
+ categories.push(await checkDocumentation(repoPath));
5002
+ categories.push(await checkHarnessEngineering(repoPath));
5075
5003
  return {
5076
- version: "2.0",
5077
- currentStage: "environment",
5078
- stageHistory: [],
5079
- lock: null,
5080
- gates: {}
5004
+ categories,
5005
+ summary: `\uC815\uC801 \uBD84\uC11D \uC644\uB8CC. ${categories.filter((c2) => c2.score >= 60).length}/6 \uCE74\uD14C\uACE0\uB9AC \uD1B5\uACFC.`
5081
5006
  };
5082
5007
  }
5083
- function isProcessAlive(pid) {
5084
- try {
5085
- process.kill(pid, 0);
5086
- return true;
5087
- } catch {
5088
- return false;
5008
+ async function checkTestCoverage(repoPath) {
5009
+ const findings = [];
5010
+ let score = 0;
5011
+ const testConfigs = [
5012
+ "jest.config.ts",
5013
+ "jest.config.js",
5014
+ "vitest.config.ts",
5015
+ "vitest.config.js",
5016
+ "pytest.ini",
5017
+ "phpunit.xml",
5018
+ ".nycrc"
5019
+ ];
5020
+ for (const f of testConfigs) {
5021
+ const found = await fs17.pathExists(join9(repoPath, f));
5022
+ findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5023
+ if (found) score += 20;
5089
5024
  }
5090
- }
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;
5094
- return false;
5095
- }
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);
5025
+ const testDirs = ["tests", "test", "__tests__", "spec"];
5026
+ let hasTestDir = false;
5027
+ for (const d of testDirs) {
5028
+ if (await fs17.pathExists(join9(repoPath, d))) {
5029
+ findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
5030
+ hasTestDir = true;
5031
+ score += 30;
5032
+ break;
5101
5033
  }
5102
5034
  }
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;
5115
- }
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);
5035
+ if (!hasTestDir) {
5036
+ findings.push({ item: "test directory", found: false, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC5C6\uC74C" });
5121
5037
  }
5038
+ score = Math.min(100, score);
5039
+ const recommendations = score < 60 ? [{ severity: "critical", message: "\uD14C\uC2A4\uD2B8 \uD658\uACBD \uBBF8\uAD6C\uC131", action: "\uD14C\uC2A4\uD2B8 \uD504\uB808\uC784\uC6CC\uD06C\uC640 \uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC\uB97C \uCD94\uAC00\uD558\uC138\uC694" }] : [];
5040
+ return { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", score, recommendations, rawFindings: findings };
5122
5041
  }
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");
5133
- }
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);
5144
- }
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
- }
5042
+ async function checkCiCd(repoPath) {
5043
+ const findings = [];
5044
+ let score = 0;
5045
+ const ciConfigs = [
5046
+ { path: ".github/workflows", label: "GitHub Actions" },
5047
+ { path: ".gitlab-ci.yml", label: "GitLab CI" },
5048
+ { path: "Jenkinsfile", label: "Jenkins" },
5049
+ { path: ".circleci", label: "CircleCI" }
5050
+ ];
5051
+ for (const { path: path10, label } of ciConfigs) {
5052
+ const found = await fs17.pathExists(join9(repoPath, path10));
5053
+ findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5054
+ if (found) score += 40;
5155
5055
  }
5156
- await saveRobocoState(cwd, state);
5056
+ score = Math.min(100, score);
5057
+ const recommendations = score === 0 ? [{ severity: "critical", message: "CI/CD \uBBF8\uAD6C\uC131", action: "GitHub Actions \uB610\uB294 \uB2E4\uB978 CI \uD30C\uC774\uD504\uB77C\uC778\uC744 \uC124\uC815\uD558\uC138\uC694" }] : [];
5058
+ return { name: "CI/CD", tier: "must", score, recommendations, rawFindings: findings };
5157
5059
  }
5158
- async function saveGateResult(cwd, key, result) {
5159
- const state = await loadRobocoState(cwd);
5160
- state.gates[key] = result;
5161
- await saveRobocoState(cwd, state);
5060
+ async function checkHooks(repoPath) {
5061
+ const findings = [];
5062
+ let score = 0;
5063
+ const hookConfigs = [
5064
+ { path: ".husky", label: "Husky" },
5065
+ { path: ".lintstagedrc", label: "lint-staged" },
5066
+ { path: ".lintstagedrc.json", label: "lint-staged (json)" },
5067
+ { path: "commitlint.config.js", label: "commitlint" },
5068
+ { path: ".claude/settings.json", label: "Claude Code settings" }
5069
+ ];
5070
+ for (const { path: path10, label } of hookConfigs) {
5071
+ const found = await fs17.pathExists(join9(repoPath, path10));
5072
+ findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5073
+ if (found) score += 20;
5074
+ }
5075
+ score = Math.min(100, score);
5076
+ const recommendations = score < 40 ? [{ severity: "warning", message: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D \uBD80\uC871", action: "Husky + lint-staged\uB97C \uC124\uC815\uD558\uC5EC \uCEE4\uBC0B \uC804 \uAC80\uC99D\uC744 \uCD94\uAC00\uD558\uC138\uC694" }] : [];
5077
+ return { name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", score, recommendations, rawFindings: findings };
5162
5078
  }
5163
- async function withRoboco(cwd, stage, command, options, fn) {
5164
- await acquireLock(cwd, stage, command, options.ttlMs);
5165
- 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
- }
5175
- }
5176
- await markStageStart(cwd, stage, command);
5177
- const value = await fn();
5178
- await markStageEnd(cwd, stage, true);
5179
- return value;
5180
- } 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
- });
5079
+ async function checkRepoStructure(repoPath) {
5080
+ const findings = [];
5081
+ let score = 0;
5082
+ const structureChecks = [
5083
+ { path: "src", label: "src/ \uB514\uB809\uD1A0\uB9AC" },
5084
+ { path: "package.json", label: "\uC758\uC874\uC131 \uAD00\uB9AC" },
5085
+ { path: ".env.example", label: "\uD658\uACBD\uBCC0\uC218 \uC608\uC2DC" },
5086
+ { path: ".gitignore", label: ".gitignore" }
5087
+ ];
5088
+ for (const { path: path10, label } of structureChecks) {
5089
+ const found = await fs17.pathExists(join9(repoPath, path10));
5090
+ findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5091
+ if (found) score += 25;
5190
5092
  }
5093
+ score = Math.min(100, score);
5094
+ return { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", score, recommendations: [], rawFindings: findings };
5191
5095
  }
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";
5096
+ async function checkDocumentation(repoPath) {
5097
+ const findings = [];
5098
+ let score = 0;
5099
+ const docChecks = [
5100
+ { path: "README.md", label: "README.md", points: 30 },
5101
+ { path: "CONTRIBUTING.md", label: "CONTRIBUTING.md", points: 20 },
5102
+ { path: "docs", label: "docs/ \uB514\uB809\uD1A0\uB9AC", points: 25 },
5103
+ { path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
5104
+ ];
5105
+ for (const { path: path10, label, points } of docChecks) {
5106
+ const found = await fs17.pathExists(join9(repoPath, path10));
5107
+ findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5108
+ if (found) score += points;
5206
5109
  }
5207
- await fs16.writeFile(handoffPath, next);
5110
+ score = Math.min(100, score);
5111
+ return { name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", score, recommendations: [], rawFindings: findings };
5208
5112
  }
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("");
5113
+ async function checkHarnessEngineering(repoPath) {
5114
+ const findings = [];
5115
+ let score = 0;
5116
+ const harnessChecks = [
5117
+ { path: "CLAUDE.md", label: "CLAUDE.md", points: 25 },
5118
+ { path: "AGENTS.md", label: "AGENTS.md", points: 15 },
5119
+ { path: ".claude/settings.json", label: ".claude/settings.json", points: 20 },
5120
+ { path: ".claude/skills", label: ".claude/skills/", points: 20 },
5121
+ { path: ".claude/commands", label: ".claude/commands/", points: 10 },
5122
+ { path: ".pai/config.json", label: "PAI config", points: 10 }
5123
+ ];
5124
+ for (const { path: path10, label, points } of harnessChecks) {
5125
+ const found = await fs17.pathExists(join9(repoPath, path10));
5126
+ findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
5127
+ if (found) score += points;
5220
5128
  }
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("");
5129
+ score = Math.min(100, score);
5130
+ return { name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", score, recommendations: [], rawFindings: findings };
5131
+ }
5132
+ var init_analyzer2 = __esm({
5133
+ "src/stages/evaluation/analyzer.ts"() {
5134
+ "use strict";
5227
5135
  }
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)})`);
5232
- }
5233
- lines.push("");
5136
+ });
5137
+
5138
+ // src/core/types/stage.ts
5139
+ var STAGE_ORDER;
5140
+ var init_stage = __esm({
5141
+ "src/core/types/stage.ts"() {
5142
+ "use strict";
5143
+ STAGE_ORDER = [
5144
+ "environment",
5145
+ "design",
5146
+ "execution",
5147
+ "validation",
5148
+ "evaluation"
5149
+ ];
5234
5150
  }
5235
- lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
5236
- lines.push(HANDOFF_END);
5237
- return lines.join("\n");
5151
+ });
5152
+
5153
+ // src/core/types/scoring.ts
5154
+ function gradeFromScore(score) {
5155
+ if (score >= 90) return "A";
5156
+ if (score >= 80) return "B";
5157
+ if (score >= 70) return "C";
5158
+ if (score >= 50) return "D";
5159
+ return "F";
5238
5160
  }
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;
5161
+ var DEFAULT_CATEGORY_WEIGHTS;
5162
+ var init_scoring = __esm({
5163
+ "src/core/types/scoring.ts"() {
5164
+ "use strict";
5165
+ DEFAULT_CATEGORY_WEIGHTS = [
5166
+ { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", weight: 0.2 },
5167
+ { name: "CI/CD", tier: "must", weight: 0.2 },
5168
+ { name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", weight: 0.2 },
5169
+ { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", weight: 0.133 },
5170
+ { name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", weight: 0.133 },
5171
+ { name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", weight: 0.134 }
5172
+ ];
5245
5173
  }
5246
- }
5247
- var ROBOCO_PATH, DEFAULT_TTL_MS, RobocoLockError, GateFailedError, HANDOFF_START, HANDOFF_END;
5248
- var init_roboco = __esm({
5249
- "src/core/roboco.ts"() {
5174
+ });
5175
+
5176
+ // src/core/types/index.ts
5177
+ var init_types = __esm({
5178
+ "src/core/types/index.ts"() {
5250
5179
  "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) {
5255
- 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";
5266
- }
5267
- result;
5268
- };
5269
- HANDOFF_START = "<!-- roboco:start -->";
5270
- HANDOFF_END = "<!-- roboco:end -->";
5180
+ init_stage();
5181
+ init_scoring();
5271
5182
  }
5272
5183
  });
5273
5184
 
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) {
5185
+ // src/stages/evaluation/scorer.ts
5186
+ var scorer_exports = {};
5187
+ __export(scorer_exports, {
5188
+ computeResult: () => computeResult
5189
+ });
5190
+ function computeResult(llmOutput, customWeights) {
5191
+ const weights = customWeights ?? DEFAULT_CATEGORY_WEIGHTS;
5192
+ const categories = llmOutput.categories.map((cat) => ({
5193
+ name: cat.name,
5194
+ tier: cat.tier,
5195
+ score: Math.round(Math.max(0, Math.min(100, cat.score))),
5196
+ grade: gradeFromScore(Math.max(0, Math.min(100, cat.score))),
5197
+ recommendations: cat.recommendations,
5198
+ rawFindings: cat.rawFindings
5199
+ }));
5200
+ const totalScore = computeWeightedAverage(categories, weights);
5201
+ let totalGrade = gradeFromScore(totalScore);
5202
+ const { penaltyApplied, penaltyReason } = checkPenalty(categories);
5203
+ if (penaltyApplied && gradeRank(totalGrade) > gradeRank("C")) {
5204
+ totalGrade = "C";
5205
+ }
5281
5206
  return {
5282
- name,
5283
- passed: violations.length === 0,
5284
- checkedAt: now(),
5285
- violations
5207
+ categories,
5208
+ totalScore,
5209
+ totalGrade,
5210
+ summary: llmOutput.summary,
5211
+ penaltyApplied,
5212
+ penaltyReason
5286
5213
  };
5287
5214
  }
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
- });
5215
+ function computeWeightedAverage(categories, weights) {
5216
+ let weightedSum = 0;
5217
+ let totalWeight = 0;
5218
+ for (const cat of categories) {
5219
+ const config = weights.find((w) => w.name === cat.name);
5220
+ const weight = config?.weight ?? (cat.tier === "must" ? 0.2 : 0.133);
5221
+ weightedSum += cat.score * weight;
5222
+ totalWeight += weight;
5297
5223
  }
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
- });
5224
+ if (totalWeight === 0) return 0;
5225
+ return Math.round(weightedSum / totalWeight * 100) / 100;
5226
+ }
5227
+ function checkPenalty(categories) {
5228
+ const failedMust = categories.filter(
5229
+ (c2) => c2.tier === "must" && c2.grade === "F"
5230
+ );
5231
+ if (failedMust.length > 0) {
5232
+ const names = failedMust.map((c2) => c2.name).join(", ");
5233
+ return {
5234
+ penaltyApplied: true,
5235
+ penaltyReason: `\uD544\uC218 \uCE74\uD14C\uACE0\uB9AC F \uB4F1\uAE09: ${names} \u2192 \uC804\uCCB4 \uB4F1\uAE09 \uCD5C\uB300 C\uB85C \uC81C\uD55C`
5236
+ };
5237
+ }
5238
+ return { penaltyApplied: false };
5239
+ }
5240
+ function gradeRank(grade) {
5241
+ const ranks = { A: 4, B: 3, C: 2, D: 1, F: 0 };
5242
+ return ranks[grade];
5243
+ }
5244
+ var init_scorer = __esm({
5245
+ "src/stages/evaluation/scorer.ts"() {
5246
+ "use strict";
5247
+ init_types();
5248
+ }
5249
+ });
5250
+
5251
+ // src/stages/evaluation/reporter.ts
5252
+ var reporter_exports = {};
5253
+ __export(reporter_exports, {
5254
+ buildDetailedReport: () => buildDetailedReport,
5255
+ buildMarkdownReport: () => buildMarkdownReport,
5256
+ printReport: () => printReport,
5257
+ printVerboseFindings: () => printVerboseFindings
5258
+ });
5259
+ import chalk5 from "chalk";
5260
+ function printReport(result) {
5261
+ console.log("");
5262
+ console.log(chalk5.hex("#7B93DB")(" PAI Evaluation Report"));
5263
+ console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5264
+ console.log("");
5265
+ const gradeColor = GRADE_COLORS[result.totalGrade];
5266
+ console.log(` \uC885\uD569 \uC810\uC218: ${gradeColor(String(result.totalScore))} / 100 \uB4F1\uAE09: ${gradeColor(result.totalGrade)}`);
5267
+ if (result.penaltyApplied) {
5268
+ console.log(chalk5.red(` \u26A0 ${result.penaltyReason}`));
5269
+ }
5270
+ console.log("");
5271
+ console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5272
+ for (const cat of result.categories) {
5273
+ const color = GRADE_COLORS[cat.grade];
5274
+ const tierLabel = cat.tier === "must" ? chalk5.hex("#E06C75")("[\uD544\uC218]") : chalk5.gray("[\uC120\uD0DD]");
5275
+ const bar = renderBar(cat.score);
5276
+ console.log(` ${tierLabel} ${cat.name.padEnd(16)} ${bar} ${color(`${cat.score}`).padStart(12)} ${color(cat.grade)}`);
5305
5277
  }
5306
- return makeResult("design.entry", violations);
5278
+ console.log("");
5279
+ console.log(chalk5.gray(` ${result.summary}`));
5280
+ console.log("");
5307
5281
  }
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
- });
5282
+ function printVerboseFindings(result) {
5283
+ for (const cat of result.categories) {
5284
+ console.log("");
5285
+ console.log(chalk5.bold(` ${cat.name} (${cat.grade}, ${cat.score}\uC810)`));
5286
+ for (const f of cat.rawFindings) {
5287
+ const icon = f.found ? chalk5.green("\u2713") : chalk5.red("\u2717");
5288
+ console.log(` ${icon} ${f.item} \u2014 ${f.details}`);
5327
5289
  }
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
- });
5290
+ for (const r of cat.recommendations) {
5291
+ const severity = r.severity === "critical" ? chalk5.red("!") : r.severity === "warning" ? chalk5.yellow("!") : chalk5.gray("i");
5292
+ console.log(` ${severity} ${r.message}`);
5293
+ console.log(chalk5.gray(` \u2192 ${r.action}`));
5339
5294
  }
5340
5295
  }
5341
- return makeResult("execution.entry", violations);
5342
5296
  }
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
- });
5297
+ function renderBar(score) {
5298
+ const width = 20;
5299
+ const filled = Math.round(score / 100 * width);
5300
+ const empty = width - filled;
5301
+ const color = score >= 80 ? chalk5.hex("#6BCB77") : score >= 60 ? chalk5.hex("#E2B340") : chalk5.hex("#E06C75");
5302
+ return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
5303
+ }
5304
+ function buildMarkdownReport(result) {
5305
+ const lines = [
5306
+ "# PAI Evaluation Report",
5307
+ "",
5308
+ `> \uC0DD\uC131\uC77C\uC2DC: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}`,
5309
+ "",
5310
+ `## \uC885\uD569 \uC810\uC218: ${result.totalScore}/100 \u2014 \uB4F1\uAE09 ${result.totalGrade}`,
5311
+ ""
5312
+ ];
5313
+ if (result.penaltyApplied) {
5314
+ lines.push(`> \u26A0 ${result.penaltyReason}`);
5315
+ lines.push("");
5316
+ }
5317
+ lines.push("## \uCE74\uD14C\uACE0\uB9AC\uBCC4 \uC0C1\uC138");
5318
+ lines.push("");
5319
+ lines.push("| \uCE74\uD14C\uACE0\uB9AC | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 |");
5320
+ lines.push("|---------|------|------|------|");
5321
+ for (const cat of result.categories) {
5322
+ const tier = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
5323
+ lines.push(`| ${cat.name} | ${tier} | ${cat.score} | ${cat.grade} |`);
5324
+ }
5325
+ lines.push("");
5326
+ lines.push("## \uAD8C\uACE0\uC0AC\uD56D");
5327
+ lines.push("");
5328
+ const allRecs = result.categories.flatMap(
5329
+ (c2) => c2.recommendations.map((r) => ({ ...r, category: c2.name }))
5330
+ );
5331
+ if (allRecs.length === 0) {
5332
+ lines.push("\uBAA8\uB4E0 \uD56D\uBAA9\uC774 \uC591\uD638\uD569\uB2C8\uB2E4!");
5352
5333
  } 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
- });
5334
+ for (const r of allRecs) {
5335
+ const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
5336
+ lines.push(`- ${icon} **${r.category}**: ${r.message}`);
5337
+ lines.push(` - ${r.action}`);
5360
5338
  }
5361
5339
  }
5362
- return makeResult("validation.entry", violations);
5340
+ lines.push("");
5341
+ lines.push(`## \uC694\uC57D`);
5342
+ lines.push("");
5343
+ lines.push(result.summary);
5344
+ lines.push("");
5345
+ lines.push("---");
5346
+ lines.push("*Generated by PAI (Plugin-based AI)*");
5347
+ return lines.join("\n") + "\n";
5363
5348
  }
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
- });
5349
+ function buildDetailedReport(result, projectName) {
5350
+ const now2 = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
5351
+ const mustCats = result.categories.filter((c2) => c2.tier === "must");
5352
+ const niceCats = result.categories.filter((c2) => c2.tier === "nice");
5353
+ const lines = [
5354
+ `# ${projectName} \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uBD84\uC11D \uB9AC\uD3EC\uD2B8`,
5355
+ "",
5356
+ `> \uC2A4\uCE94 \uC77C\uC2DC: ${now2}`,
5357
+ `> \uBD84\uC11D \uB300\uC0C1: ${projectName}`,
5358
+ "",
5359
+ "---",
5360
+ "",
5361
+ "## \uC694\uC57D",
5362
+ "",
5363
+ `${projectName}\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uC885\uD569 \uC810\uC218\uB294 **${result.totalScore}/100 (${result.totalGrade}\uB4F1\uAE09)**\uC785\uB2C8\uB2E4.`,
5364
+ ""
5365
+ ];
5366
+ if (result.penaltyApplied) {
5367
+ lines.push(`> \u26A0 ${result.penaltyReason}`);
5368
+ lines.push("");
5369
+ }
5370
+ if (result.totalGrade === "A") {
5371
+ lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uD65C\uC6A9\uD560 \uC900\uBE44\uAC00 \uC798 \uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
5372
+ } else if (result.totalGrade === "B") {
5373
+ lines.push("\uB300\uBD80\uBD84 \uC900\uBE44\uB418\uC5B4 \uC788\uC73C\uBA70, \uC77C\uBD80 \uAC1C\uC120\uC73C\uB85C \uCD5C\uC801\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC744 \uAD6C\uCD95\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
5374
+ } else if (result.totalGrade === "C") {
5375
+ lines.push("\uAE30\uBCF8\uC801\uC778 \uAD6C\uC870\uB294 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098, \uBC14\uC774\uBE0C \uCF54\uB529\uC758 \uC7A0\uC7AC\uB825\uC744 \uCDA9\uBD84\uD788 \uD65C\uC6A9\uD558\uB824\uBA74 \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
5376
+ } else if (result.totalGrade === "D") {
5377
+ lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uBCF8\uACA9\uC801\uC73C\uB85C \uD65C\uC6A9\uD558\uAE30\uC5D0\uB294 \uC0C1\uB2F9\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.");
5378
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
- }
5379
+ lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uAC70\uC758 \uAD6C\uC131\uB418\uC9C0 \uC54A\uC740 \uC0C1\uD0DC\uB85C, \uAE30\uBCF8 \uC124\uC815\uBD80\uD130 \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.");
5388
5380
  }
5389
- return makeResult("evaluation.entry", violations);
5390
- }
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;
5381
+ lines.push("");
5382
+ lines.push("---");
5383
+ lines.push("");
5384
+ lines.push("## \uD56D\uBAA9\uBCC4 \uD604\uD669");
5385
+ lines.push("");
5386
+ lines.push("| \uD56D\uBAA9 | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 | \uAC00\uC911\uCE58 |");
5387
+ lines.push("|------|------|------|------|--------|");
5388
+ for (const cat of result.categories) {
5389
+ const tier = cat.tier === "must" ? "**\uD544\uC218**" : "\uC120\uD0DD";
5390
+ lines.push(`| ${cat.name} | ${tier} | ${cat.score}/100 | ${cat.grade} | ${cat.tier === "must" ? "20%" : "13.3%"} |`);
5391
+ }
5392
+ lines.push("");
5393
+ lines.push("---");
5394
+ lines.push("");
5395
+ lines.push("## \uD56D\uBAA9\uBCC4 \uC0C1\uC138 \uBD84\uC11D");
5396
+ lines.push("");
5397
+ for (const cat of result.categories) {
5398
+ const tierLabel = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
5399
+ lines.push(`### ${cat.name} (${tierLabel}, \uAC00\uC911\uCE58 ${cat.tier === "must" ? "20%" : "13.3%"}) \u2014 ${cat.grade}\uB4F1\uAE09`);
5400
+ lines.push("");
5401
+ if (cat.rawFindings.length > 0) {
5402
+ lines.push("**\uC810\uAC80 \uACB0\uACFC:**");
5403
+ lines.push("");
5404
+ for (const f of cat.rawFindings) {
5405
+ const icon = f.found ? "\u2705" : "\u274C";
5406
+ lines.push(`- ${icon} ${f.item} \u2014 ${f.details}`);
5407
+ }
5408
+ lines.push("");
5400
5409
  }
5401
- if (!inTable) continue;
5402
- if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
5403
- inTable = false;
5404
- continue;
5410
+ if (cat.grade === "A" || cat.grade === "B") {
5411
+ lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
5412
+ } else if (cat.grade === "C") {
5413
+ lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
5414
+ } else if (cat.grade === "D") {
5415
+ lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
5416
+ } else {
5417
+ lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
5405
5418
  }
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++;
5410
- }
5411
- return count;
5412
- }
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;
5419
+ lines.push("");
5420
+ if (cat.recommendations.length > 0) {
5421
+ lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
5422
+ lines.push("");
5423
+ for (const r of cat.recommendations) {
5424
+ const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
5425
+ lines.push(`- ${icon} ${r.message}`);
5426
+ lines.push(` - \u2192 ${r.action}`);
5427
+ }
5428
+ lines.push("");
5422
5429
  }
5423
- if (!inDomainSection) continue;
5424
- if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
5425
- inDomainSection = false;
5426
- continue;
5430
+ lines.push("---");
5431
+ lines.push("");
5432
+ }
5433
+ lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
5434
+ lines.push("");
5435
+ const critical = result.categories.filter((c2) => c2.grade === "F");
5436
+ const warning = result.categories.filter((c2) => c2.grade === "D");
5437
+ const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
5438
+ if (critical.length > 0) {
5439
+ lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
5440
+ lines.push("");
5441
+ lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
5442
+ lines.push("|------|----------|------|------|");
5443
+ for (const c2 of critical) {
5444
+ const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
5445
+ lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
5427
5446
  }
5428
- if (/^###\s+\w/.test(line)) count++;
5447
+ lines.push("");
5429
5448
  }
5430
- return count;
5431
- }
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;
5449
+ if (warning.length > 0) {
5450
+ lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
5451
+ lines.push("");
5452
+ lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
5453
+ lines.push("|------|----------|------|------|");
5454
+ for (const c2 of warning) {
5455
+ const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
5456
+ lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
5441
5457
  }
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
- }
5458
+ lines.push("");
5459
+ }
5460
+ if (ok.length > 0) {
5461
+ lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
5462
+ lines.push("");
5463
+ for (const c2 of ok) {
5464
+ lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
5452
5465
  }
5466
+ lines.push("");
5453
5467
  }
5454
- await walk(srcDir);
5455
- return count;
5468
+ lines.push("---");
5469
+ lines.push("");
5470
+ lines.push("## \uACB0\uB860");
5471
+ lines.push("");
5472
+ lines.push(result.summary);
5473
+ lines.push("");
5474
+ if (result.totalGrade === "F" || result.totalGrade === "D") {
5475
+ const immediateActions = critical.length + warning.length;
5476
+ 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.`);
5477
+ } else if (result.totalGrade === "C") {
5478
+ 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.");
5479
+ } else {
5480
+ 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.");
5481
+ }
5482
+ lines.push("");
5483
+ lines.push("---");
5484
+ lines.push("");
5485
+ lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
5486
+ return lines.join("\n") + "\n";
5456
5487
  }
5457
- var EVAL_GATE_WINDOW_MS, STAGE_GATES;
5458
- var init_gates = __esm({
5459
- "src/core/gates.ts"() {
5488
+ var GRADE_COLORS;
5489
+ var init_reporter = __esm({
5490
+ "src/stages/evaluation/reporter.ts"() {
5460
5491
  "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
5492
+ GRADE_COLORS = {
5493
+ A: chalk5.hex("#6BCB77"),
5494
+ B: chalk5.hex("#7B93DB"),
5495
+ C: chalk5.hex("#E2B340"),
5496
+ D: chalk5.hex("#E06C75"),
5497
+ F: chalk5.hex("#CC4444")
5470
5498
  };
5471
5499
  }
5472
5500
  });
5473
5501
 
5474
- // src/stages/validation/runner.ts
5502
+ // src/utils/shell-cd.ts
5503
+ var shell_cd_exports = {};
5504
+ __export(shell_cd_exports, {
5505
+ installShellHelper: () => installShellHelper,
5506
+ requestCdAfter: () => requestCdAfter
5507
+ });
5475
5508
  import { join as join10 } from "path";
5509
+ import { homedir as homedir2 } from "os";
5476
5510
  import fs18 from "fs-extra";
5477
- async function runTests(cwd) {
5478
- const start = Date.now();
5479
- const gstackPath = join10(cwd, ".pai", "gstack.json");
5480
- let runner = "npm test";
5481
- if (await fs18.pathExists(gstackPath)) {
5482
- try {
5483
- const config = await fs18.readJson(gstackPath);
5484
- if (config.testRunner === "vitest") runner = "npx vitest run";
5485
- else if (config.testRunner === "jest") runner = "npx jest";
5486
- else if (config.testRunner === "mocha") runner = "npx mocha";
5487
- } catch {
5511
+ async function requestCdAfter(targetDir) {
5512
+ await fs18.ensureDir(PAI_DIR);
5513
+ await fs18.writeFile(CD_FILE, targetDir);
5514
+ }
5515
+ async function installShellHelper() {
5516
+ await fs18.ensureDir(PAI_DIR);
5517
+ if (isWindows) {
5518
+ return installPowerShellHelper();
5519
+ }
5520
+ return installBashHelper();
5521
+ }
5522
+ async function installBashHelper() {
5523
+ await fs18.writeFile(HELPER_FILE_SH, BASH_HELPER);
5524
+ const rcFile = getShellRcPath();
5525
+ const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
5526
+ if (await fs18.pathExists(rcFile)) {
5527
+ const content = await fs18.readFile(rcFile, "utf8");
5528
+ if (content.includes("shell-helper.sh")) {
5529
+ return true;
5488
5530
  }
5531
+ await fs18.appendFile(rcFile, `
5532
+ # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5533
+ ${sourceLine}
5534
+ `);
5535
+ return false;
5489
5536
  }
5490
- const pkgPath = join10(cwd, "package.json");
5491
- if (await fs18.pathExists(pkgPath)) {
5492
- try {
5493
- const pkg5 = await fs18.readJson(pkgPath);
5494
- if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
5495
- return {
5496
- runner,
5497
- passed: false,
5498
- output: "\uD14C\uC2A4\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC815\uC758\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. package.json\uC758 scripts.test\uB97C \uC124\uC815\uD558\uC138\uC694.",
5499
- duration: Date.now() - start
5500
- };
5501
- }
5502
- } catch {
5537
+ await fs18.writeFile(rcFile, `${sourceLine}
5538
+ `);
5539
+ return false;
5540
+ }
5541
+ async function installPowerShellHelper() {
5542
+ await fs18.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
5543
+ const rcFile = getShellRcPath();
5544
+ const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
5545
+ await fs18.ensureDir(join10(rcFile, ".."));
5546
+ if (await fs18.pathExists(rcFile)) {
5547
+ const content = await fs18.readFile(rcFile, "utf8");
5548
+ if (content.includes("shell-helper.ps1")) {
5549
+ return true;
5550
+ }
5551
+ await fs18.appendFile(rcFile, `
5552
+ # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
5553
+ ${sourceLine}
5554
+ `);
5555
+ return false;
5556
+ }
5557
+ await fs18.writeFile(rcFile, `${sourceLine}
5558
+ `);
5559
+ return false;
5560
+ }
5561
+ var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
5562
+ var init_shell_cd = __esm({
5563
+ "src/utils/shell-cd.ts"() {
5564
+ "use strict";
5565
+ init_platform();
5566
+ PAI_DIR = join10(homedir2(), ".pai");
5567
+ CD_FILE = join10(PAI_DIR, ".cd-after");
5568
+ HELPER_FILE_SH = join10(PAI_DIR, "shell-helper.sh");
5569
+ HELPER_FILE_PS1 = join10(PAI_DIR, "shell-helper.ps1");
5570
+ BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
5571
+ pai() {
5572
+ local cd_target="$HOME/.pai/.cd-after"
5573
+ rm -f "$cd_target"
5574
+ PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
5575
+ local exit_code=$?
5576
+ if [ -f "$cd_target" ]; then
5577
+ local dir
5578
+ dir="$(cat "$cd_target")"
5579
+ rm -f "$cd_target"
5580
+ if [ -n "$dir" ] && [ -d "$dir" ]; then
5581
+ cd "$dir" || true
5582
+ echo " \u2192 cd $dir"
5583
+ fi
5584
+ fi
5585
+ return $exit_code
5586
+ }
5587
+ `;
5588
+ POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
5589
+ function pai {
5590
+ $cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
5591
+ Remove-Item $cdTarget -ErrorAction SilentlyContinue
5592
+ $env:PAI_CD_AFTER = $cdTarget
5593
+ & npx pai-zero @args
5594
+ $exitCode = $LASTEXITCODE
5595
+ if (Test-Path $cdTarget) {
5596
+ $dir = Get-Content $cdTarget -Raw
5597
+ Remove-Item $cdTarget -ErrorAction SilentlyContinue
5598
+ if ($dir -and (Test-Path $dir)) {
5599
+ Set-Location $dir
5600
+ Write-Host " -> cd $dir"
5503
5601
  }
5504
5602
  }
5603
+ return $exitCode
5604
+ }
5605
+ `;
5606
+ }
5607
+ });
5608
+
5609
+ // src/utils/claude-settings.ts
5610
+ var claude_settings_exports = {};
5611
+ __export(claude_settings_exports, {
5612
+ ClaudeSettingsError: () => ClaudeSettingsError,
5613
+ buildSkeleton: () => buildSkeleton,
5614
+ enableOmcPlugin: () => enableOmcPlugin,
5615
+ getClaudeSettingsPath: () => getClaudeSettingsPath,
5616
+ isAlreadyEnabled: () => isAlreadyEnabled,
5617
+ mergeOmcIntoSettings: () => mergeOmcIntoSettings
5618
+ });
5619
+ import os3 from "os";
5620
+ import path9 from "path";
5621
+ import fs19 from "fs-extra";
5622
+ function getClaudeSettingsPath(homeDir = os3.homedir()) {
5623
+ return path9.join(homeDir, ".claude", "settings.json");
5624
+ }
5625
+ function parseJsonWithBom(raw) {
5626
+ const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
5627
+ return JSON.parse(stripped);
5628
+ }
5629
+ function timestampSuffix() {
5630
+ return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
5631
+ }
5632
+ async function enableOmcPlugin(options = {}) {
5633
+ const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
5634
+ const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
5635
+ const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
5636
+ const wantBackup = options.backup ?? true;
5637
+ const settingsPath = getClaudeSettingsPath();
5638
+ await fs19.ensureDir(path9.dirname(settingsPath));
5639
+ if (!await fs19.pathExists(settingsPath)) {
5640
+ const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
5641
+ await fs19.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
5642
+ return { action: "created", settingsPath };
5643
+ }
5644
+ const raw = await fs19.readFile(settingsPath, "utf8");
5645
+ let parsed;
5505
5646
  try {
5506
- const { execa } = await import("execa");
5507
- const { stdout, stderr } = await execa("npm", ["test"], {
5508
- cwd,
5509
- timeout: 12e4,
5510
- env: { ...process.env, CI: "true" }
5511
- });
5512
- return {
5513
- runner,
5514
- passed: true,
5515
- output: stdout || stderr,
5516
- duration: Date.now() - start
5517
- };
5647
+ const value = parseJsonWithBom(raw);
5648
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5649
+ throw new Error("settings.json is not a JSON object");
5650
+ }
5651
+ parsed = value;
5518
5652
  } catch (err) {
5519
- const output = err instanceof Error ? err.message : String(err);
5520
- return {
5521
- runner,
5522
- passed: false,
5523
- output,
5524
- duration: Date.now() - start
5525
- };
5653
+ const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
5654
+ await fs19.copy(settingsPath, backupPath2);
5655
+ throw new ClaudeSettingsError(
5656
+ `settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
5657
+ backupPath2
5658
+ );
5659
+ }
5660
+ if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
5661
+ return { action: "already-enabled", settingsPath };
5662
+ }
5663
+ let backupPath;
5664
+ if (wantBackup) {
5665
+ backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
5666
+ await fs19.copy(settingsPath, backupPath);
5526
5667
  }
5668
+ const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
5669
+ await fs19.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
5670
+ return { action: "added", settingsPath, backupPath };
5671
+ }
5672
+ function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
5673
+ return {
5674
+ extraKnownMarketplaces: {
5675
+ [marketplaceId]: {
5676
+ source: { source: "git", url: marketplaceUrl }
5677
+ }
5678
+ },
5679
+ enabledPlugins: {
5680
+ [pluginId]: true
5681
+ }
5682
+ };
5527
5683
  }
5528
- var init_runner = __esm({
5529
- "src/stages/validation/runner.ts"() {
5684
+ function isAlreadyEnabled(settings, marketplaceId, pluginId) {
5685
+ const markets = settings["extraKnownMarketplaces"];
5686
+ const enabled = settings["enabledPlugins"];
5687
+ const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
5688
+ const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
5689
+ return hasMarket && hasPlugin;
5690
+ }
5691
+ function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
5692
+ const next = { ...settings };
5693
+ const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
5694
+ existingMarkets[marketplaceId] = {
5695
+ source: { source: "git", url: marketplaceUrl }
5696
+ };
5697
+ next["extraKnownMarketplaces"] = existingMarkets;
5698
+ const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
5699
+ existingPlugins[pluginId] = true;
5700
+ next["enabledPlugins"] = existingPlugins;
5701
+ return next;
5702
+ }
5703
+ var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
5704
+ var init_claude_settings = __esm({
5705
+ "src/utils/claude-settings.ts"() {
5530
5706
  "use strict";
5707
+ init_platform();
5708
+ DEFAULT_MARKETPLACE_ID = "omc";
5709
+ DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
5710
+ DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
5711
+ ClaudeSettingsError = class extends Error {
5712
+ constructor(message, backupPath) {
5713
+ super(message);
5714
+ this.backupPath = backupPath;
5715
+ this.name = "ClaudeSettingsError";
5716
+ }
5717
+ backupPath;
5718
+ };
5531
5719
  }
5532
5720
  });
5533
5721
 
5534
- // src/stages/validation/harness.ts
5722
+ // src/stages/evaluation/cache.ts
5723
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
5535
5724
  import { join as join11 } from "path";
5536
- import fs19 from "fs-extra";
5537
- async function runHarnessCheck(cwd) {
5538
- const harnessPath = join11(cwd, ".pai", "harness.json");
5539
- if (!await fs19.pathExists(harnessPath)) {
5540
- return { enabled: false, specFile: null, rules: [], checks: [] };
5725
+ import { createHash } from "crypto";
5726
+ function computeRepoHash(repoPath) {
5727
+ const hash = createHash("sha256");
5728
+ for (const file of FILES_TO_HASH) {
5729
+ const fullPath = join11(repoPath, file);
5730
+ try {
5731
+ const content = readFileSync(fullPath);
5732
+ hash.update(`${file}:${content.length}`);
5733
+ } catch {
5734
+ hash.update(`${file}:missing`);
5735
+ }
5541
5736
  }
5542
- let config;
5737
+ return hash.digest("hex").slice(0, 16);
5738
+ }
5739
+ function getCachePath(repoPath) {
5740
+ return join11(repoPath, CACHE_DIR, CACHE_FILE);
5741
+ }
5742
+ function loadCache(repoPath) {
5543
5743
  try {
5544
- config = await fs19.readJson(harnessPath);
5744
+ const data = readFileSync(getCachePath(repoPath), "utf-8");
5745
+ return JSON.parse(data);
5545
5746
  } catch {
5546
- return { enabled: false, specFile: null, rules: [], checks: [] };
5547
- }
5548
- const specFile = config.specFile ?? "docs/openspec.md";
5549
- const rules = config.rules ?? [];
5550
- const checks = [];
5551
- if (rules.includes("spec-implementation-match")) {
5552
- const specExists = await fs19.pathExists(join11(cwd, specFile));
5553
- const srcExists = await fs19.pathExists(join11(cwd, "src"));
5554
- checks.push({
5555
- rule: "spec-implementation-match",
5556
- passed: specExists && srcExists,
5557
- detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
5558
- });
5559
- }
5560
- if (rules.includes("api-contract-test")) {
5561
- const testDir = await fs19.pathExists(join11(cwd, "tests"));
5562
- const testDir2 = await fs19.pathExists(join11(cwd, "test"));
5563
- checks.push({
5564
- rule: "api-contract-test",
5565
- passed: testDir || testDir2,
5566
- detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
5567
- });
5747
+ return { version: 1, entries: {} };
5568
5748
  }
5569
- return { enabled: true, specFile, rules, checks };
5570
5749
  }
5571
- var init_harness = __esm({
5572
- "src/stages/validation/harness.ts"() {
5573
- "use strict";
5574
- }
5575
- });
5576
-
5577
- // src/cli/commands/validate.cmd.ts
5578
- var validate_cmd_exports = {};
5579
- __export(validate_cmd_exports, {
5580
- handleRobocoCliError: () => handleRobocoError,
5581
- validateCommand: () => validateCommand
5582
- });
5583
- async function validateCommand(cwd, options = {}) {
5584
- try {
5585
- await withRoboco(
5586
- cwd,
5587
- "validation",
5588
- "pai test",
5589
- { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
5590
- async () => {
5591
- section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
5592
- const testResult = await runTests(cwd);
5593
- if (testResult.passed) {
5594
- success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
5595
- } else {
5596
- error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
5597
- info(testResult.output.slice(0, 300));
5598
- }
5599
- section("\uD558\uB124\uC2A4 \uAC80\uC99D");
5600
- const harness = await runHarnessCheck(cwd);
5601
- if (!harness.enabled) {
5602
- info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
5603
- info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
5604
- } else {
5605
- for (const check of harness.checks) {
5606
- if (check.passed) {
5607
- success(`${check.rule}: ${check.detail}`);
5608
- } else {
5609
- warn(`${check.rule}: ${check.detail}`);
5610
- }
5611
- }
5612
- }
5613
- const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
5614
- console.log("");
5615
- if (allPassed) {
5616
- success("\uAC80\uC99D \uD1B5\uACFC!");
5617
- } else if (!testResult.passed) {
5618
- error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
5619
- throw new Error("tests-failed");
5620
- } else {
5621
- warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
5622
- }
5623
- }
5624
- );
5625
- await syncHandoff(cwd).catch(() => {
5626
- });
5627
- } catch (err) {
5628
- handleRobocoError(err);
5750
+ function saveCache(repoPath, store) {
5751
+ const cacheDir = join11(repoPath, CACHE_DIR, "cache");
5752
+ if (!existsSync(cacheDir)) {
5753
+ mkdirSync(cacheDir, { recursive: true });
5629
5754
  }
5755
+ writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
5630
5756
  }
5631
- function handleRobocoError(err) {
5632
- if (err instanceof RobocoLockError) {
5633
- error(err.message);
5634
- process.exitCode = 1;
5635
- return;
5636
- }
5637
- if (err instanceof GateFailedError) {
5638
- error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
5639
- for (const v of err.result.violations) {
5640
- warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
5641
- }
5642
- hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
5643
- process.exitCode = 1;
5644
- return;
5645
- }
5646
- if (err instanceof Error) {
5647
- if (err.message === "tests-failed") {
5648
- process.exitCode = 1;
5649
- return;
5757
+ function getCachedResult(repoPath) {
5758
+ const store = loadCache(repoPath);
5759
+ const repoHash = computeRepoHash(repoPath);
5760
+ const entry = store.entries[repoHash];
5761
+ if (!entry) return null;
5762
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
5763
+ return entry.llmOutput;
5764
+ }
5765
+ function setCachedResult(repoPath, llmOutput) {
5766
+ const store = loadCache(repoPath);
5767
+ const repoHash = computeRepoHash(repoPath);
5768
+ const now2 = Date.now();
5769
+ for (const [key, entry] of Object.entries(store.entries)) {
5770
+ if (now2 - entry.timestamp > CACHE_TTL_MS) {
5771
+ delete store.entries[key];
5650
5772
  }
5651
- error(err.message);
5652
- process.exitCode = 1;
5653
- return;
5654
5773
  }
5655
- throw err;
5774
+ store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
5775
+ saveCache(repoPath, store);
5656
5776
  }
5657
- var init_validate_cmd = __esm({
5658
- "src/cli/commands/validate.cmd.ts"() {
5777
+ var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
5778
+ var init_cache = __esm({
5779
+ "src/stages/evaluation/cache.ts"() {
5659
5780
  "use strict";
5660
- init_ui();
5661
- init_runner();
5662
- init_harness();
5663
- init_roboco();
5664
- init_gates();
5781
+ CACHE_DIR = ".pai";
5782
+ CACHE_FILE = "cache/evaluation.json";
5783
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5784
+ FILES_TO_HASH = [
5785
+ "package.json",
5786
+ "pyproject.toml",
5787
+ "go.mod",
5788
+ "Cargo.toml",
5789
+ "tsconfig.json",
5790
+ "jest.config.ts",
5791
+ "vitest.config.ts",
5792
+ ".github/workflows",
5793
+ ".gitlab-ci.yml",
5794
+ ".husky",
5795
+ ".lintstagedrc",
5796
+ "CLAUDE.md",
5797
+ "AGENTS.md",
5798
+ ".cursorrules",
5799
+ "README.md",
5800
+ "CONTRIBUTING.md"
5801
+ ];
5665
5802
  }
5666
5803
  });
5667
5804
 
@@ -6097,7 +6234,24 @@ async function setupInDirectory(projectDir, projectName) {
6097
6234
  config,
6098
6235
  previousResults: /* @__PURE__ */ new Map()
6099
6236
  };
6100
- const result = await environmentStage.run(input);
6237
+ let stageResult;
6238
+ try {
6239
+ await withRoboco(
6240
+ projectDir,
6241
+ "environment",
6242
+ "pai init",
6243
+ {},
6244
+ async () => {
6245
+ stageResult = await environmentStage.run(input);
6246
+ }
6247
+ );
6248
+ await syncHandoff(projectDir).catch(() => {
6249
+ });
6250
+ } catch (err) {
6251
+ handleRobocoError(err);
6252
+ return;
6253
+ }
6254
+ const result = stageResult;
6101
6255
  if (result.status === "success") {
6102
6256
  try {
6103
6257
  const { requestCdAfter: requestCdAfter2 } = await Promise.resolve().then(() => (init_shell_cd(), shell_cd_exports));
@@ -6520,6 +6674,8 @@ var init_init_cmd = __esm({
6520
6674
  init_environment();
6521
6675
  init_config();
6522
6676
  init_detector();
6677
+ init_roboco();
6678
+ init_validate_cmd();
6523
6679
  init_ui();
6524
6680
  init_logger();
6525
6681
  }
@@ -7123,6 +7279,29 @@ async function runPipeline(cwd, config, options = {}) {
7123
7279
  config,
7124
7280
  previousResults: ctx.getResults()
7125
7281
  };
7282
+ if (!options.force) {
7283
+ const gateResult = await STAGE_GATES[stageName](cwd).catch(() => null);
7284
+ if (gateResult) {
7285
+ await saveGateResult(cwd, `${stageName}.entry`, gateResult).catch(() => {
7286
+ });
7287
+ if (!gateResult.passed && !options.skipGates) {
7288
+ error(`${stageName} \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 \uD30C\uC774\uD504\uB77C\uC778 \uC911\uB2E8`);
7289
+ for (const v of gateResult.violations) {
7290
+ warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
7291
+ }
7292
+ hint("\uC6B0\uD68C: pai run --skip-gates (\uACBD\uACE0) / --force (\uBB34\uC2DC)");
7293
+ await markStageEnd(cwd, stageName, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${gateResult.name}`).catch(() => {
7294
+ });
7295
+ shouldStop = true;
7296
+ break;
7297
+ }
7298
+ if (!gateResult.passed && options.skipGates) {
7299
+ for (const v of gateResult.violations) {
7300
+ warn(` [\uAC8C\uC774\uD2B8 \uACBD\uACE0] ${v.message}`);
7301
+ }
7302
+ }
7303
+ }
7304
+ }
7126
7305
  if (stage.canSkip(input)) {
7127
7306
  info(`${stageName} \u2014 \uAC74\uB108\uB700 (\uC774\uBBF8 \uC644\uB8CC)`);
7128
7307
  ctx.setResult(stageName, {
@@ -7135,8 +7314,14 @@ async function runPipeline(cwd, config, options = {}) {
7135
7314
  });
7136
7315
  continue;
7137
7316
  }
7317
+ await markStageStart(cwd, stageName, "pai run").catch(() => {
7318
+ });
7138
7319
  const result = await stage.run(input);
7139
7320
  ctx.setResult(stageName, result);
7321
+ const stageSuccess = result.status !== "failed";
7322
+ const failReason = result.errors[0]?.message;
7323
+ await markStageEnd(cwd, stageName, stageSuccess, failReason).catch(() => {
7324
+ });
7140
7325
  switch (result.status) {
7141
7326
  case "success":
7142
7327
  success(`${stageName} \uC644\uB8CC (${result.duration}ms)`);
@@ -7184,6 +7369,8 @@ var init_pipeline = __esm({
7184
7369
  init_execution();
7185
7370
  init_validation();
7186
7371
  init_evaluation();
7372
+ init_roboco();
7373
+ init_gates();
7187
7374
  init_ui();
7188
7375
  STAGES = {
7189
7376
  environment: environmentStage,
@@ -7203,23 +7390,17 @@ __export(pipeline_cmd_exports, {
7203
7390
  async function pipelineCommand(cwd, options) {
7204
7391
  printBanner();
7205
7392
  try {
7206
- await withRoboco(
7207
- cwd,
7208
- "environment",
7209
- "pai run",
7210
- {
7211
- skipGates: options.skipGates,
7212
- force: options.force,
7213
- ttlMs: PIPELINE_TTL_MS
7214
- },
7215
- async () => {
7216
- const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
7217
- const pipelineOpts = {};
7218
- if (options.from) pipelineOpts.from = options.from;
7219
- if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7220
- await runPipeline(cwd, config, pipelineOpts);
7221
- }
7222
- );
7393
+ await acquireLock(cwd, "environment", "pai run", PIPELINE_TTL_MS);
7394
+ try {
7395
+ const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
7396
+ const pipelineOpts = { skipGates: options.skipGates, force: options.force };
7397
+ if (options.from) pipelineOpts.from = options.from;
7398
+ if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7399
+ await runPipeline(cwd, config, pipelineOpts);
7400
+ } finally {
7401
+ await releaseLock(cwd).catch(() => {
7402
+ });
7403
+ }
7223
7404
  await syncHandoff(cwd).catch(() => {
7224
7405
  });
7225
7406
  } catch (err) {