pai-zero 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/pai.js CHANGED
@@ -54,6 +54,13 @@ var init_logger = __esm({
54
54
  });
55
55
 
56
56
  // src/core/config.ts
57
+ var config_exports = {};
58
+ __export(config_exports, {
59
+ createDefaultConfig: () => createDefaultConfig,
60
+ loadConfig: () => loadConfig,
61
+ normalizeMode: () => normalizeMode,
62
+ saveConfig: () => saveConfig
63
+ });
57
64
  import path from "path";
58
65
  import { createRequire } from "module";
59
66
  import fs from "fs-extra";
@@ -2087,34 +2094,33 @@ async function provisionGitHub(ctx) {
2087
2094
  await Promise.all(dirs.map((d) => fs7.ensureDir(join4(ctx.cwd, d))));
2088
2095
  const handoffPath = join4(ctx.cwd, "handoff.md");
2089
2096
  if (!await fs7.pathExists(handoffPath)) {
2090
- const now = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
2097
+ const now2 = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
2091
2098
  await fs7.writeFile(handoffPath, [
2092
2099
  `# Handoff \u2014 ${ctx.projectName}`,
2093
2100
  "",
2094
- `> \uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8: ${now}`,
2101
+ `> \uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8: ${now2}`,
2102
+ `> **\uCC38\uACE0**: \`<!-- roboco:start -->\` \uBE14\uB85D\uC740 \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC2DC \uC790\uB3D9 \uAC31\uC2E0\uB429\uB2C8\uB2E4.`,
2103
+ `> \uADF8 \uC678 \uC139\uC158(\uB2E4\uC74C \uB2E8\uACC4, \uBA54\uBAA8)\uC740 \uC790\uC720\uB86D\uAC8C \uC218\uB3D9 \uD3B8\uC9D1 \uAC00\uB2A5.`,
2095
2104
  "",
2096
- "## \uC9C4\uD589 \uC911",
2097
- "- [ ] Ideation \uBB38\uC11C \uC791\uC131 \u2014 \uC2EC\uCE35 \uC778\uD130\uBDF0\uB97C \uD1B5\uD574 \uC544\uC774\uB514\uC5B4\uB97C \uAD6C\uCCB4\uD654 (\uB2F4\uB2F9: PAI > Design)",
2105
+ "<!-- roboco:start -->",
2106
+ "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)",
2098
2107
  "",
2099
- "## \uB2E4\uC74C \uB2E8\uACC4",
2100
- "- [ ] Ideation \uAE30\uBC18 PRD \uC791\uC131 \u2014 \uC2EC\uCE35 \uC778\uD130\uBDF0\uB85C \uB9E5\uB77D/\uAE30\uC220 \uC81C\uC57D \uBCF4\uAC15",
2101
- "- [ ] \uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1 \uAD6C\uC131 \uD655\uC778 \u2014 \uC5D0\uC774\uC804\uD2B8 \uC791\uC5C5 \uD658\uACBD \uAC80\uC99D",
2102
- "- [ ] AI \uCF54\uB4DC \uC0DD\uC131 \uC2DC\uC791",
2108
+ "\uD604\uC7AC \uB2E8\uACC4: **environment**",
2103
2109
  "",
2104
- "## \uC644\uB8CC",
2105
- `- [x] \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (${now}) (\uB2F4\uB2F9: PAI > Environment)`,
2110
+ "### \uC644\uB8CC",
2111
+ `- [x] environment (pai init, ${now2.slice(0, 10)})`,
2112
+ "<!-- roboco:end -->",
2106
2113
  "",
2107
- "## \uC791\uC5C5 \uD30C\uC774\uD504\uB77C\uC778",
2108
- "```",
2109
- "1. /pai design \u2192 Ideation \uC2EC\uCE35 \uC778\uD130\uBDF0 \u2192 docs/ideation.md \uC0DD\uC131",
2110
- "2. /pai design \u2192 PRD \uC2EC\uCE35 \uC778\uD130\uBDF0 \u2192 docs/openspec.md \uC791\uC131",
2111
- "3. /pai design \u2192 \uAE30\uC220 \uC81C\uC57D \uC2EC\uCE35 \uC778\uD130\uBDF0 \u2192 \uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1 \uD655\uC778",
2112
- "4. AI \uCF54\uB4DC \uC0DD\uC131 \u2192 \uAC80\uC99D \u2192 \uD3C9\uAC00",
2113
- "```",
2114
+ "## \uB2E4\uC74C \uB2E8\uACC4",
2115
+ "- [ ] `/pai design` \u2014 Ideation + PRD \uC2EC\uCE35 \uC778\uD130\uBDF0",
2116
+ "- [ ] Claude Code\uB85C \uCF54\uB4DC \uC0DD\uC131",
2117
+ "- [ ] `pai test` \u2014 \uD14C\uC2A4\uD2B8 + \uD558\uB124\uC2A4 \uAC80\uC99D",
2118
+ "- [ ] `pai grade` \u2014 AI \uC900\uBE44\uB3C4 6\uCE74\uD14C\uACE0\uB9AC \uD3C9\uAC00",
2114
2119
  "",
2115
2120
  "## \uBA54\uBAA8",
2116
2121
  `- \uBAA8\uB4DC: ${ctx.mode}`,
2117
- "- \uCEE8\uD14D\uC2A4\uD2B8\uAC00 \uBD80\uC871\uD558\uBA74 AI\uAC00 \uC790\uB3D9\uC73C\uB85C \uC2EC\uCE35 \uC778\uD130\uBDF0\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4"
2122
+ "- \uCEE8\uD14D\uC2A4\uD2B8\uAC00 \uBD80\uC871\uD558\uBA74 AI\uAC00 \uC790\uB3D9\uC73C\uB85C \uC2EC\uCE35 \uC778\uD130\uBDF0\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4",
2123
+ "- \uB77D \uCDA9\uB3CC \uC2DC \uC989\uC2DC \uC2E4\uD328 (\uB2E4\uB978 PAI \uBA85\uB839\uC774 \uC9C4\uD589 \uC911\uC774\uBA74 \uC644\uB8CC \uD6C4 \uC7AC\uC2DC\uB3C4)"
2118
2124
  ].join("\n") + "\n");
2119
2125
  }
2120
2126
  const contribPath = join4(ctx.cwd, "CONTRIBUTING.md");
@@ -2485,13 +2491,20 @@ async function provisionGstack(ctx) {
2485
2491
  }
2486
2492
  async function provisionRoboco(ctx) {
2487
2493
  await fs7.ensureDir(join4(ctx.cwd, ".pai"));
2488
- const robocoPath = join4(ctx.cwd, ".pai", "roboco.json");
2489
- if (await fs7.pathExists(robocoPath)) return;
2490
- await fs7.writeJson(robocoPath, {
2491
- version: "1.0",
2492
- checks: ["github", "vercel", "supabase", "openspec", "omc"],
2493
- reportOutput: "AI_READINESS_REPORT.md",
2494
- note: "PAI Zero\uC5D0 \uB0B4\uC7AC\uD654\uB428. `pai evaluate`\uB85C \uC9C4\uB2E8 \uAC00\uB2A5."
2494
+ const robocoPath2 = join4(ctx.cwd, ".pai", "roboco.json");
2495
+ if (await fs7.pathExists(robocoPath2)) {
2496
+ try {
2497
+ const existing = await fs7.readJson(robocoPath2);
2498
+ if (existing?.version === "2.0") return;
2499
+ } catch {
2500
+ }
2501
+ }
2502
+ await fs7.writeJson(robocoPath2, {
2503
+ version: "2.0",
2504
+ currentStage: "environment",
2505
+ stageHistory: [],
2506
+ lock: null,
2507
+ gates: {}
2495
2508
  }, { spaces: 2 });
2496
2509
  }
2497
2510
  async function provisionHarness(ctx) {
@@ -3592,6 +3605,540 @@ var init_detector = __esm({
3592
3605
  }
3593
3606
  });
3594
3607
 
3608
+ // src/core/roboco.ts
3609
+ import os2 from "os";
3610
+ import path5 from "path";
3611
+ import fs10 from "fs-extra";
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();
3618
+ try {
3619
+ const raw = await fs10.readJson(p);
3620
+ if (raw?.version !== "2.0") return freshState();
3621
+ return raw;
3622
+ } catch {
3623
+ return freshState();
3624
+ }
3625
+ }
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) {
3641
+ try {
3642
+ process.kill(pid, 0);
3643
+ return true;
3644
+ } catch {
3645
+ return false;
3646
+ }
3647
+ }
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;
3652
+ }
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
+ }
3659
+ }
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;
3672
+ }
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);
3678
+ }
3679
+ }
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;
3711
+ }
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) {
3731
+ }
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
+ });
3747
+ }
3748
+ }
3749
+ async function syncHandoff(cwd) {
3750
+ const handoffPath = path5.join(cwd, "handoff.md");
3751
+ if (!await fs10.pathExists(handoffPath)) return;
3752
+ const state = await loadRobocoState(cwd);
3753
+ const block = buildHandoffBlock(state);
3754
+ const content = await fs10.readFile(handoffPath, "utf8");
3755
+ const startIdx = content.indexOf(HANDOFF_START);
3756
+ const endIdx = content.indexOf(HANDOFF_END);
3757
+ let next;
3758
+ if (startIdx >= 0 && endIdx > startIdx) {
3759
+ next = content.slice(0, startIdx) + block + content.slice(endIdx + HANDOFF_END.length);
3760
+ } else {
3761
+ const sep = content.endsWith("\n") ? "\n" : "\n\n";
3762
+ next = content + sep + block + "\n";
3763
+ }
3764
+ await fs10.writeFile(handoffPath, next);
3765
+ }
3766
+ function buildHandoffBlock(state) {
3767
+ const lines = [HANDOFF_START, "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)", ""];
3768
+ const done = state.stageHistory.filter((h) => h.status === "done");
3769
+ const inProgress = state.stageHistory.filter((h) => h.status === "in_progress");
3770
+ const failed = state.stageHistory.filter((h) => h.status === "failed");
3771
+ if (inProgress.length > 0) {
3772
+ lines.push("### \uC9C4\uD589 \uC911");
3773
+ for (const h of inProgress) {
3774
+ lines.push(`- [ ] ${h.stage} (${h.by}, \uC2DC\uC791 ${fmtDate(h.startedAt)})`);
3775
+ }
3776
+ lines.push("");
3777
+ }
3778
+ if (failed.length > 0) {
3779
+ lines.push("### \uC2E4\uD328");
3780
+ for (const h of failed) {
3781
+ lines.push(`- [x] ${h.stage} \u2014 ${h.reason ?? "\uC6D0\uC778 \uBD88\uBA85"} (${fmtDate(h.startedAt)})`);
3782
+ }
3783
+ lines.push("");
3784
+ }
3785
+ if (done.length > 0) {
3786
+ lines.push("### \uC644\uB8CC");
3787
+ for (const h of done) {
3788
+ lines.push(`- [x] ${h.stage} (${h.by}, ${fmtDate(h.endedAt ?? h.startedAt)})`);
3789
+ }
3790
+ lines.push("");
3791
+ }
3792
+ lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
3793
+ lines.push(HANDOFF_END);
3794
+ return lines.join("\n");
3795
+ }
3796
+ function fmtDate(iso) {
3797
+ try {
3798
+ const d = new Date(iso);
3799
+ return d.toISOString().slice(0, 16).replace("T", " ");
3800
+ } catch {
3801
+ return iso;
3802
+ }
3803
+ }
3804
+ var ROBOCO_PATH, DEFAULT_TTL_MS, RobocoLockError, GateFailedError, HANDOFF_START, HANDOFF_END;
3805
+ var init_roboco = __esm({
3806
+ "src/core/roboco.ts"() {
3807
+ "use strict";
3808
+ ROBOCO_PATH = path5.join(".pai", "roboco.json");
3809
+ DEFAULT_TTL_MS = 10 * 60 * 1e3;
3810
+ RobocoLockError = class extends Error {
3811
+ constructor(message, holder) {
3812
+ super(message);
3813
+ this.holder = holder;
3814
+ this.name = "RobocoLockError";
3815
+ }
3816
+ holder;
3817
+ };
3818
+ GateFailedError = class extends Error {
3819
+ constructor(result) {
3820
+ super(`\uAC8C\uC774\uD2B8 \uC2E4\uD328 (${result.name}): ${result.violations.length}\uAC74 \uC704\uBC18`);
3821
+ this.result = result;
3822
+ this.name = "GateFailedError";
3823
+ }
3824
+ result;
3825
+ };
3826
+ HANDOFF_START = "<!-- roboco:start -->";
3827
+ HANDOFF_END = "<!-- roboco:end -->";
3828
+ }
3829
+ });
3830
+
3831
+ // src/core/gates.ts
3832
+ var gates_exports = {};
3833
+ __export(gates_exports, {
3834
+ STAGE_GATES: () => STAGE_GATES,
3835
+ countBusinessRules: () => countBusinessRules,
3836
+ countDomainObjects: () => countDomainObjects,
3837
+ countEndpoints: () => countEndpoints,
3838
+ designEntryGate: () => designEntryGate,
3839
+ evaluationEntryGate: () => evaluationEntryGate,
3840
+ executionEntryGate: () => executionEntryGate,
3841
+ validateEdges: () => validateEdges,
3842
+ validationEntryGate: () => validationEntryGate
3843
+ });
3844
+ import path6 from "path";
3845
+ import fs11 from "fs-extra";
3846
+ function now() {
3847
+ return (/* @__PURE__ */ new Date()).toISOString();
3848
+ }
3849
+ function makeResult(name, violations) {
3850
+ return {
3851
+ name,
3852
+ passed: violations.length === 0,
3853
+ checkedAt: now(),
3854
+ violations
3855
+ };
3856
+ }
3857
+ async function designEntryGate(cwd) {
3858
+ const violations = [];
3859
+ const claudeMd = path6.join(cwd, "CLAUDE.md");
3860
+ if (!await fs11.pathExists(claudeMd)) {
3861
+ violations.push({
3862
+ rule: "claude-md-exists",
3863
+ message: "CLAUDE.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
3864
+ location: "CLAUDE.md"
3865
+ });
3866
+ }
3867
+ const configJson = path6.join(cwd, ".pai", "config.json");
3868
+ if (!await fs11.pathExists(configJson)) {
3869
+ violations.push({
3870
+ rule: "pai-config-exists",
3871
+ message: "PAI \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
3872
+ location: ".pai/config.json"
3873
+ });
3874
+ }
3875
+ return makeResult("design.entry", violations);
3876
+ }
3877
+ async function executionEntryGate(cwd) {
3878
+ const violations = [];
3879
+ const openspec = path6.join(cwd, "docs", "openspec.md");
3880
+ const hasOpenspec = await fs11.pathExists(openspec);
3881
+ if (!hasOpenspec) {
3882
+ violations.push({
3883
+ rule: "openspec-exists",
3884
+ message: "docs/openspec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3885
+ location: "docs/openspec.md"
3886
+ });
3887
+ } else {
3888
+ const content = await fs11.readFile(openspec, "utf8");
3889
+ const endpointCount = countEndpoints(content);
3890
+ if (endpointCount < 1) {
3891
+ violations.push({
3892
+ rule: "openspec-endpoints-min",
3893
+ message: `openspec.md\uC5D0 \uC815\uC758\uB41C API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694. /pai design \uC2E4\uD589 \uAD8C\uC7A5.`,
3894
+ location: "docs/openspec.md \xA7 5. API \uC5D4\uB4DC\uD3EC\uC778\uD2B8"
3895
+ });
3896
+ }
3897
+ }
3898
+ const omc = path6.join(cwd, ".pai", "omc.md");
3899
+ if (await fs11.pathExists(omc)) {
3900
+ const content = await fs11.readFile(omc, "utf8");
3901
+ const domainCount = countDomainObjects(content);
3902
+ if (domainCount < 1) {
3903
+ violations.push({
3904
+ rule: "omc-domain-min",
3905
+ message: "omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
3906
+ location: ".pai/omc.md \xA7 \uB3C4\uBA54\uC778 \uAC1D\uCCB4"
3907
+ });
3908
+ }
3909
+ }
3910
+ const edgeViolations = await validateEdges(cwd);
3911
+ for (const ev of edgeViolations) {
3912
+ violations.push({
3913
+ rule: `edge-${ev.edge.from}-${ev.edge.to}`,
3914
+ message: ev.message,
3915
+ location: ev.edge.path
3916
+ });
3917
+ }
3918
+ return makeResult("execution.entry", violations);
3919
+ }
3920
+ async function validationEntryGate(cwd) {
3921
+ const violations = [];
3922
+ const srcDir = path6.join(cwd, "src");
3923
+ if (!await fs11.pathExists(srcDir)) {
3924
+ violations.push({
3925
+ rule: "src-dir-exists",
3926
+ message: "src/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3927
+ location: "src/"
3928
+ });
3929
+ } else {
3930
+ const codeFileCount = await countCodeFiles(srcDir);
3931
+ if (codeFileCount === 0) {
3932
+ violations.push({
3933
+ rule: "code-files-min",
3934
+ message: "src/ \uB0B4\uBD80\uC5D0 \uCF54\uB4DC \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
3935
+ location: "src/"
3936
+ });
3937
+ }
3938
+ }
3939
+ const edgeViolations = await validateEdges(cwd);
3940
+ for (const ev of edgeViolations) {
3941
+ violations.push({
3942
+ rule: `edge-${ev.edge.from}-${ev.edge.to}`,
3943
+ message: ev.message,
3944
+ location: ev.edge.path
3945
+ });
3946
+ }
3947
+ return makeResult("validation.entry", violations);
3948
+ }
3949
+ async function evaluationEntryGate(cwd) {
3950
+ const violations = [];
3951
+ const state = await loadRobocoState(cwd);
3952
+ const recentValidation = [...state.stageHistory].reverse().find((h) => h.stage === "validation");
3953
+ if (!recentValidation) {
3954
+ violations.push({
3955
+ rule: "validation-executed",
3956
+ message: "validation \uB2E8\uACC4\uAC00 \uD55C \uBC88\uB3C4 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai test`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
3957
+ });
3958
+ } else if (recentValidation.status !== "done") {
3959
+ violations.push({
3960
+ rule: "validation-passed",
3961
+ message: `\uCD5C\uADFC validation\uC774 ${recentValidation.status} \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD14C\uC2A4\uD2B8\uB97C \uC131\uACF5\uC2DC\uD0A8 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.`
3962
+ });
3963
+ } else {
3964
+ const endedAt = recentValidation.endedAt ?? recentValidation.startedAt;
3965
+ const age = Date.now() - new Date(endedAt).getTime();
3966
+ if (age > EVAL_GATE_WINDOW_MS) {
3967
+ const mins = Math.round(age / 6e4);
3968
+ violations.push({
3969
+ rule: "validation-recent",
3970
+ message: `\uCD5C\uADFC validation \uD1B5\uACFC\uAC00 ${mins}\uBD84 \uC804\uC785\uB2C8\uB2E4 (1\uC2DC\uAC04 \uC774\uB0B4\uC5EC\uC57C \uD568). --force\uB85C \uC6B0\uD68C \uAC00\uB2A5.`
3971
+ });
3972
+ }
3973
+ }
3974
+ return makeResult("evaluation.entry", violations);
3975
+ }
3976
+ function countEndpoints(openspecContent) {
3977
+ const lines = openspecContent.split("\n");
3978
+ let inTable = false;
3979
+ let count = 0;
3980
+ for (const rawLine of lines) {
3981
+ const line = rawLine.trim();
3982
+ if (/^##\s*5\.|^##\s*API|API 엔드포인트/i.test(line)) {
3983
+ inTable = true;
3984
+ continue;
3985
+ }
3986
+ if (!inTable) continue;
3987
+ if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
3988
+ inTable = false;
3989
+ continue;
3990
+ }
3991
+ if (/^\|\s*Method/i.test(line)) continue;
3992
+ if (/^\|\s*[-:]+\s*\|/.test(line)) continue;
3993
+ const m = line.match(/^\|\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\|\s*(\/\S+)\s*\|/i);
3994
+ if (m) count++;
3995
+ }
3996
+ return count;
3997
+ }
3998
+ function countDomainObjects(omcContent) {
3999
+ const lines = omcContent.split("\n");
4000
+ let count = 0;
4001
+ let inDomainSection = false;
4002
+ for (const rawLine of lines) {
4003
+ const line = rawLine.trim();
4004
+ if (/^##\s*도메인 객체|^##\s*Domain Objects/i.test(line)) {
4005
+ inDomainSection = true;
4006
+ continue;
4007
+ }
4008
+ if (!inDomainSection) continue;
4009
+ if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
4010
+ inDomainSection = false;
4011
+ continue;
4012
+ }
4013
+ if (/^###\s+\w/.test(line)) count++;
4014
+ }
4015
+ return count;
4016
+ }
4017
+ async function validateEdges(cwd) {
4018
+ const config = await loadConfig(cwd);
4019
+ if (!config?.edges || config.edges.length === 0) return [];
4020
+ const violations = [];
4021
+ for (const edge of config.edges) {
4022
+ if (edge.path) {
4023
+ const fullPath = path6.join(cwd, edge.path);
4024
+ if (!await fs11.pathExists(fullPath)) {
4025
+ violations.push({
4026
+ edge,
4027
+ message: `${edge.from}\u2192${edge.to} \uACC4\uC57D \uD30C\uC77C \uC5C6\uC74C: ${edge.path}`
4028
+ });
4029
+ continue;
4030
+ }
4031
+ }
4032
+ switch (edge.via) {
4033
+ case "specFile":
4034
+ case "endpoints": {
4035
+ const specPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, "docs", "openspec.md");
4036
+ if (await fs11.pathExists(specPath)) {
4037
+ const content = await fs11.readFile(specPath, "utf8");
4038
+ const count = countEndpoints(content);
4039
+ if (count === 0) {
4040
+ violations.push({
4041
+ edge,
4042
+ message: `${edge.from}\u2192${edge.to}: openspec.md\uC5D0 \uC815\uC758\uB41C \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
4043
+ });
4044
+ }
4045
+ }
4046
+ break;
4047
+ }
4048
+ case "domain": {
4049
+ const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
4050
+ if (await fs11.pathExists(omcPath)) {
4051
+ const content = await fs11.readFile(omcPath, "utf8");
4052
+ const count = countDomainObjects(content);
4053
+ if (count === 0) {
4054
+ violations.push({
4055
+ edge,
4056
+ message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
4057
+ });
4058
+ }
4059
+ }
4060
+ break;
4061
+ }
4062
+ case "businessRules": {
4063
+ const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
4064
+ if (await fs11.pathExists(omcPath)) {
4065
+ const content = await fs11.readFile(omcPath, "utf8");
4066
+ const ruleCount = countBusinessRules(content);
4067
+ if (ruleCount === 0) {
4068
+ violations.push({
4069
+ edge,
4070
+ message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uBE44\uC988\uB2C8\uC2A4 \uADDC\uCE59\uC774 0\uAC1C \u2014 harness \uAC80\uC99D \uB300\uC0C1 \uC5C6\uC74C`
4071
+ });
4072
+ }
4073
+ }
4074
+ break;
4075
+ }
4076
+ }
4077
+ }
4078
+ return violations;
4079
+ }
4080
+ function countBusinessRules(omcContent) {
4081
+ const lines = omcContent.split("\n");
4082
+ let inSection = false;
4083
+ let count = 0;
4084
+ for (const rawLine of lines) {
4085
+ const line = rawLine.trim();
4086
+ if (/^##\s*비즈니스 규칙|^##\s*Business Rules/i.test(line)) {
4087
+ inSection = true;
4088
+ continue;
4089
+ }
4090
+ if (!inSection) continue;
4091
+ if (/^##\s/.test(line) && !/비즈니스|Business/i.test(line)) {
4092
+ inSection = false;
4093
+ continue;
4094
+ }
4095
+ if (/^-\s+\*\*/.test(line) || /^-\s+[^\s]/.test(line)) count++;
4096
+ }
4097
+ return count;
4098
+ }
4099
+ async function countCodeFiles(srcDir) {
4100
+ const exts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt"]);
4101
+ let count = 0;
4102
+ async function walk(dir) {
4103
+ let entries;
4104
+ try {
4105
+ entries = await fs11.readdir(dir);
4106
+ } catch {
4107
+ return;
4108
+ }
4109
+ for (const name of entries) {
4110
+ if (name.startsWith(".") || name === "node_modules") continue;
4111
+ const full = path6.join(dir, name);
4112
+ const stat = await fs11.stat(full).catch(() => null);
4113
+ if (!stat) continue;
4114
+ if (stat.isDirectory()) {
4115
+ await walk(full);
4116
+ } else if (exts.has(path6.extname(name))) {
4117
+ count++;
4118
+ }
4119
+ }
4120
+ }
4121
+ await walk(srcDir);
4122
+ return count;
4123
+ }
4124
+ var EVAL_GATE_WINDOW_MS, STAGE_GATES;
4125
+ var init_gates = __esm({
4126
+ "src/core/gates.ts"() {
4127
+ "use strict";
4128
+ init_roboco();
4129
+ init_config();
4130
+ EVAL_GATE_WINDOW_MS = 60 * 60 * 1e3;
4131
+ STAGE_GATES = {
4132
+ environment: async () => makeResult("environment.entry", []),
4133
+ // 항상 통과
4134
+ design: designEntryGate,
4135
+ execution: executionEntryGate,
4136
+ validation: validationEntryGate,
4137
+ evaluation: evaluationEntryGate
4138
+ };
4139
+ }
4140
+ });
4141
+
3595
4142
  // src/stages/environment/doctor.ts
3596
4143
  var doctor_exports = {};
3597
4144
  __export(doctor_exports, {
@@ -3599,7 +4146,7 @@ __export(doctor_exports, {
3599
4146
  });
3600
4147
  import { join as join6 } from "path";
3601
4148
  import { homedir } from "os";
3602
- import fs10 from "fs-extra";
4149
+ import fs12 from "fs-extra";
3603
4150
  async function runDoctor() {
3604
4151
  section("PAI Doctor \u2014 \uD658\uACBD \uC9C4\uB2E8");
3605
4152
  const checks = [];
@@ -3619,7 +4166,7 @@ async function runDoctor() {
3619
4166
  fix: claudeCheck.ok ? void 0 : "npm install -g @anthropic-ai/claude-code"
3620
4167
  });
3621
4168
  const globalConfigPath = join6(homedir(), ".pai", "config.json");
3622
- const hasGlobalConfig = await fs10.pathExists(globalConfigPath);
4169
+ const hasGlobalConfig = await fs12.pathExists(globalConfigPath);
3623
4170
  checks.push({
3624
4171
  label: "\uAE00\uB85C\uBC8C \uC124\uC815",
3625
4172
  ok: true,
@@ -3637,6 +4184,25 @@ async function runDoctor() {
3637
4184
  // optional이므로 항상 OK
3638
4185
  detail: hasSdk ? "\uC124\uCE58\uB428 (AI \uAE30\uB2A5 \uD65C\uC131\uD654)" : "\uBBF8\uC124\uCE58 (\uC815\uC801 \uBD84\uC11D \uBAA8\uB4DC)"
3639
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
+ }
3640
4206
  console.log("");
3641
4207
  let passed = 0;
3642
4208
  for (const check of checks) {
@@ -3675,8 +4241,8 @@ var init_doctor = __esm({
3675
4241
  });
3676
4242
 
3677
4243
  // src/utils/github-fetch.ts
3678
- import path5 from "path";
3679
- import fs11 from "fs-extra";
4244
+ import path7 from "path";
4245
+ import fs13 from "fs-extra";
3680
4246
  async function httpGet(url, timeoutMs, accept) {
3681
4247
  const controller = new AbortController();
3682
4248
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -3707,8 +4273,8 @@ async function downloadFile(downloadUrl, destPath, timeoutMs) {
3707
4273
  throw new Error(`Download failed ${res.status} ${res.statusText} (${downloadUrl})`);
3708
4274
  }
3709
4275
  const buf = Buffer.from(await res.arrayBuffer());
3710
- await fs11.ensureDir(path5.dirname(destPath));
3711
- await fs11.writeFile(destPath, buf);
4276
+ await fs13.ensureDir(path7.dirname(destPath));
4277
+ await fs13.writeFile(destPath, buf);
3712
4278
  }
3713
4279
  async function fetchGithubDir(opts) {
3714
4280
  const {
@@ -3732,10 +4298,10 @@ async function fetchGithubDir(opts) {
3732
4298
  for (const entry of entries) {
3733
4299
  const relName = entry.name;
3734
4300
  if (entry.type === "dir") {
3735
- await walk(`${currentSrc}/${relName}`, path5.join(currentDest, relName));
4301
+ await walk(`${currentSrc}/${relName}`, path7.join(currentDest, relName));
3736
4302
  } else if (entry.type === "file") {
3737
- const destFile = path5.join(currentDest, relName);
3738
- if (!overwrite && await fs11.pathExists(destFile)) {
4303
+ const destFile = path7.join(currentDest, relName);
4304
+ if (!overwrite && await fs13.pathExists(destFile)) {
3739
4305
  result.skipped.push(destFile);
3740
4306
  continue;
3741
4307
  }
@@ -3767,8 +4333,8 @@ var fetch_cmd_exports = {};
3767
4333
  __export(fetch_cmd_exports, {
3768
4334
  fetchCommand: () => fetchCommand
3769
4335
  });
3770
- import path6 from "path";
3771
- import fs12 from "fs-extra";
4336
+ import path8 from "path";
4337
+ import fs14 from "fs-extra";
3772
4338
  import chalk4 from "chalk";
3773
4339
  async function fetchCommand(cwd, recipeKey, options) {
3774
4340
  if (options.list) {
@@ -3806,7 +4372,7 @@ async function fetchCommand(cwd, recipeKey, options) {
3806
4372
  const installed = [];
3807
4373
  for (const key of keys) {
3808
4374
  const recipe = RECIPES[key];
3809
- const targetDir = path6.join(cwd, recipe.target);
4375
+ const targetDir = path8.join(cwd, recipe.target);
3810
4376
  console.log("");
3811
4377
  console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
3812
4378
  const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
@@ -3828,7 +4394,7 @@ async function fetchCommand(cwd, recipeKey, options) {
3828
4394
  if (result.written.length > 0) {
3829
4395
  success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
3830
4396
  for (const f of result.written) {
3831
- console.log(chalk4.gray(` ${path6.relative(cwd, f)}`));
4397
+ console.log(chalk4.gray(` ${path8.relative(cwd, f)}`));
3832
4398
  }
3833
4399
  }
3834
4400
  if (result.skipped.length > 0) {
@@ -3864,10 +4430,10 @@ async function fetchCommand(cwd, recipeKey, options) {
3864
4430
  }
3865
4431
  async function appendEnvKeys(cwd, recipe) {
3866
4432
  if (recipe.envKeys.length === 0) return;
3867
- const envPath = path6.join(cwd, ".env.local");
4433
+ const envPath = path8.join(cwd, ".env.local");
3868
4434
  let content = "";
3869
- if (await fs12.pathExists(envPath)) {
3870
- content = await fs12.readFile(envPath, "utf8");
4435
+ if (await fs14.pathExists(envPath)) {
4436
+ content = await fs14.readFile(envPath, "utf8");
3871
4437
  }
3872
4438
  const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
3873
4439
  if (missingKeys.length === 0) return;
@@ -3880,13 +4446,13 @@ async function appendEnvKeys(cwd, recipe) {
3880
4446
  if (ek.hint) lines.push(`# ${ek.hint}`);
3881
4447
  lines.push(`${ek.key}=${ek.default ?? ""}`);
3882
4448
  }
3883
- await fs12.ensureFile(envPath);
3884
- await fs12.appendFile(envPath, lines.join("\n") + "\n");
4449
+ await fs14.ensureFile(envPath);
4450
+ await fs14.appendFile(envPath, lines.join("\n") + "\n");
3885
4451
  }
3886
4452
  async function upsertRecipesSkill(cwd, installedKeys) {
3887
- const skillDir = path6.join(cwd, ".claude", "skills", "recipes");
3888
- await fs12.ensureDir(skillDir);
3889
- const skillPath = path6.join(skillDir, "SKILL.md");
4453
+ const skillDir = path8.join(cwd, ".claude", "skills", "recipes");
4454
+ await fs14.ensureDir(skillDir);
4455
+ const skillPath = path8.join(skillDir, "SKILL.md");
3890
4456
  const recipes = installedKeys.map((k) => RECIPES[k]);
3891
4457
  const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
3892
4458
  const body = [
@@ -3925,10 +4491,10 @@ async function upsertRecipesSkill(cwd, installedKeys) {
3925
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",
3926
4492
  ""
3927
4493
  ].join("\n");
3928
- await fs12.writeFile(skillPath, body);
4494
+ await fs14.writeFile(skillPath, body);
3929
4495
  }
3930
4496
  async function upsertClaudeMdBlock(cwd, installedKeys) {
3931
- const claudeMdPath = path6.join(cwd, "CLAUDE.md");
4497
+ const claudeMdPath = path8.join(cwd, "CLAUDE.md");
3932
4498
  const BLOCK_START = "<!-- pai:recipes:start -->";
3933
4499
  const BLOCK_END = "<!-- pai:recipes:end -->";
3934
4500
  const lines = [];
@@ -3948,8 +4514,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
3948
4514
  lines.push(BLOCK_END);
3949
4515
  const block = lines.join("\n");
3950
4516
  let content = "";
3951
- if (await fs12.pathExists(claudeMdPath)) {
3952
- content = await fs12.readFile(claudeMdPath, "utf8");
4517
+ if (await fs14.pathExists(claudeMdPath)) {
4518
+ content = await fs14.readFile(claudeMdPath, "utf8");
3953
4519
  }
3954
4520
  const startIdx = content.indexOf(BLOCK_START);
3955
4521
  const endIdx = content.indexOf(BLOCK_END);
@@ -3962,8 +4528,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
3962
4528
  if (content.length > 0) content += "\n";
3963
4529
  content += block + "\n";
3964
4530
  }
3965
- await fs12.ensureFile(claudeMdPath);
3966
- await fs12.writeFile(claudeMdPath, content);
4531
+ await fs14.ensureFile(claudeMdPath);
4532
+ await fs14.writeFile(claudeMdPath, content);
3967
4533
  }
3968
4534
  var init_fetch_cmd = __esm({
3969
4535
  "src/cli/commands/fetch.cmd.ts"() {
@@ -4197,7 +4763,7 @@ __export(analyzer_exports2, {
4197
4763
  analyzeRepository: () => analyzeRepository
4198
4764
  });
4199
4765
  import { join as join7 } from "path";
4200
- import fs13 from "fs-extra";
4766
+ import fs15 from "fs-extra";
4201
4767
  async function analyzeRepository(repoPath) {
4202
4768
  try {
4203
4769
  return await aiAnalysis(repoPath);
@@ -4258,14 +4824,14 @@ async function checkTestCoverage(repoPath) {
4258
4824
  ".nycrc"
4259
4825
  ];
4260
4826
  for (const f of testConfigs) {
4261
- const found = await fs13.pathExists(join7(repoPath, f));
4827
+ const found = await fs15.pathExists(join7(repoPath, f));
4262
4828
  findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4263
4829
  if (found) score += 20;
4264
4830
  }
4265
4831
  const testDirs = ["tests", "test", "__tests__", "spec"];
4266
4832
  let hasTestDir = false;
4267
4833
  for (const d of testDirs) {
4268
- if (await fs13.pathExists(join7(repoPath, d))) {
4834
+ if (await fs15.pathExists(join7(repoPath, d))) {
4269
4835
  findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
4270
4836
  hasTestDir = true;
4271
4837
  score += 30;
@@ -4288,8 +4854,8 @@ async function checkCiCd(repoPath) {
4288
4854
  { path: "Jenkinsfile", label: "Jenkins" },
4289
4855
  { path: ".circleci", label: "CircleCI" }
4290
4856
  ];
4291
- for (const { path: path8, label } of ciConfigs) {
4292
- const found = await fs13.pathExists(join7(repoPath, path8));
4857
+ for (const { path: path10, label } of ciConfigs) {
4858
+ const found = await fs15.pathExists(join7(repoPath, path10));
4293
4859
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4294
4860
  if (found) score += 40;
4295
4861
  }
@@ -4307,8 +4873,8 @@ async function checkHooks(repoPath) {
4307
4873
  { path: "commitlint.config.js", label: "commitlint" },
4308
4874
  { path: ".claude/settings.json", label: "Claude Code settings" }
4309
4875
  ];
4310
- for (const { path: path8, label } of hookConfigs) {
4311
- const found = await fs13.pathExists(join7(repoPath, path8));
4876
+ for (const { path: path10, label } of hookConfigs) {
4877
+ const found = await fs15.pathExists(join7(repoPath, path10));
4312
4878
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4313
4879
  if (found) score += 20;
4314
4880
  }
@@ -4325,8 +4891,8 @@ async function checkRepoStructure(repoPath) {
4325
4891
  { path: ".env.example", label: "\uD658\uACBD\uBCC0\uC218 \uC608\uC2DC" },
4326
4892
  { path: ".gitignore", label: ".gitignore" }
4327
4893
  ];
4328
- for (const { path: path8, label } of structureChecks) {
4329
- const found = await fs13.pathExists(join7(repoPath, path8));
4894
+ for (const { path: path10, label } of structureChecks) {
4895
+ const found = await fs15.pathExists(join7(repoPath, path10));
4330
4896
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4331
4897
  if (found) score += 25;
4332
4898
  }
@@ -4342,8 +4908,8 @@ async function checkDocumentation(repoPath) {
4342
4908
  { path: "docs", label: "docs/ \uB514\uB809\uD1A0\uB9AC", points: 25 },
4343
4909
  { path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
4344
4910
  ];
4345
- for (const { path: path8, label, points } of docChecks) {
4346
- const found = await fs13.pathExists(join7(repoPath, path8));
4911
+ for (const { path: path10, label, points } of docChecks) {
4912
+ const found = await fs15.pathExists(join7(repoPath, path10));
4347
4913
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4348
4914
  if (found) score += points;
4349
4915
  }
@@ -4361,8 +4927,8 @@ async function checkHarnessEngineering(repoPath) {
4361
4927
  { path: ".claude/commands", label: ".claude/commands/", points: 10 },
4362
4928
  { path: ".pai/config.json", label: "PAI config", points: 10 }
4363
4929
  ];
4364
- for (const { path: path8, label, points } of harnessChecks) {
4365
- const found = await fs13.pathExists(join7(repoPath, path8));
4930
+ for (const { path: path10, label, points } of harnessChecks) {
4931
+ const found = await fs15.pathExists(join7(repoPath, path10));
4366
4932
  findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
4367
4933
  if (found) score += points;
4368
4934
  }
@@ -4587,13 +5153,13 @@ function buildMarkdownReport(result) {
4587
5153
  return lines.join("\n") + "\n";
4588
5154
  }
4589
5155
  function buildDetailedReport(result, projectName) {
4590
- const now = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
5156
+ const now2 = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
4591
5157
  const mustCats = result.categories.filter((c2) => c2.tier === "must");
4592
5158
  const niceCats = result.categories.filter((c2) => c2.tier === "nice");
4593
5159
  const lines = [
4594
5160
  `# ${projectName} \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uBD84\uC11D \uB9AC\uD3EC\uD2B8`,
4595
5161
  "",
4596
- `> \uC2A4\uCE94 \uC77C\uC2DC: ${now}`,
5162
+ `> \uC2A4\uCE94 \uC77C\uC2DC: ${now2}`,
4597
5163
  `> \uBD84\uC11D \uB300\uC0C1: ${projectName}`,
4598
5164
  "",
4599
5165
  "---",
@@ -4747,54 +5313,54 @@ __export(shell_cd_exports, {
4747
5313
  });
4748
5314
  import { join as join8 } from "path";
4749
5315
  import { homedir as homedir2 } from "os";
4750
- import fs14 from "fs-extra";
5316
+ import fs16 from "fs-extra";
4751
5317
  async function requestCdAfter(targetDir) {
4752
- await fs14.ensureDir(PAI_DIR);
4753
- await fs14.writeFile(CD_FILE, targetDir);
5318
+ await fs16.ensureDir(PAI_DIR);
5319
+ await fs16.writeFile(CD_FILE, targetDir);
4754
5320
  }
4755
5321
  async function installShellHelper() {
4756
- await fs14.ensureDir(PAI_DIR);
5322
+ await fs16.ensureDir(PAI_DIR);
4757
5323
  if (isWindows) {
4758
5324
  return installPowerShellHelper();
4759
5325
  }
4760
5326
  return installBashHelper();
4761
5327
  }
4762
5328
  async function installBashHelper() {
4763
- await fs14.writeFile(HELPER_FILE_SH, BASH_HELPER);
5329
+ await fs16.writeFile(HELPER_FILE_SH, BASH_HELPER);
4764
5330
  const rcFile = getShellRcPath();
4765
5331
  const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
4766
- if (await fs14.pathExists(rcFile)) {
4767
- const content = await fs14.readFile(rcFile, "utf8");
5332
+ if (await fs16.pathExists(rcFile)) {
5333
+ const content = await fs16.readFile(rcFile, "utf8");
4768
5334
  if (content.includes("shell-helper.sh")) {
4769
5335
  return true;
4770
5336
  }
4771
- await fs14.appendFile(rcFile, `
5337
+ await fs16.appendFile(rcFile, `
4772
5338
  # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
4773
5339
  ${sourceLine}
4774
5340
  `);
4775
5341
  return false;
4776
5342
  }
4777
- await fs14.writeFile(rcFile, `${sourceLine}
5343
+ await fs16.writeFile(rcFile, `${sourceLine}
4778
5344
  `);
4779
5345
  return false;
4780
5346
  }
4781
5347
  async function installPowerShellHelper() {
4782
- await fs14.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
5348
+ await fs16.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
4783
5349
  const rcFile = getShellRcPath();
4784
5350
  const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
4785
- await fs14.ensureDir(join8(rcFile, ".."));
4786
- if (await fs14.pathExists(rcFile)) {
4787
- const content = await fs14.readFile(rcFile, "utf8");
5351
+ await fs16.ensureDir(join8(rcFile, ".."));
5352
+ if (await fs16.pathExists(rcFile)) {
5353
+ const content = await fs16.readFile(rcFile, "utf8");
4788
5354
  if (content.includes("shell-helper.ps1")) {
4789
5355
  return true;
4790
5356
  }
4791
- await fs14.appendFile(rcFile, `
5357
+ await fs16.appendFile(rcFile, `
4792
5358
  # PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
4793
5359
  ${sourceLine}
4794
5360
  `);
4795
5361
  return false;
4796
5362
  }
4797
- await fs14.writeFile(rcFile, `${sourceLine}
5363
+ await fs16.writeFile(rcFile, `${sourceLine}
4798
5364
  `);
4799
5365
  return false;
4800
5366
  }
@@ -4856,11 +5422,11 @@ __export(claude_settings_exports, {
4856
5422
  isAlreadyEnabled: () => isAlreadyEnabled,
4857
5423
  mergeOmcIntoSettings: () => mergeOmcIntoSettings
4858
5424
  });
4859
- import os2 from "os";
4860
- import path7 from "path";
4861
- import fs15 from "fs-extra";
4862
- function getClaudeSettingsPath(homeDir = os2.homedir()) {
4863
- return path7.join(homeDir, ".claude", "settings.json");
5425
+ import os3 from "os";
5426
+ import path9 from "path";
5427
+ import fs17 from "fs-extra";
5428
+ function getClaudeSettingsPath(homeDir = os3.homedir()) {
5429
+ return path9.join(homeDir, ".claude", "settings.json");
4864
5430
  }
4865
5431
  function parseJsonWithBom(raw) {
4866
5432
  const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
@@ -4875,13 +5441,13 @@ async function enableOmcPlugin(options = {}) {
4875
5441
  const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
4876
5442
  const wantBackup = options.backup ?? true;
4877
5443
  const settingsPath = getClaudeSettingsPath();
4878
- await fs15.ensureDir(path7.dirname(settingsPath));
4879
- if (!await fs15.pathExists(settingsPath)) {
5444
+ await fs17.ensureDir(path9.dirname(settingsPath));
5445
+ if (!await fs17.pathExists(settingsPath)) {
4880
5446
  const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
4881
- await fs15.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
5447
+ await fs17.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
4882
5448
  return { action: "created", settingsPath };
4883
5449
  }
4884
- const raw = await fs15.readFile(settingsPath, "utf8");
5450
+ const raw = await fs17.readFile(settingsPath, "utf8");
4885
5451
  let parsed;
4886
5452
  try {
4887
5453
  const value = parseJsonWithBom(raw);
@@ -4891,7 +5457,7 @@ async function enableOmcPlugin(options = {}) {
4891
5457
  parsed = value;
4892
5458
  } catch (err) {
4893
5459
  const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
4894
- await fs15.copy(settingsPath, backupPath2);
5460
+ await fs17.copy(settingsPath, backupPath2);
4895
5461
  throw new ClaudeSettingsError(
4896
5462
  `settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
4897
5463
  backupPath2
@@ -4903,10 +5469,10 @@ async function enableOmcPlugin(options = {}) {
4903
5469
  let backupPath;
4904
5470
  if (wantBackup) {
4905
5471
  backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
4906
- await fs15.copy(settingsPath, backupPath);
5472
+ await fs17.copy(settingsPath, backupPath);
4907
5473
  }
4908
5474
  const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
4909
- await fs15.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
5475
+ await fs17.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
4910
5476
  return { action: "added", settingsPath, backupPath };
4911
5477
  }
4912
5478
  function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
@@ -5005,13 +5571,13 @@ function getCachedResult(repoPath) {
5005
5571
  function setCachedResult(repoPath, llmOutput) {
5006
5572
  const store = loadCache(repoPath);
5007
5573
  const repoHash = computeRepoHash(repoPath);
5008
- const now = Date.now();
5574
+ const now2 = Date.now();
5009
5575
  for (const [key, entry] of Object.entries(store.entries)) {
5010
- if (now - entry.timestamp > CACHE_TTL_MS) {
5576
+ if (now2 - entry.timestamp > CACHE_TTL_MS) {
5011
5577
  delete store.entries[key];
5012
5578
  }
5013
5579
  }
5014
- store.entries[repoHash] = { repoPath, repoHash, timestamp: now, llmOutput };
5580
+ store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
5015
5581
  saveCache(repoPath, store);
5016
5582
  }
5017
5583
  var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
@@ -5042,59 +5608,270 @@ var init_cache = __esm({
5042
5608
  }
5043
5609
  });
5044
5610
 
5045
- // src/cli/commands/evaluate.cmd.ts
5046
- var evaluate_cmd_exports = {};
5047
- __export(evaluate_cmd_exports, {
5048
- evaluateCommand: () => evaluateCommand
5049
- });
5050
- import { join as join10, basename } from "path";
5051
- import fs16 from "fs-extra";
5052
- async function evaluateCommand(cwd, options) {
5053
- const useCache = options.cache !== false;
5054
- let llmOutput = useCache ? getCachedResult(cwd) : null;
5055
- if (llmOutput) {
5056
- info("\uCE90\uC2DC\uB41C \uACB0\uACFC \uC0AC\uC6A9 (24\uC2DC\uAC04 \uC774\uB0B4)");
5057
- } else {
5058
- llmOutput = await analyzeRepository(cwd);
5059
- if (useCache) {
5060
- setCachedResult(cwd, llmOutput);
5611
+ // src/stages/validation/runner.ts
5612
+ import { join as join10 } from "path";
5613
+ import fs18 from "fs-extra";
5614
+ async function runTests(cwd) {
5615
+ const start = Date.now();
5616
+ const gstackPath = join10(cwd, ".pai", "gstack.json");
5617
+ let runner = "npm test";
5618
+ if (await fs18.pathExists(gstackPath)) {
5619
+ try {
5620
+ const config = await fs18.readJson(gstackPath);
5621
+ if (config.testRunner === "vitest") runner = "npx vitest run";
5622
+ else if (config.testRunner === "jest") runner = "npx jest";
5623
+ else if (config.testRunner === "mocha") runner = "npx mocha";
5624
+ } catch {
5061
5625
  }
5062
5626
  }
5063
- const result = computeResult(llmOutput);
5064
- printReport(result);
5065
- if (options.verbose) {
5066
- printVerboseFindings(result);
5067
- }
5068
- const config = await loadConfig(cwd);
5069
- const projectName = config?.projectName ?? basename(cwd);
5070
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5071
- const reportDir = join10(cwd, "docs", "p-reports");
5072
- const reportPath = join10(reportDir, `${today}.md`);
5073
- await fs16.ensureDir(reportDir);
5074
- const detailedReport = buildDetailedReport(result, projectName);
5075
- await fs16.writeFile(reportPath, detailedReport, "utf8");
5076
- console.log("");
5077
- success(`\uB9AC\uD3EC\uD2B8 \uC800\uC7A5: docs/p-reports/${today}.md`);
5078
- console.log("");
5079
- console.log(detailedReport);
5080
- if (options.output) {
5081
- await fs16.writeFile(options.output, detailedReport, "utf8");
5082
- success(`\uCD94\uAC00 \uC800\uC7A5: ${options.output}`);
5627
+ const pkgPath = join10(cwd, "package.json");
5628
+ if (await fs18.pathExists(pkgPath)) {
5629
+ try {
5630
+ const pkg5 = await fs18.readJson(pkgPath);
5631
+ if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
5632
+ return {
5633
+ runner,
5634
+ passed: false,
5635
+ 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.",
5636
+ duration: Date.now() - start
5637
+ };
5638
+ }
5639
+ } catch {
5640
+ }
5083
5641
  }
5084
- if (options.failUnder && result.totalScore < options.failUnder) {
5085
- error(`\uC810\uC218 ${result.totalScore}\uC774 \uCD5C\uC18C \uAE30\uC900 ${options.failUnder}\uC5D0 \uBBF8\uB2EC\uD569\uB2C8\uB2E4.`);
5086
- process.exitCode = 1;
5642
+ try {
5643
+ const { execa } = await import("execa");
5644
+ const { stdout, stderr } = await execa("npm", ["test"], {
5645
+ cwd,
5646
+ timeout: 12e4,
5647
+ env: { ...process.env, CI: "true" }
5648
+ });
5649
+ return {
5650
+ runner,
5651
+ passed: true,
5652
+ output: stdout || stderr,
5653
+ duration: Date.now() - start
5654
+ };
5655
+ } catch (err) {
5656
+ const output = err instanceof Error ? err.message : String(err);
5657
+ return {
5658
+ runner,
5659
+ passed: false,
5660
+ output,
5661
+ duration: Date.now() - start
5662
+ };
5087
5663
  }
5088
5664
  }
5089
- var init_evaluate_cmd = __esm({
5090
- "src/cli/commands/evaluate.cmd.ts"() {
5665
+ var init_runner = __esm({
5666
+ "src/stages/validation/runner.ts"() {
5091
5667
  "use strict";
5092
- init_ui();
5093
- init_analyzer2();
5094
- init_scorer();
5668
+ }
5669
+ });
5670
+
5671
+ // src/stages/validation/harness.ts
5672
+ import { join as join11 } from "path";
5673
+ import fs19 from "fs-extra";
5674
+ async function runHarnessCheck(cwd) {
5675
+ const harnessPath = join11(cwd, ".pai", "harness.json");
5676
+ if (!await fs19.pathExists(harnessPath)) {
5677
+ return { enabled: false, specFile: null, rules: [], checks: [] };
5678
+ }
5679
+ let config;
5680
+ try {
5681
+ config = await fs19.readJson(harnessPath);
5682
+ } catch {
5683
+ return { enabled: false, specFile: null, rules: [], checks: [] };
5684
+ }
5685
+ const specFile = config.specFile ?? "docs/openspec.md";
5686
+ const rules = config.rules ?? [];
5687
+ const checks = [];
5688
+ if (rules.includes("spec-implementation-match")) {
5689
+ const specExists = await fs19.pathExists(join11(cwd, specFile));
5690
+ const srcExists = await fs19.pathExists(join11(cwd, "src"));
5691
+ checks.push({
5692
+ rule: "spec-implementation-match",
5693
+ passed: specExists && srcExists,
5694
+ 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()
5695
+ });
5696
+ }
5697
+ if (rules.includes("api-contract-test")) {
5698
+ const testDir = await fs19.pathExists(join11(cwd, "tests"));
5699
+ const testDir2 = await fs19.pathExists(join11(cwd, "test"));
5700
+ checks.push({
5701
+ rule: "api-contract-test",
5702
+ passed: testDir || testDir2,
5703
+ 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"
5704
+ });
5705
+ }
5706
+ return { enabled: true, specFile, rules, checks };
5707
+ }
5708
+ var init_harness = __esm({
5709
+ "src/stages/validation/harness.ts"() {
5710
+ "use strict";
5711
+ }
5712
+ });
5713
+
5714
+ // src/cli/commands/validate.cmd.ts
5715
+ var validate_cmd_exports = {};
5716
+ __export(validate_cmd_exports, {
5717
+ handleRobocoCliError: () => handleRobocoError,
5718
+ validateCommand: () => validateCommand
5719
+ });
5720
+ async function validateCommand(cwd, options = {}) {
5721
+ try {
5722
+ await withRoboco(
5723
+ cwd,
5724
+ "validation",
5725
+ "pai test",
5726
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
5727
+ async () => {
5728
+ section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
5729
+ const testResult = await runTests(cwd);
5730
+ if (testResult.passed) {
5731
+ success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
5732
+ } else {
5733
+ error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
5734
+ info(testResult.output.slice(0, 300));
5735
+ }
5736
+ section("\uD558\uB124\uC2A4 \uAC80\uC99D");
5737
+ const harness = await runHarnessCheck(cwd);
5738
+ if (!harness.enabled) {
5739
+ info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
5740
+ info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
5741
+ } else {
5742
+ for (const check of harness.checks) {
5743
+ if (check.passed) {
5744
+ success(`${check.rule}: ${check.detail}`);
5745
+ } else {
5746
+ warn(`${check.rule}: ${check.detail}`);
5747
+ }
5748
+ }
5749
+ }
5750
+ const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
5751
+ console.log("");
5752
+ if (allPassed) {
5753
+ success("\uAC80\uC99D \uD1B5\uACFC!");
5754
+ } else if (!testResult.passed) {
5755
+ error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
5756
+ throw new Error("tests-failed");
5757
+ } else {
5758
+ warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
5759
+ }
5760
+ }
5761
+ );
5762
+ await syncHandoff(cwd).catch(() => {
5763
+ });
5764
+ } catch (err) {
5765
+ handleRobocoError(err);
5766
+ }
5767
+ }
5768
+ function handleRobocoError(err) {
5769
+ if (err instanceof RobocoLockError) {
5770
+ error(err.message);
5771
+ process.exitCode = 1;
5772
+ return;
5773
+ }
5774
+ if (err instanceof GateFailedError) {
5775
+ error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
5776
+ for (const v of err.result.violations) {
5777
+ warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
5778
+ }
5779
+ hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
5780
+ process.exitCode = 1;
5781
+ return;
5782
+ }
5783
+ if (err instanceof Error) {
5784
+ if (err.message === "tests-failed") {
5785
+ process.exitCode = 1;
5786
+ return;
5787
+ }
5788
+ error(err.message);
5789
+ process.exitCode = 1;
5790
+ return;
5791
+ }
5792
+ throw err;
5793
+ }
5794
+ var init_validate_cmd = __esm({
5795
+ "src/cli/commands/validate.cmd.ts"() {
5796
+ "use strict";
5797
+ init_ui();
5798
+ init_runner();
5799
+ init_harness();
5800
+ init_roboco();
5801
+ init_gates();
5802
+ }
5803
+ });
5804
+
5805
+ // src/cli/commands/evaluate.cmd.ts
5806
+ var evaluate_cmd_exports = {};
5807
+ __export(evaluate_cmd_exports, {
5808
+ evaluateCommand: () => evaluateCommand
5809
+ });
5810
+ import { join as join12, basename } from "path";
5811
+ import fs20 from "fs-extra";
5812
+ async function evaluateCommand(cwd, options) {
5813
+ const useCache = options.cache !== false;
5814
+ try {
5815
+ await withRoboco(
5816
+ cwd,
5817
+ "evaluation",
5818
+ "pai grade",
5819
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.evaluation },
5820
+ async () => {
5821
+ let llmOutput = useCache ? getCachedResult(cwd) : null;
5822
+ if (llmOutput) {
5823
+ info("\uCE90\uC2DC\uB41C \uACB0\uACFC \uC0AC\uC6A9 (24\uC2DC\uAC04 \uC774\uB0B4)");
5824
+ } else {
5825
+ llmOutput = await analyzeRepository(cwd);
5826
+ if (useCache) setCachedResult(cwd, llmOutput);
5827
+ }
5828
+ const result = computeResult(llmOutput);
5829
+ printReport(result);
5830
+ if (options.verbose) printVerboseFindings(result);
5831
+ const config = await loadConfig(cwd);
5832
+ const projectName = config?.projectName ?? basename(cwd);
5833
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5834
+ const reportDir = join12(cwd, "docs", "p-reports");
5835
+ const reportPath = join12(reportDir, `${today}.md`);
5836
+ await fs20.ensureDir(reportDir);
5837
+ const detailedReport = buildDetailedReport(result, projectName);
5838
+ await fs20.writeFile(reportPath, detailedReport, "utf8");
5839
+ console.log("");
5840
+ success(`\uB9AC\uD3EC\uD2B8 \uC800\uC7A5: docs/p-reports/${today}.md`);
5841
+ console.log("");
5842
+ console.log(detailedReport);
5843
+ if (options.output) {
5844
+ await fs20.writeFile(options.output, detailedReport, "utf8");
5845
+ success(`\uCD94\uAC00 \uC800\uC7A5: ${options.output}`);
5846
+ }
5847
+ if (options.failUnder && result.totalScore < options.failUnder) {
5848
+ error(`\uC810\uC218 ${result.totalScore}\uC774 \uCD5C\uC18C \uAE30\uC900 ${options.failUnder}\uC5D0 \uBBF8\uB2EC\uD569\uB2C8\uB2E4.`);
5849
+ throw new Error("score-below-threshold");
5850
+ }
5851
+ }
5852
+ );
5853
+ await syncHandoff(cwd).catch(() => {
5854
+ });
5855
+ } catch (err) {
5856
+ if (err instanceof Error && err.message === "score-below-threshold") {
5857
+ process.exitCode = 1;
5858
+ return;
5859
+ }
5860
+ handleRobocoError(err);
5861
+ }
5862
+ }
5863
+ var init_evaluate_cmd = __esm({
5864
+ "src/cli/commands/evaluate.cmd.ts"() {
5865
+ "use strict";
5866
+ init_ui();
5867
+ init_analyzer2();
5868
+ init_scorer();
5095
5869
  init_cache();
5096
5870
  init_reporter();
5097
5871
  init_config();
5872
+ init_roboco();
5873
+ init_gates();
5874
+ init_validate_cmd();
5098
5875
  }
5099
5876
  });
5100
5877
 
@@ -5105,7 +5882,22 @@ __export(env_cmd_exports, {
5105
5882
  envSetupCommand: () => envSetupCommand,
5106
5883
  envStatusCommand: () => envStatusCommand
5107
5884
  });
5108
- async function envSetupCommand(cwd) {
5885
+ async function envSetupCommand(cwd, options = {}) {
5886
+ try {
5887
+ await withRoboco(
5888
+ cwd,
5889
+ "environment",
5890
+ "pai add",
5891
+ { skipGates: options.skipGates, force: options.force },
5892
+ async () => envSetupCommandInner(cwd)
5893
+ );
5894
+ await syncHandoff(cwd).catch(() => {
5895
+ });
5896
+ } catch (err) {
5897
+ handleRobocoError(err);
5898
+ }
5899
+ }
5900
+ async function envSetupCommandInner(cwd) {
5109
5901
  const { default: inquirer } = await import("inquirer");
5110
5902
  const { basename: basename5 } = await import("path");
5111
5903
  section("\uD50C\uB7EC\uADF8\uC778 \uCD94\uAC00 \xB7 \uBAA8\uB4DC \uC2B9\uACA9");
@@ -5231,6 +6023,54 @@ async function envStatusCommand(cwd) {
5231
6023
  hint(`\uC2B9\uACA9 \uACBD\uB85C: ${state.projectMode} \u2192 ${next} (pai add \uC5D0\uC11C \uC2B9\uACA9 \uAC00\uB2A5)`);
5232
6024
  }
5233
6025
  }
6026
+ const robo = await loadRobocoState(cwd);
6027
+ console.log("");
6028
+ console.log(colors.accent(" \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 (roboco)"));
6029
+ 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"));
6030
+ const stages = ["environment", "design", "execution", "validation", "evaluation"];
6031
+ for (const s of stages) {
6032
+ const latest = [...robo.stageHistory].reverse().find((h) => h.stage === s);
6033
+ const gate = robo.gates[`${s}.entry`];
6034
+ let mark = "\u25CB";
6035
+ let label = "\uB300\uAE30";
6036
+ if (latest?.status === "done") {
6037
+ mark = "\u2713";
6038
+ label = "\uC644\uB8CC";
6039
+ } else if (latest?.status === "in_progress") {
6040
+ mark = "\u25B8";
6041
+ label = "\uC9C4\uD589 \uC911";
6042
+ } else if (latest?.status === "failed") {
6043
+ mark = "\u2717";
6044
+ label = `\uC2E4\uD328 (${latest.reason ?? "\uC6D0\uC778 \uBD88\uBA85"})`;
6045
+ } else if (gate && !gate.passed) {
6046
+ mark = "\u25CB";
6047
+ label = `\uB300\uAE30 (\uAC8C\uC774\uD2B8 ${gate.violations.length}\uAC74 \uBBF8\uD1B5\uACFC)`;
6048
+ }
6049
+ console.log(` ${mark} ${s.padEnd(12)} ${colors.dim(label)}`);
6050
+ }
6051
+ const failedGates = Object.values(robo.gates).filter((g) => !g.passed);
6052
+ if (failedGates.length > 0) {
6053
+ console.log("");
6054
+ console.log(colors.accent(" \uBBF8\uD1B5\uACFC \uAC8C\uC774\uD2B8"));
6055
+ for (const g of failedGates) {
6056
+ console.log(` \u2717 ${g.name}`);
6057
+ for (const v of g.violations) {
6058
+ console.log(colors.dim(` \xB7 ${v.message}`));
6059
+ }
6060
+ }
6061
+ }
6062
+ console.log("");
6063
+ if (robo.lock) {
6064
+ const stale = isLockStale(robo.lock);
6065
+ const mins = Math.round((Date.now() - new Date(robo.lock.acquiredAt).getTime()) / 6e4);
6066
+ if (stale) {
6067
+ warn(`\uB77D stale (${robo.lock.command}, PID ${robo.lock.pid}) \u2014 \uB2E4\uC74C \uBA85\uB839 \uC2DC \uC790\uB3D9 \uD574\uC81C\uB428`);
6068
+ } else {
6069
+ warn(`\uC9C4\uD589 \uC911: ${robo.lock.command} (PID ${robo.lock.pid}, ${mins}\uBD84 \uC804)`);
6070
+ }
6071
+ } else {
6072
+ success("\uB77D \uC5C6\uC74C (\uC5B4\uB290 \uBA85\uB839\uC774\uB4E0 \uC2E4\uD589 \uAC00\uB2A5)");
6073
+ }
5234
6074
  }
5235
6075
  var init_env_cmd = __esm({
5236
6076
  "src/cli/commands/env.cmd.ts"() {
@@ -5242,6 +6082,8 @@ var init_env_cmd = __esm({
5242
6082
  init_doctor();
5243
6083
  init_analyzer();
5244
6084
  init_logger();
6085
+ init_roboco();
6086
+ init_validate_cmd();
5245
6087
  }
5246
6088
  });
5247
6089
 
@@ -5250,8 +6092,8 @@ var init_cmd_exports = {};
5250
6092
  __export(init_cmd_exports, {
5251
6093
  initCommand: () => initCommand
5252
6094
  });
5253
- import { join as join11, basename as basename2 } from "path";
5254
- import fs17 from "fs-extra";
6095
+ import { join as join13, basename as basename2 } from "path";
6096
+ import fs21 from "fs-extra";
5255
6097
  async function initCommand(cwd, nameArg) {
5256
6098
  printWelcomeBanner();
5257
6099
  const { isWindows: isWindows2, diagnoseWindowsEnv: diagnoseWindowsEnv2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
@@ -5309,11 +6151,11 @@ async function initCommand(cwd, nameArg) {
5309
6151
  const evalResult = computeResult2(llmOutput);
5310
6152
  printReport2(evalResult);
5311
6153
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5312
- const reportDir = join11(cwd, "docs", "p-reports");
5313
- await fs17.ensureDir(reportDir);
6154
+ const reportDir = join13(cwd, "docs", "p-reports");
6155
+ await fs21.ensureDir(reportDir);
5314
6156
  const legacyName = basename2(cwd);
5315
6157
  const detailedReport = buildDetailedReport3(evalResult, legacyName);
5316
- await fs17.writeFile(join11(reportDir, `${today}.md`), detailedReport, "utf8");
6158
+ await fs21.writeFile(join13(reportDir, `${today}.md`), detailedReport, "utf8");
5317
6159
  console.log("");
5318
6160
  success(`\uB9AC\uD3EC\uD2B8 \uC800\uC7A5: docs/p-reports/${today}.md`);
5319
6161
  } catch {
@@ -5358,8 +6200,8 @@ async function initCommand(cwd, nameArg) {
5358
6200
  }]);
5359
6201
  projectName = answer.name.trim();
5360
6202
  }
5361
- const projectDir = join11(cwd, projectName);
5362
- if (await fs17.pathExists(projectDir)) {
6203
+ const projectDir = join13(cwd, projectName);
6204
+ if (await fs21.pathExists(projectDir)) {
5363
6205
  const existingConfig = await loadConfig(projectDir);
5364
6206
  if (existingConfig) {
5365
6207
  console.log("");
@@ -5372,7 +6214,7 @@ async function initCommand(cwd, nameArg) {
5372
6214
  warn(`${projectName}/ \uD3F4\uB354\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.`);
5373
6215
  hint("\uC774 \uD3F4\uB354\uC5D0 PAI\uB97C \uC124\uCE58\uD569\uB2C8\uB2E4.");
5374
6216
  } else {
5375
- await fs17.ensureDir(projectDir);
6217
+ await fs21.ensureDir(projectDir);
5376
6218
  success(`${projectName}/ \uD3F4\uB354 \uC0DD\uC131`);
5377
6219
  }
5378
6220
  await setupInDirectory(projectDir, projectName);
@@ -5457,7 +6299,7 @@ async function showCompletion(projectName, projectDir, extraTools, isCurrentDir)
5457
6299
  ...extraTools.includes("omc") ? [{ label: "OMC \uB7F0\uD0C0\uC784", path: ".omc" }] : []
5458
6300
  ];
5459
6301
  for (const check of checks) {
5460
- const exists = await fs17.pathExists(join11(projectDir, check.path));
6302
+ const exists = await fs21.pathExists(join13(projectDir, check.path));
5461
6303
  console.log(` ${exists ? colors.success("\u2713") : colors.err("\u2717")} ${check.label.padEnd(16)} ${colors.dim(check.path)}`);
5462
6304
  }
5463
6305
  console.log("");
@@ -5482,10 +6324,10 @@ async function showCompletion(projectName, projectDir, extraTools, isCurrentDir)
5482
6324
  printReport2(evalResult);
5483
6325
  await sleep2(500);
5484
6326
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5485
- const reportDir = join11(projectDir, "docs", "p-reports");
5486
- await fs17.ensureDir(reportDir);
6327
+ const reportDir = join13(projectDir, "docs", "p-reports");
6328
+ await fs21.ensureDir(reportDir);
5487
6329
  const detailedReport = buildDetailedReport3(evalResult, projectName);
5488
- await fs17.writeFile(join11(reportDir, `${today}.md`), detailedReport, "utf8");
6330
+ await fs21.writeFile(join13(reportDir, `${today}.md`), detailedReport, "utf8");
5489
6331
  await sleep2(500);
5490
6332
  console.log("");
5491
6333
  success(`\uB9AC\uD3EC\uD2B8 \uC800\uC7A5: docs/p-reports/${today}.md`);
@@ -5536,7 +6378,7 @@ async function showCompletion(projectName, projectDir, extraTools, isCurrentDir)
5536
6378
  const shellRc = getShellRcPath2();
5537
6379
  let yoloAlreadyAliased = false;
5538
6380
  try {
5539
- const rcContent = await fs17.readFile(shellRc, "utf8");
6381
+ const rcContent = await fs21.readFile(shellRc, "utf8");
5540
6382
  yoloAlreadyAliased = checkYolo(rcContent);
5541
6383
  } catch {
5542
6384
  }
@@ -5556,10 +6398,10 @@ async function showCompletion(projectName, projectDir, extraTools, isCurrentDir)
5556
6398
  try {
5557
6399
  const { getYoloAliasLine: getYoloAliasLine2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
5558
6400
  const aliasLine = getYoloAliasLine2();
5559
- const rcContent = await fs17.readFile(shellRc, "utf8").catch(() => "");
6401
+ const rcContent = await fs21.readFile(shellRc, "utf8").catch(() => "");
5560
6402
  if (!rcContent.includes("claude-yolo")) {
5561
- await fs17.ensureDir(join11(shellRc, ".."));
5562
- await fs17.appendFile(shellRc, `
6403
+ await fs21.ensureDir(join13(shellRc, ".."));
6404
+ await fs21.appendFile(shellRc, `
5563
6405
  # PAI \u2014 claude-YOLO mode
5564
6406
  ${aliasLine}
5565
6407
  `);
@@ -5752,9 +6594,9 @@ async function installOrchestratorOnly(projectDir, projectName) {
5752
6594
  const evalResult = computeResult2(llmOutput);
5753
6595
  printReport2(evalResult);
5754
6596
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5755
- const reportDir = join11(projectDir, "docs", "p-reports");
5756
- await fs17.ensureDir(reportDir);
5757
- await fs17.writeFile(join11(reportDir, `${today}.md`), buildDetailedReport3(evalResult, projectName), "utf8");
6597
+ const reportDir = join13(projectDir, "docs", "p-reports");
6598
+ await fs21.ensureDir(reportDir);
6599
+ await fs21.writeFile(join13(reportDir, `${today}.md`), buildDetailedReport3(evalResult, projectName), "utf8");
5758
6600
  console.log("");
5759
6601
  hint(`\uC0C1\uC138 \uB9AC\uD3EC\uD2B8: docs/p-reports/${today}.md`);
5760
6602
  } catch {
@@ -5804,7 +6646,7 @@ async function detectLegacyProject(cwd) {
5804
6646
  ".gitignore"
5805
6647
  ];
5806
6648
  for (const signal of signals) {
5807
- if (await fs17.pathExists(join11(cwd, signal))) return true;
6649
+ if (await fs21.pathExists(join13(cwd, signal))) return true;
5808
6650
  }
5809
6651
  return false;
5810
6652
  }
@@ -5895,17 +6737,17 @@ var init_help_cmd = __esm({
5895
6737
  });
5896
6738
 
5897
6739
  // src/stages/design/openspec.ts
5898
- import { join as join12 } from "path";
5899
- import fs18 from "fs-extra";
6740
+ import { join as join14 } from "path";
6741
+ import fs22 from "fs-extra";
5900
6742
  async function initOpenSpec(cwd, projectName) {
5901
- const docsDir = join12(cwd, "docs");
5902
- await fs18.ensureDir(docsDir);
5903
- const openspecPath = join12(docsDir, "openspec.md");
5904
- if (await fs18.pathExists(openspecPath)) {
6743
+ const docsDir = join14(cwd, "docs");
6744
+ await fs22.ensureDir(docsDir);
6745
+ const openspecPath = join14(docsDir, "openspec.md");
6746
+ if (await fs22.pathExists(openspecPath)) {
5905
6747
  info("docs/openspec.md \uC774\uBBF8 \uC874\uC7AC \u2014 \uAC74\uB108\uB700");
5906
6748
  return;
5907
6749
  }
5908
- await fs18.writeFile(openspecPath, [
6750
+ await fs22.writeFile(openspecPath, [
5909
6751
  `# OpenSpec \u2014 ${projectName}`,
5910
6752
  "",
5911
6753
  "## 1. \uBAA9\uC801 (Purpose)",
@@ -5933,13 +6775,13 @@ async function initOpenSpec(cwd, projectName) {
5933
6775
  }
5934
6776
  async function validateOpenSpec(cwd) {
5935
6777
  const candidates = [
5936
- join12(cwd, "docs", "openspec.md"),
5937
- join12(cwd, "openspec.md"),
5938
- join12(cwd, ".pai", "openspec.md")
6778
+ join14(cwd, "docs", "openspec.md"),
6779
+ join14(cwd, "openspec.md"),
6780
+ join14(cwd, ".pai", "openspec.md")
5939
6781
  ];
5940
6782
  let specPath = null;
5941
6783
  for (const p of candidates) {
5942
- if (await fs18.pathExists(p)) {
6784
+ if (await fs22.pathExists(p)) {
5943
6785
  specPath = p;
5944
6786
  break;
5945
6787
  }
@@ -5953,7 +6795,7 @@ async function validateOpenSpec(cwd) {
5953
6795
  warnings: ["openspec.md \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai design init` \uC744 \uC2E4\uD589\uD558\uC138\uC694."]
5954
6796
  };
5955
6797
  }
5956
- const content = await fs18.readFile(specPath, "utf8");
6798
+ const content = await fs22.readFile(specPath, "utf8");
5957
6799
  const missing = [];
5958
6800
  let filled = 0;
5959
6801
  for (const section2 of REQUIRED_SECTIONS) {
@@ -6001,17 +6843,17 @@ var init_openspec = __esm({
6001
6843
  });
6002
6844
 
6003
6845
  // src/stages/design/omc.ts
6004
- import { join as join13 } from "path";
6005
- import fs19 from "fs-extra";
6846
+ import { join as join15 } from "path";
6847
+ import fs23 from "fs-extra";
6006
6848
  async function initOMC(cwd, projectName) {
6007
- const paiDir = join13(cwd, ".pai");
6008
- await fs19.ensureDir(paiDir);
6009
- const omcPath = join13(paiDir, "omc.md");
6010
- if (await fs19.pathExists(omcPath)) {
6849
+ const paiDir = join15(cwd, ".pai");
6850
+ await fs23.ensureDir(paiDir);
6851
+ const omcPath = join15(paiDir, "omc.md");
6852
+ if (await fs23.pathExists(omcPath)) {
6011
6853
  info(".pai/omc.md \uC774\uBBF8 \uC874\uC7AC \u2014 \uAC74\uB108\uB700");
6012
6854
  return;
6013
6855
  }
6014
- await fs19.writeFile(omcPath, [
6856
+ await fs23.writeFile(omcPath, [
6015
6857
  `# OMC \u2014 Object Model Context (${projectName})`,
6016
6858
  "",
6017
6859
  "> AI\uAC00 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB3C4\uBA54\uC778\uC744 \uC774\uD574\uD558\uAE30 \uC704\uD55C \uD575\uC2EC \uAC1D\uCCB4 \uBAA8\uB378",
@@ -6051,195 +6893,71 @@ __export(design_cmd_exports, {
6051
6893
  designValidateCommand: () => designValidateCommand
6052
6894
  });
6053
6895
  import { basename as basename3 } from "path";
6054
- async function designInitCommand(cwd) {
6055
- section("\uC124\uACC4 \uD15C\uD50C\uB9BF \uC0DD\uC131");
6056
- const config = await loadConfig(cwd);
6057
- const projectName = config?.projectName ?? basename3(cwd);
6058
- await initOpenSpec(cwd, projectName);
6059
- await initOMC(cwd, projectName);
6060
- info("");
6061
- info("\uB2E4\uC74C \uB2E8\uACC4: docs/openspec.md\uB97C \uC5F4\uC5B4 PRD\uB97C \uC791\uC131\uD558\uC138\uC694.");
6062
- }
6063
- async function designValidateCommand(cwd) {
6064
- section("PRD \uC644\uC131\uB3C4 \uAC80\uC99D");
6065
- const result = await validateOpenSpec(cwd);
6066
- info(`\uC139\uC158 \uC644\uC131\uB3C4: ${result.filledSections}/${result.totalSections}`);
6067
- if (result.missing.length > 0) {
6068
- console.log("");
6069
- warn("\uBBF8\uC791\uC131 \uC139\uC158:");
6070
- for (const m of result.missing) {
6071
- warn(` - ${m}`);
6072
- }
6073
- }
6074
- for (const w of result.warnings) {
6075
- warn(w);
6076
- }
6077
- if (result.complete) {
6078
- console.log("");
6079
- success("PRD \uC644\uC131\uB3C4 \uAC80\uC99D \uD1B5\uACFC!");
6080
- } else {
6081
- console.log("");
6082
- info("docs/openspec.md\uB97C \uC5F4\uC5B4 \uBBF8\uC791\uC131 \uC139\uC158\uC744 \uCC44\uC6CC\uC8FC\uC138\uC694.");
6083
- }
6084
- }
6085
- var init_design_cmd = __esm({
6086
- "src/cli/commands/design.cmd.ts"() {
6087
- "use strict";
6088
- init_ui();
6089
- init_openspec();
6090
- init_omc();
6091
- init_config();
6092
- }
6093
- });
6094
-
6095
- // src/stages/validation/runner.ts
6096
- import { join as join14 } from "path";
6097
- import fs20 from "fs-extra";
6098
- async function runTests(cwd) {
6099
- const start = Date.now();
6100
- const gstackPath = join14(cwd, ".pai", "gstack.json");
6101
- let runner = "npm test";
6102
- if (await fs20.pathExists(gstackPath)) {
6103
- try {
6104
- const config = await fs20.readJson(gstackPath);
6105
- if (config.testRunner === "vitest") runner = "npx vitest run";
6106
- else if (config.testRunner === "jest") runner = "npx jest";
6107
- else if (config.testRunner === "mocha") runner = "npx mocha";
6108
- } catch {
6109
- }
6110
- }
6111
- const pkgPath = join14(cwd, "package.json");
6112
- if (await fs20.pathExists(pkgPath)) {
6113
- try {
6114
- const pkg5 = await fs20.readJson(pkgPath);
6115
- if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
6116
- return {
6117
- runner,
6118
- passed: false,
6119
- 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.",
6120
- duration: Date.now() - start
6121
- };
6122
- }
6123
- } catch {
6124
- }
6125
- }
6896
+ async function designInitCommand(cwd, options = {}) {
6126
6897
  try {
6127
- const { execa } = await import("execa");
6128
- const { stdout, stderr } = await execa("npm", ["test"], {
6898
+ await withRoboco(
6129
6899
  cwd,
6130
- timeout: 12e4,
6131
- env: { ...process.env, CI: "true" }
6900
+ "design",
6901
+ "pai design init",
6902
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.design },
6903
+ async () => {
6904
+ section("\uC124\uACC4 \uD15C\uD50C\uB9BF \uC0DD\uC131");
6905
+ const config = await loadConfig(cwd);
6906
+ const projectName = config?.projectName ?? basename3(cwd);
6907
+ await initOpenSpec(cwd, projectName);
6908
+ await initOMC(cwd, projectName);
6909
+ info("");
6910
+ info("\uB2E4\uC74C \uB2E8\uACC4: docs/openspec.md\uB97C \uC5F4\uC5B4 PRD\uB97C \uC791\uC131\uD558\uC138\uC694.");
6911
+ }
6912
+ );
6913
+ await syncHandoff(cwd).catch(() => {
6132
6914
  });
6133
- return {
6134
- runner,
6135
- passed: true,
6136
- output: stdout || stderr,
6137
- duration: Date.now() - start
6138
- };
6139
6915
  } catch (err) {
6140
- const output = err instanceof Error ? err.message : String(err);
6141
- return {
6142
- runner,
6143
- passed: false,
6144
- output,
6145
- duration: Date.now() - start
6146
- };
6916
+ handleRobocoError(err);
6147
6917
  }
6148
6918
  }
6149
- var init_runner = __esm({
6150
- "src/stages/validation/runner.ts"() {
6151
- "use strict";
6152
- }
6153
- });
6154
-
6155
- // src/stages/validation/harness.ts
6156
- import { join as join15 } from "path";
6157
- import fs21 from "fs-extra";
6158
- async function runHarnessCheck(cwd) {
6159
- const harnessPath = join15(cwd, ".pai", "harness.json");
6160
- if (!await fs21.pathExists(harnessPath)) {
6161
- return { enabled: false, specFile: null, rules: [], checks: [] };
6162
- }
6163
- let config;
6919
+ async function designValidateCommand(cwd, options = {}) {
6164
6920
  try {
6165
- config = await fs21.readJson(harnessPath);
6166
- } catch {
6167
- return { enabled: false, specFile: null, rules: [], checks: [] };
6168
- }
6169
- const specFile = config.specFile ?? "docs/openspec.md";
6170
- const rules = config.rules ?? [];
6171
- const checks = [];
6172
- if (rules.includes("spec-implementation-match")) {
6173
- const specExists = await fs21.pathExists(join15(cwd, specFile));
6174
- const srcExists = await fs21.pathExists(join15(cwd, "src"));
6175
- checks.push({
6176
- rule: "spec-implementation-match",
6177
- passed: specExists && srcExists,
6178
- 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()
6179
- });
6180
- }
6181
- if (rules.includes("api-contract-test")) {
6182
- const testDir = await fs21.pathExists(join15(cwd, "tests"));
6183
- const testDir2 = await fs21.pathExists(join15(cwd, "test"));
6184
- checks.push({
6185
- rule: "api-contract-test",
6186
- passed: testDir || testDir2,
6187
- 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"
6188
- });
6189
- }
6190
- return { enabled: true, specFile, rules, checks };
6191
- }
6192
- var init_harness = __esm({
6193
- "src/stages/validation/harness.ts"() {
6194
- "use strict";
6195
- }
6196
- });
6197
-
6198
- // src/cli/commands/validate.cmd.ts
6199
- var validate_cmd_exports = {};
6200
- __export(validate_cmd_exports, {
6201
- validateCommand: () => validateCommand
6202
- });
6203
- async function validateCommand(cwd) {
6204
- section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
6205
- const testResult = await runTests(cwd);
6206
- if (testResult.passed) {
6207
- success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
6208
- } else {
6209
- error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
6210
- info(testResult.output.slice(0, 300));
6211
- }
6212
- section("\uD558\uB124\uC2A4 \uAC80\uC99D");
6213
- const harness = await runHarnessCheck(cwd);
6214
- if (!harness.enabled) {
6215
- info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
6216
- info("\uC124\uC815 \uCD94\uAC00: `pai env setup` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
6217
- } else {
6218
- for (const check of harness.checks) {
6219
- if (check.passed) {
6220
- success(`${check.rule}: ${check.detail}`);
6221
- } else {
6222
- warn(`${check.rule}: ${check.detail}`);
6921
+ await withRoboco(
6922
+ cwd,
6923
+ "design",
6924
+ "pai design validate",
6925
+ { skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.design },
6926
+ async () => {
6927
+ section("PRD \uC644\uC131\uB3C4 \uAC80\uC99D");
6928
+ const result = await validateOpenSpec(cwd);
6929
+ info(`\uC139\uC158 \uC644\uC131\uB3C4: ${result.filledSections}/${result.totalSections}`);
6930
+ if (result.missing.length > 0) {
6931
+ console.log("");
6932
+ warn("\uBBF8\uC791\uC131 \uC139\uC158:");
6933
+ for (const m of result.missing) warn(` - ${m}`);
6934
+ }
6935
+ for (const w of result.warnings) warn(w);
6936
+ if (result.complete) {
6937
+ console.log("");
6938
+ success("PRD \uC644\uC131\uB3C4 \uAC80\uC99D \uD1B5\uACFC!");
6939
+ } else {
6940
+ console.log("");
6941
+ info("docs/openspec.md\uB97C \uC5F4\uC5B4 \uBBF8\uC791\uC131 \uC139\uC158\uC744 \uCC44\uC6CC\uC8FC\uC138\uC694.");
6942
+ }
6223
6943
  }
6224
- }
6225
- }
6226
- const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
6227
- console.log("");
6228
- if (allPassed) {
6229
- success("\uAC80\uC99D \uD1B5\uACFC!");
6230
- } else if (!testResult.passed) {
6231
- error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
6232
- process.exitCode = 1;
6233
- } else {
6234
- warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
6944
+ );
6945
+ await syncHandoff(cwd).catch(() => {
6946
+ });
6947
+ } catch (err) {
6948
+ handleRobocoError(err);
6235
6949
  }
6236
6950
  }
6237
- var init_validate_cmd = __esm({
6238
- "src/cli/commands/validate.cmd.ts"() {
6951
+ var init_design_cmd = __esm({
6952
+ "src/cli/commands/design.cmd.ts"() {
6239
6953
  "use strict";
6240
6954
  init_ui();
6241
- init_runner();
6242
- init_harness();
6955
+ init_openspec();
6956
+ init_omc();
6957
+ init_config();
6958
+ init_roboco();
6959
+ init_gates();
6960
+ init_validate_cmd();
6243
6961
  }
6244
6962
  });
6245
6963
 
@@ -6295,13 +7013,13 @@ var init_context = __esm({
6295
7013
 
6296
7014
  // src/stages/design/index.ts
6297
7015
  import { join as join16 } from "path";
6298
- import fs22 from "fs-extra";
7016
+ import fs24 from "fs-extra";
6299
7017
  async function autoInstallHarness(cwd) {
6300
7018
  const harnessPath = join16(cwd, ".pai", "harness.json");
6301
- if (await fs22.pathExists(harnessPath)) return;
7019
+ if (await fs24.pathExists(harnessPath)) return;
6302
7020
  await withSpinner("Harness Engineering \uC790\uB3D9 \uC124\uC815 \uC911...", async () => {
6303
- await fs22.ensureDir(join16(cwd, ".pai"));
6304
- await fs22.writeJson(harnessPath, {
7021
+ await fs24.ensureDir(join16(cwd, ".pai"));
7022
+ await fs24.writeJson(harnessPath, {
6305
7023
  version: "1.0",
6306
7024
  specFile: "docs/openspec.md",
6307
7025
  checkOn: ["pre-commit", "ci"],
@@ -6621,22 +7339,41 @@ __export(pipeline_cmd_exports, {
6621
7339
  });
6622
7340
  async function pipelineCommand(cwd, options) {
6623
7341
  printBanner();
6624
- const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "mockup");
6625
- const pipelineOpts = {};
6626
- if (options.from) {
6627
- pipelineOpts.from = options.from;
6628
- }
6629
- if (options.only) {
6630
- pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7342
+ try {
7343
+ await withRoboco(
7344
+ cwd,
7345
+ "environment",
7346
+ "pai run",
7347
+ {
7348
+ skipGates: options.skipGates,
7349
+ force: options.force,
7350
+ ttlMs: PIPELINE_TTL_MS
7351
+ },
7352
+ async () => {
7353
+ const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
7354
+ const pipelineOpts = {};
7355
+ if (options.from) pipelineOpts.from = options.from;
7356
+ if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
7357
+ await runPipeline(cwd, config, pipelineOpts);
7358
+ }
7359
+ );
7360
+ await syncHandoff(cwd).catch(() => {
7361
+ });
7362
+ } catch (err) {
7363
+ handleRobocoError(err);
6631
7364
  }
6632
- await runPipeline(cwd, config, pipelineOpts);
6633
7365
  }
7366
+ var PIPELINE_TTL_MS;
6634
7367
  var init_pipeline_cmd = __esm({
6635
7368
  "src/cli/commands/pipeline.cmd.ts"() {
6636
7369
  "use strict";
7370
+ init_ui();
6637
7371
  init_pipeline();
6638
7372
  init_config();
6639
7373
  init_ui();
7374
+ init_roboco();
7375
+ init_validate_cmd();
7376
+ PIPELINE_TTL_MS = 30 * 60 * 1e3;
6640
7377
  }
6641
7378
  });
6642
7379
 
@@ -6658,7 +7395,7 @@ __export(remove_cmd_exports, {
6658
7395
  removeCommand: () => removeCommand
6659
7396
  });
6660
7397
  import { basename as basename4, dirname } from "path";
6661
- import fs23 from "fs-extra";
7398
+ import fs25 from "fs-extra";
6662
7399
  async function removeCommand(cwd, options) {
6663
7400
  section("\uD504\uB85C\uC81D\uD2B8 \uC0AD\uC81C");
6664
7401
  const config = await loadConfig(cwd);
@@ -6676,7 +7413,7 @@ async function removeCommand(cwd, options) {
6676
7413
  console.log(colors.err(` ${folderName}/ \uD3F4\uB354 \uC804\uCCB4\uAC00 \uC0AD\uC81C\uB429\uB2C8\uB2E4.`));
6677
7414
  hint("\uC774 \uC791\uC5C5\uC740 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6678
7415
  console.log("");
6679
- const items = await fs23.readdir(cwd);
7416
+ const items = await fs25.readdir(cwd);
6680
7417
  const fileCount = items.filter((i) => !i.startsWith(".")).length;
6681
7418
  const hiddenCount = items.filter((i) => i.startsWith(".")).length;
6682
7419
  info(`\uD30C\uC77C/\uD3F4\uB354 ${fileCount}\uAC1C, \uC228\uAE40 \uD56D\uBAA9 ${hiddenCount}\uAC1C`);
@@ -6695,7 +7432,7 @@ async function removeCommand(cwd, options) {
6695
7432
  }
6696
7433
  process.chdir(parentDir);
6697
7434
  try {
6698
- await fs23.remove(cwd);
7435
+ await fs25.remove(cwd);
6699
7436
  console.log("");
6700
7437
  success(`${folderName}/ \uD504\uB85C\uC81D\uD2B8\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
6701
7438
  try {
@@ -6974,7 +7711,7 @@ __export(savetoken_cmd_exports, {
6974
7711
  savetokenCommand: () => savetokenCommand
6975
7712
  });
6976
7713
  import { join as join17, relative } from "path";
6977
- import fs24 from "fs-extra";
7714
+ import fs26 from "fs-extra";
6978
7715
  import chalk7 from "chalk";
6979
7716
  async function savetokenCommand(cwd) {
6980
7717
  const { createSpinner: createSpinner2 } = await Promise.resolve().then(() => (init_progress(), progress_exports));
@@ -7044,10 +7781,10 @@ async function savetokenCommand(cwd) {
7044
7781
  console.log(` ${chalk7.red("\u25CF")} \uB192\uC74C \uCF54\uB4DC \uC0DD\uC131, \uBCF5\uC7A1\uD55C \uCD94\uB860, \uCC3D\uC758\uC801 \uC0DD\uC131`);
7045
7782
  console.log(` \u2192 ${colors.dim("AI \uD544\uC218 \u2014 \uD504\uB86C\uD504\uD2B8 \uCD5C\uC801\uD654\uB85C \uD1A0\uD070 \uC808\uAC10")}`);
7046
7783
  const reportDir = join17(cwd, ".pai");
7047
- await fs24.ensureDir(reportDir);
7784
+ await fs26.ensureDir(reportDir);
7048
7785
  const report = buildReport(callSites, cwd);
7049
7786
  const reportPath = join17(reportDir, "savetoken-report.md");
7050
- await fs24.writeFile(reportPath, report, "utf8");
7787
+ await fs26.writeFile(reportPath, report, "utf8");
7051
7788
  console.log("");
7052
7789
  success("\uC2A4\uCE94 \uB9AC\uD3EC\uD2B8 \uC800\uC7A5: .pai/savetoken-report.md");
7053
7790
  console.log("");
@@ -7205,7 +7942,7 @@ __export(wakeup_cmd_exports, {
7205
7942
  });
7206
7943
  import { join as join18 } from "path";
7207
7944
  import { homedir as homedir3, platform as osPlatform } from "os";
7208
- import fs25 from "fs-extra";
7945
+ import fs27 from "fs-extra";
7209
7946
  import chalk8 from "chalk";
7210
7947
  async function wakeupCommand(timeOrAction, schedule = "\uD3C9\uC77C") {
7211
7948
  if (timeOrAction === "off") {
@@ -7252,9 +7989,9 @@ async function wakeupCommand(timeOrAction, schedule = "\uD3C9\uC77C") {
7252
7989
  projectDir,
7253
7990
  launchMode
7254
7991
  };
7255
- await fs25.ensureDir(PAI_DIR2);
7256
- await fs25.writeJson(CONFIG_FILE2, config, { spaces: 2 });
7257
- await fs25.writeJson(MESSAGES_FILE, MESSAGES);
7992
+ await fs27.ensureDir(PAI_DIR2);
7993
+ await fs27.writeJson(CONFIG_FILE2, config, { spaces: 2 });
7994
+ await fs27.writeJson(MESSAGES_FILE, MESSAGES);
7258
7995
  await createWakeupScript(config);
7259
7996
  if (osPlatform() === "darwin") {
7260
7997
  await setupMacOS(config);
@@ -7285,7 +8022,7 @@ async function setupMacOS(config) {
7285
8022
  const { execa } = await import("execa");
7286
8023
  const [hour, minute] = config.time.split(":").map(Number);
7287
8024
  const plistDir = join18(homedir3(), "Library", "LaunchAgents");
7288
- await fs25.ensureDir(plistDir);
8025
+ await fs27.ensureDir(plistDir);
7289
8026
  const weekdays = scheduleToWeekdays(config.schedule);
7290
8027
  let calendarEntries;
7291
8028
  if (weekdays.length === 7) {
@@ -7321,7 +8058,7 @@ ${calendarEntries}
7321
8058
  <string>${PAI_DIR2}/wakeup.log</string>
7322
8059
  </dict>
7323
8060
  </plist>`;
7324
- await fs25.writeFile(PLIST_PATH, plist);
8061
+ await fs27.writeFile(PLIST_PATH, plist);
7325
8062
  await execa("launchctl", ["unload", PLIST_PATH]).catch(() => {
7326
8063
  });
7327
8064
  await execa("launchctl", ["load", PLIST_PATH]);
@@ -7344,7 +8081,7 @@ async function setupWindows(config) {
7344
8081
  const { execa } = await import("execa");
7345
8082
  const [hour, minute] = config.time.split(":").map(Number);
7346
8083
  const psScriptDir = join18(homedir3(), ".pai");
7347
- await fs25.ensureDir(psScriptDir);
8084
+ await fs27.ensureDir(psScriptDir);
7348
8085
  const psScriptPath = join18(psScriptDir, "wakeup.ps1");
7349
8086
  const claudeCmd = config.launchMode === "yolo" ? "claude --dangerously-skip-permissions" : "claude";
7350
8087
  const psScript = `# PAI Wakeup \u2014 Claude Code \uC138\uC158 \uC790\uB3D9 \uC2DC\uC791
@@ -7374,7 +8111,7 @@ $notifier.Show([Windows.UI.Notifications.ToastNotification]::new($xml))
7374
8111
  # Open PowerShell with Claude Code
7375
8112
  Start-Process powershell -ArgumentList "-NoExit", "-Command", "Get-Content '$todayFile'; Write-Host ''; Set-Location '${config.projectDir}'; ${claudeCmd}"
7376
8113
  `;
7377
- await fs25.writeFile(psScriptPath, psScript, "utf8");
8114
+ await fs27.writeFile(psScriptPath, psScript, "utf8");
7378
8115
  const daysMap = {
7379
8116
  "\uD3C9\uC77C": "MON,TUE,WED,THU,FRI",
7380
8117
  "\uB9E4\uC77C": "MON,TUE,WED,THU,FRI,SAT,SUN",
@@ -7422,7 +8159,7 @@ async function disableWakeup() {
7422
8159
  if (osPlatform() === "darwin") {
7423
8160
  await execa("launchctl", ["unload", PLIST_PATH]).catch(() => {
7424
8161
  });
7425
- await fs25.remove(PLIST_PATH).catch(() => {
8162
+ await fs27.remove(PLIST_PATH).catch(() => {
7426
8163
  });
7427
8164
  success("launchd \uC2A4\uCF00\uC904 \uC81C\uAC70");
7428
8165
  console.log("");
@@ -7443,14 +8180,14 @@ async function disableWakeup() {
7443
8180
  } else {
7444
8181
  await removeCronEntry();
7445
8182
  }
7446
- await fs25.remove(CONFIG_FILE2).catch(() => {
8183
+ await fs27.remove(CONFIG_FILE2).catch(() => {
7447
8184
  });
7448
8185
  console.log("");
7449
8186
  success("\u2600\uFE0F \uC6E8\uC774\uD06C\uC5C5 \uD574\uC81C \uC644\uB8CC");
7450
8187
  }
7451
8188
  async function showStatus() {
7452
- if (await fs25.pathExists(CONFIG_FILE2)) {
7453
- const config = await fs25.readJson(CONFIG_FILE2);
8189
+ if (await fs27.pathExists(CONFIG_FILE2)) {
8190
+ const config = await fs27.readJson(CONFIG_FILE2);
7454
8191
  console.log("");
7455
8192
  success("\u2600\uFE0F \uC6E8\uC774\uD06C\uC5C5 \uD65C\uC131\uD654");
7456
8193
  console.log(` \uC2DC\uAC04 ${chalk8.white(config.time)}`);
@@ -7458,7 +8195,7 @@ async function showStatus() {
7458
8195
  console.log(` \uD504\uB85C\uC81D\uD2B8 ${chalk8.white(config.projectDir)}`);
7459
8196
  console.log(` \uBAA8\uB4DC ${chalk8.white(config.launchMode === "yolo" ? "claude-YOLO mode" : "\uC77C\uBC18 \uBAA8\uB4DC")}`);
7460
8197
  if (osPlatform() === "darwin") {
7461
- const plistExists = await fs25.pathExists(PLIST_PATH);
8198
+ const plistExists = await fs27.pathExists(PLIST_PATH);
7462
8199
  console.log(` launchd ${plistExists ? chalk8.green("\uD65C\uC131") : chalk8.red("\uBE44\uD65C\uC131")}`);
7463
8200
  }
7464
8201
  console.log("");
@@ -7611,7 +8348,7 @@ fi
7611
8348
 
7612
8349
  echo "[$(date)] PAI Wakeup completed" >> "$LOG_FILE"
7613
8350
  `;
7614
- await fs25.writeFile(SCRIPT_FILE, script, { mode: 493 });
8351
+ await fs27.writeFile(SCRIPT_FILE, script, { mode: 493 });
7615
8352
  }
7616
8353
  var PAI_DIR2, CONFIG_FILE2, MESSAGES_FILE, SCRIPT_FILE, PLIST_NAME, PLIST_PATH, CRON_MARKER, MESSAGES;
7617
8354
  var init_wakeup_cmd = __esm({
@@ -7711,9 +8448,9 @@ function createProgram() {
7711
8448
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init_cmd(), init_cmd_exports));
7712
8449
  await initCommand2(process.cwd(), projectName);
7713
8450
  });
7714
- program2.command("add").description("\uD50C\uB7EC\uADF8\uC778 \uCD94\uAC00 (Mockup \u2192 Production \uD655\uC7A5)").action(async () => {
8451
+ program2.command("add").description("\uD50C\uB7EC\uADF8\uC778 \uCD94\uAC00 (Mockup \u2192 Production \uD655\uC7A5)").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC").action(async (options) => {
7715
8452
  const { addCommand } = await Promise.resolve().then(() => (init_add_cmd(), add_cmd_exports));
7716
- await addCommand(process.cwd());
8453
+ await addCommand(process.cwd(), options);
7717
8454
  });
7718
8455
  program2.command("check").description("\uD658\uACBD \uC810\uAC80 (Node / Git / Claude Code)").action(async () => {
7719
8456
  const { checkCommand: checkCommand2 } = await Promise.resolve().then(() => (init_check_cmd(), check_cmd_exports));
@@ -7728,23 +8465,23 @@ function createProgram() {
7728
8465
  await helpCommand2();
7729
8466
  });
7730
8467
  const design = program2.command("design").description("\uC124\uACC4 \uAD00\uB9AC (OpenSpec / OMC)");
7731
- design.command("init").description("PRD/OMC \uD15C\uD50C\uB9BF \uC0DD\uC131").action(async () => {
8468
+ design.command("init").description("PRD/OMC \uD15C\uD50C\uB9BF \uC0DD\uC131").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC").action(async (options) => {
7732
8469
  const { designInitCommand: designInitCommand2 } = await Promise.resolve().then(() => (init_design_cmd(), design_cmd_exports));
7733
- await designInitCommand2(process.cwd());
8470
+ await designInitCommand2(process.cwd(), options);
7734
8471
  });
7735
- design.command("validate").description("PRD \uC644\uC131\uB3C4 \uAC80\uC99D").action(async () => {
8472
+ design.command("validate").description("PRD \uC644\uC131\uB3C4 \uAC80\uC99D").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC").action(async (options) => {
7736
8473
  const { designValidateCommand: designValidateCommand2 } = await Promise.resolve().then(() => (init_design_cmd(), design_cmd_exports));
7737
- await designValidateCommand2(process.cwd());
8474
+ await designValidateCommand2(process.cwd(), options);
7738
8475
  });
7739
- program2.command("test").description("\uD14C\uC2A4\uD2B8 / \uD558\uB124\uC2A4 \uAC80\uC99D \uC2E4\uD589").action(async () => {
8476
+ program2.command("test").description("\uD14C\uC2A4\uD2B8 / \uD558\uB124\uC2A4 \uAC80\uC99D \uC2E4\uD589").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC").action(async (options) => {
7740
8477
  const { testCommand } = await Promise.resolve().then(() => (init_test_cmd(), test_cmd_exports));
7741
- await testCommand(process.cwd());
8478
+ await testCommand(process.cwd(), options);
7742
8479
  });
7743
- program2.command("grade").description("AI \uAC1C\uBC1C \uC900\uBE44\uB3C4 \uD3C9\uAC00 (6\uCE74\uD14C\uACE0\uB9AC \uC810\uC218)").option("--fail-under <score>", "\uCD5C\uC18C \uC810\uC218 (\uBBF8\uB2EC \uC2DC exit 1)", parseInt).option("--verbose", "\uC0C1\uC138 findings \uCD9C\uB825").option("-o, --output <file>", "\uACB0\uACFC\uB97C \uD30C\uC77C\uB85C \uC800\uC7A5").option("--no-cache", "\uCE90\uC2DC \uBB34\uC2DC\uD558\uACE0 \uC0C8\uB85C \uBD84\uC11D").action(async (options) => {
8480
+ program2.command("grade").description("AI \uAC1C\uBC1C \uC900\uBE44\uB3C4 \uD3C9\uAC00 (6\uCE74\uD14C\uACE0\uB9AC \uC810\uC218)").option("--fail-under <score>", "\uCD5C\uC18C \uC810\uC218 (\uBBF8\uB2EC \uC2DC exit 1)", parseInt).option("--verbose", "\uC0C1\uC138 findings \uCD9C\uB825").option("-o, --output <file>", "\uACB0\uACFC\uB97C \uD30C\uC77C\uB85C \uC800\uC7A5").option("--no-cache", "\uCE90\uC2DC \uBB34\uC2DC\uD558\uACE0 \uC0C8\uB85C \uBD84\uC11D").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC (\uCD5C\uADFC validation \uC5C6\uC5B4\uB3C4 \uC2E4\uD589)").action(async (options) => {
7744
8481
  const { gradeCommand } = await Promise.resolve().then(() => (init_grade_cmd(), grade_cmd_exports));
7745
8482
  await gradeCommand(process.cwd(), options);
7746
8483
  });
7747
- program2.command("run").description("\uC804\uCCB4 5\uB2E8\uACC4 \uD30C\uC774\uD504\uB77C\uC778 \uC2E4\uD589").option("--from <stage>", "\uD2B9\uC815 \uB2E8\uACC4\uBD80\uD130 \uC2E4\uD589").option("--only <stages>", "\uC120\uD0DD\uC801 \uB2E8\uACC4 \uC2E4\uD589 (\uC27C\uD45C \uAD6C\uBD84)").action(async (options) => {
8484
+ program2.command("run").description("\uC804\uCCB4 5\uB2E8\uACC4 \uD30C\uC774\uD504\uB77C\uC778 \uC2E4\uD589").option("--from <stage>", "\uD2B9\uC815 \uB2E8\uACC4\uBD80\uD130 \uC2E4\uD589").option("--only <stages>", "\uC120\uD0DD\uC801 \uB2E8\uACC4 \uC2E4\uD589 (\uC27C\uD45C \uAD6C\uBD84)").option("--skip-gates", "\uAC8C\uC774\uD2B8 \uACBD\uACE0\uB9CC \uCD9C\uB825\uD558\uACE0 \uC9C4\uD589").option("--force", "\uAC8C\uC774\uD2B8 \uC804\uBA74 \uBB34\uC2DC").action(async (options) => {
7748
8485
  const { runCommand } = await Promise.resolve().then(() => (init_run_cmd(), run_cmd_exports));
7749
8486
  await runCommand(process.cwd(), options);
7750
8487
  });