@byh3071/vhk 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +485 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  // src/index.ts
49
49
  import { Command, Help } from "commander";
50
50
  import { pathToFileURL } from "url";
51
- import chalk33 from "chalk";
51
+ import chalk34 from "chalk";
52
52
  import inquirer13 from "inquirer";
53
53
 
54
54
  // src/lib/nlp-router.ts
@@ -123,6 +123,12 @@ var RULES = [
123
123
  confidence: "high",
124
124
  test: (t2) => /검증\s*묶음|사전\s*검증|저장\s*전\s*(검증|확인)|^verify$/.test(t2)
125
125
  },
126
+ {
127
+ command: "review",
128
+ explanation: "\uC801\uB300\uC801 \uC790\uAE30\uAC80\uC99D \u2014 \uC99D\uAC70\uB85C \uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC (vhk review)",
129
+ confidence: "high",
130
+ test: (t2) => /적대\s*검증|자기\s*검증|거짓\s*완료|완료\s*심문|^review$|^검토$/.test(t2)
131
+ },
126
132
  {
127
133
  command: "init",
128
134
  explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
@@ -468,6 +474,8 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
468
474
  "\uBAA8\uB4DC",
469
475
  "verify",
470
476
  "\uC0AC\uC804\uC810\uAC80",
477
+ "review",
478
+ "\uAC80\uD1A0",
471
479
  "help"
472
480
  ]);
473
481
  function isOptionToken(token) {
@@ -505,7 +513,7 @@ function detectNaturalLanguageInput(argv) {
505
513
  }
506
514
 
507
515
  // src/lib/nlp-run.ts
508
- import chalk31 from "chalk";
516
+ import chalk32 from "chalk";
509
517
  import inquirer12 from "inquirer";
510
518
 
511
519
  // src/commands/gate.ts
@@ -2661,7 +2669,9 @@ function generateGateScript(id) {
2661
2669
  " process.exit(1)",
2662
2670
  "}",
2663
2671
  "",
2664
- "const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf-8')) : {}",
2672
+ "// BOM-safe \uC77D\uAE30: PowerShell Set-Content -Encoding utf8 \uC758 UTF-8 BOM \uC81C\uAC70(\uC5C6\uC73C\uBA74 throw).",
2673
+ "const readJson = (p) => { const t = readFileSync(p, 'utf-8'); return JSON.parse(t.charCodeAt(0) === 0xfeff ? t.slice(1) : t) }",
2674
+ "const pkg = existsSync('package.json') ? readJson('package.json') : {}",
2665
2675
  "const scripts = pkg.scripts ?? {}",
2666
2676
  "const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
2667
2677
  "const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
@@ -5030,7 +5040,7 @@ function readCloudConfig(rootDir) {
5030
5040
  const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5031
5041
  if (!fs11.existsSync(p)) return null;
5032
5042
  try {
5033
- const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
5043
+ const parsed = readJsonFile(p);
5034
5044
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
5035
5045
  return { gistId: parsed.gistId };
5036
5046
  }
@@ -5364,9 +5374,116 @@ import { execFileSync as execFileSync4 } from "child_process";
5364
5374
  import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
5365
5375
  import { join as join10 } from "path";
5366
5376
  import chalk30 from "chalk";
5377
+
5378
+ // src/commands/verify-report.ts
5379
+ function escapeHtml(text) {
5380
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5381
+ }
5382
+ var STATUS_COLOR = {
5383
+ PASS: { bg: "#16a34a", fg: "#ffffff", label: "PASS" },
5384
+ WARN: { bg: "#d97706", fg: "#ffffff", label: "WARN" },
5385
+ FAIL: { bg: "#dc2626", fg: "#ffffff", label: "FAIL" }
5386
+ };
5387
+ var GATE_STATUS = {
5388
+ pass: { color: "#16a34a", label: "\uD1B5\uACFC" },
5389
+ fail: { color: "#dc2626", label: "\uC2E4\uD328" },
5390
+ skip: { color: "#d97706", label: "\uAC74\uB108\uB700" }
5391
+ };
5392
+ function renderGateRow(g) {
5393
+ const s = GATE_STATUS[g.status];
5394
+ const exit = g.exitCode === null ? "\u2014" : String(g.exitCode);
5395
+ const detail = g.detail ? escapeHtml(g.detail) : "";
5396
+ return ` <tr>
5397
+ <td class="gate-label">${escapeHtml(g.label)}</td>
5398
+ <td><span class="gate-status" style="color:${s.color}">${s.label}</span></td>
5399
+ <td class="gate-exit">${escapeHtml(exit)}</td>
5400
+ <td class="gate-detail">${detail}</td>
5401
+ </tr>`;
5402
+ }
5403
+ function renderReportHtml(report) {
5404
+ const c = STATUS_COLOR[report.status];
5405
+ const s = report.summary;
5406
+ const rows = report.gates.map(renderGateRow).join("\n");
5407
+ const actions = report.nextActions.map((a) => ` <li>${escapeHtml(a)}</li>`).join("\n");
5408
+ return `<!DOCTYPE html>
5409
+ <html lang="ko">
5410
+ <head>
5411
+ <meta charset="utf-8">
5412
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5413
+ <title>vhk verify \uB9AC\uD3EC\uD2B8 \u2014 ${c.label}</title>
5414
+ <style>
5415
+ :root { color-scheme: light dark; }
5416
+ * { box-sizing: border-box; }
5417
+ body {
5418
+ margin: 0; padding: 2rem 1rem;
5419
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
5420
+ background: #f8fafc; color: #0f172a; line-height: 1.55;
5421
+ }
5422
+ .wrap { max-width: 760px; margin: 0 auto; }
5423
+ header { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
5424
+ h1 { font-size: 1.25rem; margin: 0; font-weight: 700; }
5425
+ .badge {
5426
+ display: inline-block; padding: 0.35rem 1rem; border-radius: 9999px;
5427
+ font-weight: 800; font-size: 1rem; letter-spacing: 0.05em;
5428
+ background: ${c.bg}; color: ${c.fg};
5429
+ }
5430
+ .summary { color: #475569; font-size: 0.95rem; margin: 0.25rem 0 1.5rem; }
5431
+ .summary strong { color: #0f172a; }
5432
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1.75rem; font-size: 0.95rem; }
5433
+ th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid #e2e8f0; }
5434
+ th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; }
5435
+ .gate-label { font-weight: 600; }
5436
+ .gate-status { font-weight: 700; }
5437
+ .gate-exit { font-variant-numeric: tabular-nums; color: #475569; }
5438
+ .gate-detail { color: #64748b; font-size: 0.88rem; }
5439
+ h2 { font-size: 1rem; margin: 0 0 0.5rem; }
5440
+ ul { margin: 0 0 1.75rem; padding-left: 1.25rem; }
5441
+ li { margin: 0.2rem 0; }
5442
+ footer { color: #94a3b8; font-size: 0.8rem; border-top: 1px solid #e2e8f0; padding-top: 0.75rem; }
5443
+ @media (prefers-color-scheme: dark) {
5444
+ body { background: #0f172a; color: #e2e8f0; }
5445
+ .summary { color: #94a3b8; } .summary strong { color: #e2e8f0; }
5446
+ th { color: #94a3b8; } th, td { border-color: #1e293b; }
5447
+ .gate-exit { color: #94a3b8; } .gate-detail { color: #94a3b8; }
5448
+ footer { color: #64748b; border-color: #1e293b; }
5449
+ }
5450
+ </style>
5451
+ </head>
5452
+ <body>
5453
+ <div class="wrap">
5454
+ <header>
5455
+ <h1>\u{1F50E} vhk verify \uB9AC\uD3EC\uD2B8</h1>
5456
+ <span class="badge">${c.label}</span>
5457
+ </header>
5458
+ <p class="summary">
5459
+ \uAC8C\uC774\uD2B8 <strong>${s.total}</strong>\uAC1C \u2014 \uD1B5\uACFC ${s.pass} / \uC2E4\uD328 ${s.fail} / \uAC74\uB108\uB700 ${s.skip}
5460
+ </p>
5461
+ <table>
5462
+ <thead>
5463
+ <tr><th>\uAC8C\uC774\uD2B8</th><th>\uC0C1\uD0DC</th><th>\uC885\uB8CC\uCF54\uB4DC</th><th>\uBE44\uACE0</th></tr>
5464
+ </thead>
5465
+ <tbody>
5466
+ ${rows}
5467
+ </tbody>
5468
+ </table>
5469
+ <h2>\uB2E4\uC74C \uD589\uB3D9</h2>
5470
+ <ul>
5471
+ ${actions}
5472
+ </ul>
5473
+ <footer>
5474
+ \uC0DD\uC131: ${escapeHtml(report.generatedAt)} \xB7 \uB0A0\uC9DC: ${escapeHtml(report.date)} \xB7 schema v${report.schemaVersion}
5475
+ </footer>
5476
+ </div>
5477
+ </body>
5478
+ </html>
5479
+ `;
5480
+ }
5481
+
5482
+ // src/commands/verify.ts
5367
5483
  var REPORT_SCHEMA_VERSION = 1;
5368
5484
  var REPORT_DIR_REL = join10(".vhk", "reports");
5369
5485
  var REPORT_PATH_REL = join10(REPORT_DIR_REL, "latest.json");
5486
+ var REPORT_HTML_PATH_REL = join10(REPORT_DIR_REL, "latest.html");
5370
5487
  var SHIM = /* @__PURE__ */ new Set(["pnpm", "npm", "npx", "yarn"]);
5371
5488
  function detectPm(cwd) {
5372
5489
  if (existsSync16(join10(cwd, "pnpm-lock.yaml"))) return "pnpm";
@@ -5523,9 +5640,67 @@ var STATUS_BADGE = {
5523
5640
  WARN: chalk30.yellow.bold("WARN"),
5524
5641
  FAIL: chalk30.red.bold("FAIL")
5525
5642
  };
5643
+ async function renderVerifyReport(cwd, opts) {
5644
+ const jsonPath = join10(cwd, REPORT_PATH_REL);
5645
+ let report;
5646
+ if (existsSync16(jsonPath)) {
5647
+ try {
5648
+ report = readJsonFile(jsonPath);
5649
+ } catch {
5650
+ console.log(chalk30.yellow(" \u26A0\uFE0F \uAE30\uC874 latest.json \uC190\uC0C1 \u2014 verify \uC7AC\uC2E4\uD589\uC73C\uB85C \uC99D\uAC70\uB97C \uB2E4\uC2DC \uB9CC\uB4ED\uB2C8\uB2E4."));
5651
+ report = verifyEvidence(cwd).report;
5652
+ }
5653
+ } else {
5654
+ console.log(chalk30.dim(" latest.json \uC5C6\uC74C \u2014 verify 1\uD68C \uC120\uC2E4\uD589\uC73C\uB85C \uC99D\uAC70\uB97C \uB9CC\uB4ED\uB2C8\uB2E4."));
5655
+ report = verifyEvidence(cwd).report;
5656
+ }
5657
+ const html = renderReportHtml(report);
5658
+ const htmlPath = join10(cwd, REPORT_HTML_PATH_REL);
5659
+ try {
5660
+ mkdirSync11(join10(cwd, REPORT_DIR_REL), { recursive: true });
5661
+ writeFileSync11(htmlPath, html, "utf-8");
5662
+ } catch (e) {
5663
+ console.error(
5664
+ chalk30.red(` \u274C \uB9AC\uD3EC\uD2B8 HTML \uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (${REPORT_HTML_PATH_REL}): ${e instanceof Error ? e.message : String(e)}`)
5665
+ );
5666
+ console.error(chalk30.dim(" \uD574\uB2F9 \uACBD\uB85C\uC758 \uC4F0\uAE30 \uAD8C\uD55C\uC744 \uD655\uC778\uD558\uC138\uC694."));
5667
+ process.exitCode = 1;
5668
+ return;
5669
+ }
5670
+ console.log(chalk30.bold("\n\u{1F50E} \uAC80\uC99D \uB9AC\uD3EC\uD2B8 (verify --report)"));
5671
+ console.log(` \uACB0\uACFC: ${STATUS_BADGE[report.status]}`);
5672
+ console.log(chalk30.dim(` \u{1F4C4} HTML: ${REPORT_HTML_PATH_REL}`));
5673
+ process.exitCode = report.status === "FAIL" ? 1 : 0;
5674
+ if (opts.open) {
5675
+ if (isInteractive()) openReportInBrowser(htmlPath);
5676
+ else console.log(chalk30.dim(" (\uBE44\uB300\uD654\uD615/CI/MCP \u2014 --open \uC790\uB3D9 \uC2A4\uD0B5)"));
5677
+ return;
5678
+ }
5679
+ printNextStep({
5680
+ message: "\uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uC644\uB8CC. \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uB824\uBA74:",
5681
+ command: "vhk verify --report --open",
5682
+ cursorHint: "\uB9AC\uD3EC\uD2B8 \uC5F4\uC5B4\uC918"
5683
+ });
5684
+ }
5685
+ function openReportInBrowser(filePath) {
5686
+ let result;
5687
+ if (process.platform === "darwin") {
5688
+ result = safeExecFile("open", [filePath]);
5689
+ } else if (process.platform === "win32") {
5690
+ result = safeExecFile("rundll32.exe", ["url.dll,FileProtocolHandler", filePath]);
5691
+ } else {
5692
+ result = safeExecFile("xdg-open", [filePath]);
5693
+ }
5694
+ if (result.ok) console.log(chalk30.green(" \u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
5695
+ else console.log(chalk30.yellow(" \u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC704 \uD30C\uC77C\uC744 \uC9C1\uC811 \uC5EC\uC138\uC694."));
5696
+ }
5526
5697
  async function verify(opts = {}) {
5527
5698
  if (!ensureNotHardStopped("verify")) return;
5528
5699
  const cwd = process.cwd();
5700
+ if (opts.report) {
5701
+ await renderVerifyReport(cwd, opts);
5702
+ return;
5703
+ }
5529
5704
  const { report, path: path14 } = verifyEvidence(cwd);
5530
5705
  if (opts.json) {
5531
5706
  console.log(JSON.stringify(report, null, 2));
@@ -5564,6 +5739,276 @@ async function verify(opts = {}) {
5564
5739
  }
5565
5740
  }
5566
5741
 
5742
+ // src/commands/review.ts
5743
+ import { existsSync as existsSync17, writeFileSync as writeFileSync12 } from "fs";
5744
+ import { join as join11 } from "path";
5745
+ import chalk31 from "chalk";
5746
+ var GOALS_DIR2 = "goals";
5747
+ var COVERAGE_MIN = 0.5;
5748
+ var STALE_AGE_MS = 6 * 60 * 60 * 1e3;
5749
+ var REVIEW_DISCLAIMER = [
5750
+ "\u26A0\uFE0F \uC774 \uD310\uC815\uC740 \uBCF4\uC7A5\uC774 \uC544\uB2C8\uB77C \uC2E0\uB8B0\uB3C4 \uC2E0\uD638\uC785\uB2C8\uB2E4 \u2014 \uD1B5\uACFC\uD574\uB3C4 \uAC70\uC9D3\uC644\uB8CC \uAC00\uB2A5\uC131\uC740 \uB0A8\uC2B5\uB2C8\uB2E4.",
5751
+ ' \xB7 \uAE30\uB2A5 \uACE0\uC720 \uC644\uB8CC\uC870\uAC74\uC740 \uAC8C\uC774\uD2B8 \uD0A4\uC6CC\uB4DC(tsc/test/build/secure)\uC5D0 \uB9E4\uD551\uB418\uC9C0 \uC54A\uC73C\uBA74 "\uBBF8\uAC80\uC99D"\uC73C\uB85C \uB0A8\uC2B5\uB2C8\uB2E4.',
5752
+ " \xB7 \uC99D\uAC70(latest.json)\uB294 commit/goal \uBC14\uC778\uB529\uC774 \uC5C6\uC5B4 \uC2E0\uC120\uB3C4\uB294 \uC0DD\uC131\uC2DC\uAC01\uC73C\uB85C\uB9CC \uCD94\uC815\uD569\uB2C8\uB2E4 \u2014 \uCF54\uB4DC \uBCC0\uACBD \uD6C4\uC5D4 vhk verify \uC7AC\uC2E4\uD589 \uD544\uC694.",
5753
+ " \xB7 git diff \uBBF8\uC0AC\uC6A9(v0) \u2014 \uAE30\uC874 \uD14C\uC2A4\uD2B8\uAC00 green \uC774\uC5B4\uB3C4 \uC774\uBC88 \uBCC0\uACBD\uC744 \uCEE4\uBC84\uD558\uC9C0 \uBABB\uD588\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
5754
+ ].join("\n");
5755
+ function parseCompletionChecks(body) {
5756
+ const lines = body.split(/\r?\n/);
5757
+ const out = [];
5758
+ let inSection = false;
5759
+ for (const raw of lines) {
5760
+ const line = raw.trimEnd();
5761
+ const heading = line.match(/^#{1,6}\s+(.*)$/);
5762
+ if (heading) {
5763
+ inSection = /completion\s*check/i.test(heading[1]);
5764
+ continue;
5765
+ }
5766
+ if (!inSection) continue;
5767
+ const box = line.match(/^\s*-\s*\[([ xX])\]\s*(.+)$/);
5768
+ if (box) out.push({ text: box[2].trim(), checked: box[1].toLowerCase() === "x" });
5769
+ }
5770
+ return out;
5771
+ }
5772
+ function impliedGates(text) {
5773
+ if (/게이트|공통\s*게이트|goal\s*check/i.test(text)) {
5774
+ return ["typecheck", "test", "build", "secure"];
5775
+ }
5776
+ const gates = [];
5777
+ if (/tsc|typecheck|타입\s*체크/i.test(text)) gates.push("typecheck");
5778
+ if (/테스트|test|회귀|vitest/i.test(text)) gates.push("test");
5779
+ if (/빌드|build/i.test(text)) gates.push("build");
5780
+ if (/시크릿|secret|secure|누출|보안\s*스캔/i.test(text)) gates.push("secure");
5781
+ return gates;
5782
+ }
5783
+ function assessFreshness(report, nowMs) {
5784
+ const generatedAt = report?.generatedAt ?? null;
5785
+ const parsed = generatedAt ? Date.parse(generatedAt) : NaN;
5786
+ if (!Number.isFinite(parsed)) {
5787
+ return { generatedAt, ageMs: null, stale: true, confirmed: false, note: "\uC0DD\uC131\uC2DC\uAC01 \uBD88\uBA85 \u2014 \uC2E0\uC120\uB3C4 \uBBF8\uD655\uC778" };
5788
+ }
5789
+ const ageMs = nowMs - parsed;
5790
+ const stale = ageMs > STALE_AGE_MS || ageMs < 0;
5791
+ const mins = Math.round(ageMs / 6e4);
5792
+ const human = mins >= 120 ? `${Math.round(mins / 60)}\uC2DC\uAC04` : `${mins}\uBD84`;
5793
+ return {
5794
+ generatedAt,
5795
+ ageMs,
5796
+ stale,
5797
+ confirmed: true,
5798
+ note: stale ? `\uC99D\uAC70\uAC00 ${human} \uC804 \uC0DD\uC131(\uB610\uB294 \uBBF8\uB798\uC2DC\uAC01) \u2014 \uCF54\uB4DC \uBCC0\uACBD \uC2DC \uBB34\uD6A8, vhk verify \uC7AC\uC2E4\uD589 \uAD8C\uC7A5` : `\uC99D\uAC70 ${human} \uC804 \uC0DD\uC131`
5799
+ };
5800
+ }
5801
+ function crossCheck(checks, goalStatus, report, nowMs) {
5802
+ const suspicions = [];
5803
+ const gaps = [];
5804
+ const gateById = /* @__PURE__ */ new Map();
5805
+ if (report) for (const g of report.gates) gateById.set(g.id, g);
5806
+ const freshness = assessFreshness(report, nowMs);
5807
+ if (goalStatus === "DONE" && report && report.status === "FAIL") {
5808
+ suspicions.push({
5809
+ check: "goal status = DONE",
5810
+ reason: "verify \uC804\uCCB4 \uACB0\uACFC\uAC00 FAIL \u2014 \uC644\uB8CC \uC120\uC5B8\uACFC \uC99D\uAC70\uAC00 \uBAA8\uC21C(\uAC70\uC9D3\uC644\uB8CC \uAC15\uD55C \uC758\uC2EC)."
5811
+ });
5812
+ }
5813
+ const checked = checks.filter((c) => c.checked);
5814
+ let mappedCount = 0;
5815
+ for (const c of checked) {
5816
+ const gates = impliedGates(c.text);
5817
+ if (gates.length === 0) {
5818
+ gaps.push({ check: c.text, note: "\uBBF8\uAC80\uC99D \u2014 \uAC8C\uC774\uD2B8 \uD0A4\uC6CC\uB4DC \uB9E4\uD551 \uBD88\uAC00(\uAE30\uB2A5 \uC644\uB8CC\uC870\uAC74, \uC218\uB3D9 \uD655\uC778 \uD544\uC694)." });
5819
+ continue;
5820
+ }
5821
+ mappedCount++;
5822
+ for (const gid of gates) {
5823
+ const g = gateById.get(gid);
5824
+ if (!report || !g) {
5825
+ suspicions.push({ check: c.text, reason: `${gid} \uAC8C\uC774\uD2B8 \uC99D\uAC70 \uC5C6\uC74C(latest.json \uBD80\uC7AC/\uBBF8\uC2E4\uD589) \u2014 \uCCB4\uD06C\uB428\uC774\uB098 \uB4B7\uBC1B\uCE68 \uBABB \uD568.` });
5826
+ } else if (g.status === "fail") {
5827
+ suspicions.push({ check: c.text, reason: `${gid} \uAC8C\uC774\uD2B8 FAIL(\uC885\uB8CC\uCF54\uB4DC ${g.exitCode ?? "?"}) \u2014 \uCCB4\uD06C\uB428\uACFC \uBAA8\uC21C.` });
5828
+ } else if (g.status === "skip") {
5829
+ suspicions.push({ check: c.text, reason: `${gid} \uAC8C\uC774\uD2B8 skip \u2014 \uAC80\uC99D\uC774 \uBCC0\uACBD\uC744 \uC548 \uAC74\uB4DC\uB838\uC744 \uC218 \uC788\uC74C(\uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC).` });
5830
+ }
5831
+ }
5832
+ }
5833
+ const coverage = checked.length === 0 ? 0 : mappedCount / checked.length;
5834
+ let confidence;
5835
+ if (checked.length === 0 || suspicions.length > 0) {
5836
+ confidence = "low";
5837
+ } else if (gaps.length > 0 || coverage < COVERAGE_MIN || !freshness.confirmed || freshness.stale) {
5838
+ confidence = "medium";
5839
+ } else {
5840
+ confidence = "high";
5841
+ }
5842
+ let reprompt;
5843
+ if (checked.length === 0) {
5844
+ reprompt = "\uCCB4\uD06C\uB41C \uC644\uB8CC\uC870\uAC74\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC644\uB8CC \uC8FC\uC7A5\uC774 \uC5C6\uC5B4 \uC2EC\uBB38\uD560 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4(vacuous).";
5845
+ } else if (suspicions.length > 0) {
5846
+ reprompt = "\uB2E4\uC74C \uC644\uB8CC\uC870\uAC74\uC774 \uC99D\uAC70\uC640 \uBAA8\uC21C\uB418\uAC70\uB098 \uC99D\uAC70\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4:\n" + suspicions.map((s) => ` - ${s.check} \u2192 ${s.reason}`).join("\n") + "\n\uAC01 \uD56D\uBAA9\uC758 \uC2E4\uC81C \uC99D\uAC70(\uAC8C\uC774\uD2B8 \uD1B5\uACFC/\uCD94\uAC00\uB41C \uD14C\uC2A4\uD2B8/\uBCC0\uACBD \uD30C\uC77C)\uB97C \uC81C\uC2DC\uD558\uAC70\uB098, \uCDA9\uC871 \uBABB \uD558\uBA74 done \uC744 \uCCA0\uD68C\uD558\uC138\uC694.";
5847
+ } else if (gaps.length > 0) {
5848
+ reprompt = "\uB2E4\uC74C \uC644\uB8CC\uC870\uAC74\uC740 \uAC8C\uC774\uD2B8 \uC99D\uAC70\uB85C \uC790\uB3D9 \uD655\uC778\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4(\uBBF8\uAC80\uC99D \u2014 \uC218\uB3D9 \uD655\uC778 \uD544\uC694):\n" + gaps.map((g) => ` - ${g.check}`).join("\n");
5849
+ } else {
5850
+ reprompt = "\uCCB4\uD06C\uB41C \uBAA8\uB4E0 \uC644\uB8CC\uC870\uAC74\uC774 \uAC8C\uC774\uD2B8 \uC99D\uAC70\uB85C \uB4B7\uBC1B\uCE68\uB429\uB2C8\uB2E4(\uB2E8, \uBCF4\uC7A5\uC740 \uC544\uB2D8).";
5851
+ }
5852
+ return {
5853
+ confidence,
5854
+ coverage,
5855
+ checkedCount: checked.length,
5856
+ mappedCount,
5857
+ unmappedCount: gaps.length,
5858
+ freshness,
5859
+ suspicions,
5860
+ gaps,
5861
+ disclaimer: REVIEW_DISCLAIMER,
5862
+ reprompt
5863
+ };
5864
+ }
5865
+ function resolveGoal(optId, goals) {
5866
+ let id;
5867
+ if (optId !== void 0) {
5868
+ const n = Number(optId);
5869
+ id = Number.isFinite(n) ? n : null;
5870
+ } else {
5871
+ id = selectActiveId(goals);
5872
+ }
5873
+ if (id === null) return null;
5874
+ return goals.find((g) => g.frontmatter.id === id) ?? null;
5875
+ }
5876
+ var CONFIDENCE_LABEL = {
5877
+ low: chalk31.red.bold("\uB0AE\uC74C (\uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC \uB610\uB294 \uC644\uB8CC \uC8FC\uC7A5 \uC5C6\uC74C)"),
5878
+ medium: chalk31.yellow.bold("\uC911\uAC04 (\uCEE4\uBC84\uB9AC\uC9C0/\uC2E0\uC120\uB3C4 \uBD80\uC871 \u2014 \uC99D\uAC70 \uC5C6\uC74C \u2260 \uD1B5\uACFC)"),
5879
+ high: chalk31.green.bold("\uB192\uC74C (\uC758\uC2EC 0 + \uCEE4\uBC84\uB9AC\uC9C0\xB7\uC2E0\uC120\uB3C4 \uCDA9\uBD84 \u2014 \uB2E8 \uBCF4\uC7A5 \uC544\uB2D8)")
5880
+ };
5881
+ async function review(opts = {}) {
5882
+ if (!ensureNotHardStopped("review")) return;
5883
+ const cwd = process.cwd();
5884
+ const goals = listGoals(GOALS_DIR2);
5885
+ if (goals.length === 0) {
5886
+ console.error(chalk31.yellow(" \u26A0\uFE0F goals/ \uC5D0 goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4. vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
5887
+ process.exitCode = 1;
5888
+ return;
5889
+ }
5890
+ const goal = resolveGoal(opts.id, goals);
5891
+ if (!goal || typeof goal.frontmatter.id !== "number") {
5892
+ console.error(chalk31.red(` \u274C \uB300\uC0C1 goal \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4${opts.id ? ` (--id ${opts.id})` : " (active goal \uC5C6\uC74C)"}.`));
5893
+ process.exitCode = 1;
5894
+ return;
5895
+ }
5896
+ const goalId = goal.frontmatter.id;
5897
+ const goalStatus = goal.frontmatter.status ?? "NOT_STARTED";
5898
+ const checks = parseCompletionChecks(goal.body);
5899
+ if (opts.id === void 0 && goalStatus === "NOT_STARTED") {
5900
+ console.error(chalk31.yellow(` \u26A0\uFE0F active goal ${goalId} \uAC00 NOT_STARTED \u2014 \uC644\uB8CC \uC8FC\uC7A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uAC80\uC99D \uB300\uC0C1\uC740 --id \uB85C \uC9C0\uC815\uD558\uC138\uC694.`));
5901
+ }
5902
+ const jsonPath = join11(cwd, REPORT_PATH_REL);
5903
+ if (!existsSync17(jsonPath)) {
5904
+ console.error(chalk31.yellow(` \u26A0\uFE0F \uC99D\uAC70 \uBD80\uC7AC \u2014 ${REPORT_PATH_REL} \uC5C6\uC74C. review \uB97C \uC911\uB2E8\uD569\uB2C8\uB2E4(\uC0C8 \uC99D\uAC70 \uC548 \uB9CC\uB4E6).`));
5905
+ printNextStep({
5906
+ message: "\uC99D\uAC70(latest.json)\uAC00 \uC788\uC5B4\uC57C review \uAC00 \uAD50\uCC28\uAC80\uC99D\uD569\uB2C8\uB2E4:",
5907
+ command: "vhk verify",
5908
+ cursorHint: "\uBA3C\uC800 \uAC80\uC99D \uB3CC\uB824\uC918"
5909
+ });
5910
+ process.exitCode = 1;
5911
+ return;
5912
+ }
5913
+ let report;
5914
+ try {
5915
+ report = readJsonFile(jsonPath);
5916
+ } catch {
5917
+ console.error(chalk31.red(` \u274C ${REPORT_PATH_REL} \uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4(\uC190\uC0C1). vhk verify \uB85C \uC7AC\uC0DD\uC131\uD558\uC138\uC694.`));
5918
+ process.exitCode = 1;
5919
+ return;
5920
+ }
5921
+ const analysis = crossCheck(checks, goalStatus, report, Date.now());
5922
+ const result = {
5923
+ ...analysis,
5924
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
5925
+ goalId,
5926
+ goalStatus,
5927
+ reportStatus: report.status
5928
+ };
5929
+ console.log(chalk31.bold(`
5930
+ \u{1F52C} \uC801\uB300\uC801 \uC790\uAE30\uAC80\uC99D (review) \u2014 Goal ${goalId}`));
5931
+ console.log(chalk31.gray("\u2500".repeat(44)));
5932
+ console.log(
5933
+ chalk31.dim(
5934
+ ` goal status: ${goalStatus} \xB7 verify: ${report.status} \xB7 \uCCB4\uD06C\uB41C \uC644\uB8CC\uC870\uAC74 ${result.checkedCount}\uAC1C (\uB9E4\uD551 ${result.mappedCount} / \uBBF8\uAC80\uC99D ${result.unmappedCount}, coverage ${result.coverage * 100 | 0}%)`
5935
+ )
5936
+ );
5937
+ console.log(chalk31.dim(` \uC99D\uAC70 \uC2E0\uC120\uB3C4: ${result.freshness.note}`));
5938
+ if (result.checkedCount === 0) {
5939
+ console.log(chalk31.yellow("\n \u26AA \uC644\uB8CC \uC8FC\uC7A5 \uC5C6\uC74C(\uCCB4\uD06C\uB41C \uC644\uB8CC\uC870\uAC74 0\uAC1C) \u2014 \uC2EC\uBB38\uD560 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4(vacuous)."));
5940
+ }
5941
+ if (result.suspicions.length > 0) {
5942
+ console.log(chalk31.red.bold(`
5943
+ \u{1F6A9} \uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC ${result.suspicions.length}\uAC74`));
5944
+ for (const s of result.suspicions) console.log(chalk31.red(` \u2717 ${s.check}
5945
+ \u21B3 ${s.reason}`));
5946
+ }
5947
+ if (result.gaps.length > 0) {
5948
+ console.log(chalk31.yellow.bold(`
5949
+ \u26A0\uFE0F \uBBF8\uAC80\uC99D(unmapped) ${result.gaps.length}\uAC74 \u2014 \uAC8C\uC774\uD2B8\uB85C \uC790\uB3D9 \uD655\uC778 \uBD88\uAC00`));
5950
+ for (const g of result.gaps) console.log(chalk31.yellow(` ? ${g.check}`));
5951
+ }
5952
+ if (result.checkedCount > 0 && result.suspicions.length === 0 && result.gaps.length === 0) {
5953
+ console.log(chalk31.green("\n \u2713 \uCCB4\uD06C\uB41C \uC644\uB8CC\uC870\uAC74\uC774 \uBAA8\uB450 \uAC8C\uC774\uD2B8 \uC99D\uAC70\uB85C \uB4B7\uBC1B\uCE68\uB428."));
5954
+ }
5955
+ console.log(`
5956
+ \uC2E0\uB8B0\uB3C4: ${CONFIDENCE_LABEL[result.confidence]}`);
5957
+ console.log(chalk31.yellow(`
5958
+ ${result.disclaimer}`));
5959
+ let mergeOk = false;
5960
+ try {
5961
+ const merged = { ...report, review: result };
5962
+ writeFileSync12(jsonPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
5963
+ mergeOk = true;
5964
+ console.log(chalk31.dim(` \u{1F4C4} \uD310\uC815 \uBCD1\uD569: ${REPORT_PATH_REL} (review \uC139\uC158)`));
5965
+ } catch (e) {
5966
+ console.error(chalk31.red(` \u274C review \uD310\uC815 \uAE30\uB85D \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`));
5967
+ }
5968
+ if (!mergeOk) {
5969
+ process.exitCode = 1;
5970
+ printNextStep({
5971
+ message: "review \uD310\uC815 \uAE30\uB85D \uC2E4\uD328(exit 1) \u2014 .vhk/reports \uC4F0\uAE30 \uAD8C\uD55C \uD655\uC778 \uD6C4 \uC7AC\uC2E4\uD589:",
5972
+ command: "vhk review",
5973
+ cursorHint: "\uAD8C\uD55C \uD655\uC778 \uD6C4 \uB2E4\uC2DC \uAC80\uD1A0\uD574\uC918"
5974
+ });
5975
+ return;
5976
+ }
5977
+ const vacuous = result.checkedCount === 0;
5978
+ const cleanHigh = result.suspicions.length === 0 && result.gaps.length === 0 && result.confidence === "high";
5979
+ process.exitCode = vacuous || cleanHigh ? 0 : 1;
5980
+ if (vacuous) {
5981
+ printNextStep({
5982
+ message: "\uC644\uB8CC \uC8FC\uC7A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC644\uB8CC\uC870\uAC74\uC744 \uCC44\uC6B4 \uB4A4 \uAC80\uC99D\uD558\uC138\uC694:",
5983
+ command: "vhk verify",
5984
+ cursorHint: "\uC644\uB8CC\uC870\uAC74 \uCC44\uC6CC\uC918"
5985
+ });
5986
+ } else if (result.suspicions.length > 0) {
5987
+ console.log(chalk31.dim("\n AI \uC7AC\uC9C8\uBB38 \uD504\uB86C\uD504\uD2B8:"));
5988
+ console.log(chalk31.cyan(result.reprompt.split("\n").map((l) => ` ${l}`).join("\n")));
5989
+ printNextStep({
5990
+ message: "\uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC\uC73C\uB85C \uC2E4\uD328(exit 1) \u2014 \uC99D\uAC70 \uBCF4\uAC15 \uD6C4 \uB2E4\uC2DC \uAC80\uC99D\uD558\uC138\uC694:",
5991
+ command: "vhk verify",
5992
+ cursorHint: "\uC758\uC2EC \uD56D\uBAA9 \uC99D\uAC70 \uBCF4\uAC15\uD574\uC918",
5993
+ alternative: result.suspicions[0].reason
5994
+ });
5995
+ } else if (cleanHigh) {
5996
+ printNextStep({
5997
+ message: "\uC2EC\uBB38 \uD1B5\uACFC(\uC2E0\uB8B0\uB3C4 \uB192\uC74C, \uBCF4\uC7A5 \uC544\uB2D8). \uC644\uB8CC \uCC98\uB9AC\uD558\uB824\uBA74:",
5998
+ command: `vhk goal done --id ${goalId}`,
5999
+ cursorHint: "goal \uC644\uB8CC \uCC98\uB9AC\uD574\uC918"
6000
+ });
6001
+ } else {
6002
+ console.log(chalk31.dim("\n AI \uC7AC\uC9C8\uBB38 \uD504\uB86C\uD504\uD2B8:"));
6003
+ console.log(chalk31.cyan(result.reprompt.split("\n").map((l) => ` ${l}`).join("\n")));
6004
+ printNextStep({
6005
+ message: `\uC99D\uAC70 \uBD88\uCDA9\uBD84(\uC2E0\uB8B0\uB3C4 ${result.confidence}) \u2014 goal done \uAE08\uC9C0. \uCEE4\uBC84\uB9AC\uC9C0/\uC2E0\uC120\uB3C4 \uBCF4\uAC15 \uD6C4 \uC7AC\uAC80\uC99D:`,
6006
+ command: "vhk verify",
6007
+ cursorHint: "\uC99D\uAC70 \uBCF4\uAC15 \uD6C4 \uB2E4\uC2DC \uAC80\uC99D\uD574\uC918"
6008
+ });
6009
+ }
6010
+ }
6011
+
5567
6012
  // src/lib/risk-policy.ts
5568
6013
  var HIGH_RISK_ACTIONS = [
5569
6014
  "undo",
@@ -5729,6 +6174,8 @@ async function dispatchNlpRoute(route, input) {
5729
6174
  return mode();
5730
6175
  case "verify":
5731
6176
  return verify();
6177
+ case "review":
6178
+ return review();
5732
6179
  }
5733
6180
  }
5734
6181
  var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
@@ -5742,14 +6189,14 @@ function requiresConfirmation(route) {
5742
6189
  async function runNaturalLanguageRoute(input) {
5743
6190
  const route = routeNaturalLanguage(input);
5744
6191
  if (!route) {
5745
- console.log(chalk31.yellow(`
6192
+ console.log(chalk32.yellow(`
5746
6193
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
5747
6194
  `));
5748
6195
  return;
5749
6196
  }
5750
6197
  console.log("");
5751
- console.log(chalk31.cyan(` \u{1F4AC} "${input}"`));
5752
- console.log(chalk31.cyan(` \u2192 ${route.explanation}`));
6198
+ console.log(chalk32.cyan(` \u{1F4AC} "${input}"`));
6199
+ console.log(chalk32.cyan(` \u2192 ${route.explanation}`));
5753
6200
  if (requiresConfirmation(route)) {
5754
6201
  const { confirm } = await inquirer12.prompt([{
5755
6202
  type: "confirm",
@@ -5758,7 +6205,7 @@ async function runNaturalLanguageRoute(input) {
5758
6205
  default: true
5759
6206
  }]);
5760
6207
  if (!confirm) {
5761
- console.log(chalk31.dim(` ${ko.nlp.menuHint}`));
6208
+ console.log(chalk32.dim(` ${ko.nlp.menuHint}`));
5762
6209
  return;
5763
6210
  }
5764
6211
  }
@@ -5767,7 +6214,7 @@ async function runNaturalLanguageRoute(input) {
5767
6214
  if (riskAction) {
5768
6215
  await runGuarded(
5769
6216
  riskAction,
5770
- { channel: "nl", approved: false, log: (m) => console.log(chalk31.yellow(` ${m}`)) },
6217
+ { channel: "nl", approved: false, log: (m) => console.log(chalk32.yellow(` ${m}`)) },
5771
6218
  () => dispatchNlpRoute(route, input)
5772
6219
  );
5773
6220
  return;
@@ -5776,77 +6223,77 @@ async function runNaturalLanguageRoute(input) {
5776
6223
  }
5777
6224
 
5778
6225
  // src/commands/agent.ts
5779
- import chalk32 from "chalk";
6226
+ import chalk33 from "chalk";
5780
6227
  function activeGoalId() {
5781
6228
  const goals = listGoals("goals");
5782
6229
  const id = selectActiveId(goals);
5783
6230
  return id ?? void 0;
5784
6231
  }
5785
6232
  async function blocker(description) {
5786
- console.log(chalk32.bold(`
6233
+ console.log(chalk33.bold(`
5787
6234
  ${ko.agent.blockerTitle}
5788
6235
  `));
5789
6236
  if (!description || !description.trim()) {
5790
- console.log(chalk32.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5791
- console.log(chalk32.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
6237
+ console.log(chalk33.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
6238
+ console.log(chalk33.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
5792
6239
  process.exitCode = 1;
5793
6240
  return;
5794
6241
  }
5795
6242
  const goalId = activeGoalId();
5796
6243
  const r = appendBlocker(description, goalId);
5797
- console.log(chalk32.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
6244
+ console.log(chalk33.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
5798
6245
  if (r.hardStopTripped) {
5799
- console.log(chalk32.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5800
- console.log(chalk32.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
6246
+ console.log(chalk33.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
6247
+ console.log(chalk33.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
5801
6248
  process.exitCode = 2;
5802
6249
  }
5803
6250
  }
5804
6251
  async function learn(lesson) {
5805
- console.log(chalk32.bold(`
6252
+ console.log(chalk33.bold(`
5806
6253
  ${ko.agent.learnTitle}
5807
6254
  `));
5808
6255
  if (!lesson || !lesson.trim()) {
5809
- console.log(chalk32.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5810
- console.log(chalk32.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
6256
+ console.log(chalk33.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
6257
+ console.log(chalk33.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
5811
6258
  process.exitCode = 1;
5812
6259
  return;
5813
6260
  }
5814
6261
  const goalId = activeGoalId();
5815
6262
  appendLearning(lesson, goalId);
5816
- console.log(chalk32.green(" \u2705 learnings.md append."));
6263
+ console.log(chalk33.green(" \u2705 learnings.md append."));
5817
6264
  console.log(
5818
- chalk32.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
6265
+ chalk33.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
5819
6266
  );
5820
6267
  }
5821
6268
  async function resume(opts = {}) {
5822
- console.log(chalk32.bold(`
6269
+ console.log(chalk33.bold(`
5823
6270
  ${ko.agent.resumeTitle}
5824
6271
  `));
5825
6272
  if (!isHardStopActive()) {
5826
- console.log(chalk32.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
6273
+ console.log(chalk33.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
5827
6274
  return;
5828
6275
  }
5829
6276
  const reason = readHardStopReason();
5830
6277
  if (reason) {
5831
- console.log(chalk32.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5832
- console.log(chalk32.dim(` ${reason.split("\n").join("\n ")}`));
6278
+ console.log(chalk33.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
6279
+ console.log(chalk33.dim(` ${reason.split("\n").join("\n ")}`));
5833
6280
  console.log("");
5834
6281
  }
5835
6282
  if (!opts.confirm) {
5836
6283
  console.log(
5837
- chalk32.red(
6284
+ chalk33.red(
5838
6285
  " \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
5839
6286
  )
5840
6287
  );
5841
- console.log(chalk32.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
6288
+ console.log(chalk33.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
5842
6289
  process.exitCode = 1;
5843
6290
  return;
5844
6291
  }
5845
6292
  const removed = clearHardStop();
5846
6293
  if (removed) {
5847
- console.log(chalk32.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
6294
+ console.log(chalk33.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
5848
6295
  } else {
5849
- console.log(chalk32.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
6296
+ console.log(chalk33.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
5850
6297
  }
5851
6298
  }
5852
6299
 
@@ -5867,7 +6314,7 @@ async function guardCli(action, approved, run) {
5867
6314
  }]);
5868
6315
  return ok;
5869
6316
  },
5870
- log: (m) => console.log(chalk33.yellow(` ${m}`))
6317
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
5871
6318
  },
5872
6319
  run
5873
6320
  );
@@ -5880,7 +6327,7 @@ async function guardCliDefer(action, approved, run) {
5880
6327
  approved,
5881
6328
  // TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
5882
6329
  confirm: async () => !!process.stdout.isTTY,
5883
- log: (m) => console.log(chalk33.yellow(` ${m}`))
6330
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
5884
6331
  },
5885
6332
  run
5886
6333
  );
@@ -5919,6 +6366,7 @@ var KO_ALIASES = {
5919
6366
  memory: "\uAE30\uC5B5",
5920
6367
  brief: "\uBE0C\uB9AC\uD551",
5921
6368
  goal: "\uBAA9\uD45C",
6369
+ review: "\uAC80\uD1A0",
5922
6370
  blocker: "\uBE14\uB85C\uCEE4",
5923
6371
  learn: "\uAD50\uD6C8",
5924
6372
  resume: "\uC7AC\uAC1C"
@@ -6040,9 +6488,12 @@ program.command("context").alias("\uB9E5\uB77D").option("--compact", "\uD1A0\uD0
6040
6488
  program.command("mode [target]").alias("\uBAA8\uB4DC").description("Safety Mode \uC870\uD68C/\uBCC0\uACBD (lite|standard|strict) \u2014 \uC704\uD5D8 \uC791\uC5C5 \uAC00\uB4DC \uAC15\uB3C4").action(async (target) => {
6041
6489
  await mode(target);
6042
6490
  });
6043
- program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").option("--json", "\uB9AC\uD3EC\uD2B8 JSON \uC744 stdout \uC73C\uB85C \uCD9C\uB825 (CI\uC6A9 \u2014 \uACBD\uB85C \uB300\uC2E0)").description("\uAC80\uC99D \uAC8C\uC774\uD2B8(tsc/test/build/secure) \uC2E4\uC81C \uC2E4\uD589 + \uC99D\uAC70 \uAE30\uB85D (.vhk/reports/latest.json)").action(async (opts) => {
6491
+ program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").option("--json", "\uB9AC\uD3EC\uD2B8 JSON \uC744 stdout \uC73C\uB85C \uCD9C\uB825 (CI\uC6A9 \u2014 \uACBD\uB85C \uB300\uC2E0)").option("--report", "latest.json \uC744 \uC0AC\uB78C\uC6A9 \uC815\uC801 HTML(.vhk/reports/latest.html) \uB85C \uB80C\uB354 (\uC678\uBD80 \uC758\uC874 0)").option("--open", "\uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uD6C4 \uAE30\uBCF8 \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uAE30 (\uBE44\uB300\uD654\uD615/CI/MCP \uC790\uB3D9 \uC2A4\uD0B5)").description("\uAC80\uC99D \uAC8C\uC774\uD2B8(tsc/test/build/secure) \uC2E4\uC81C \uC2E4\uD589 + \uC99D\uAC70 \uAE30\uB85D (.vhk/reports/latest.json)").action(async (opts) => {
6044
6492
  await verify(opts);
6045
6493
  });
6494
+ program.command("review").alias("\uAC80\uD1A0").option("--id <id>", "\uB300\uC0C1 goal id (\uC5C6\uC73C\uBA74 active goal)").description("\uC801\uB300\uC801 \uC790\uAE30\uAC80\uC99D \u2014 latest.json \u2194 goal \uC644\uB8CC\uC870\uAC74 \uAD50\uCC28\uAC80\uC99D (\uAC70\uC9D3\uC644\uB8CC \uC758\uC2EC \uD0D0\uC9C0, \uBCF4\uC7A5 \uC544\uB2D8)").action(async (opts) => {
6495
+ await review(opts);
6496
+ });
6046
6497
  program.command("context-show").alias("\uB9E5\uB77D\uBCF4\uAE30").description("\uD604\uC7AC \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uB0B4\uC6A9 \uCD9C\uB825").action(async () => {
6047
6498
  await contextShow();
6048
6499
  });
@@ -6157,9 +6608,9 @@ if (isMainModule) {
6157
6608
  }
6158
6609
  } catch (err) {
6159
6610
  if (isPromptAbortError(err)) {
6160
- console.error(chalk33.yellow("\n \u26A0\uFE0F \uB300\uD654\uD615 \uC785\uB825\uC774 \uCDE8\uC18C/\uC885\uB8CC\uB410\uC2B5\uB2C8\uB2E4. (\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uD574\uB2F9 \uBA85\uB839\uC744 \uC4F8 \uC218 \uC5C6\uC5B4\uC694)"));
6611
+ console.error(chalk34.yellow("\n \u26A0\uFE0F \uB300\uD654\uD615 \uC785\uB825\uC774 \uCDE8\uC18C/\uC885\uB8CC\uB410\uC2B5\uB2C8\uB2E4. (\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uD574\uB2F9 \uBA85\uB839\uC744 \uC4F8 \uC218 \uC5C6\uC5B4\uC694)"));
6161
6612
  } else {
6162
- console.error(chalk33.red(`
6613
+ console.error(chalk34.red(`
6163
6614
  \u274C ${err instanceof Error ? err.message : String(err)}`));
6164
6615
  }
6165
6616
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Vibe Harness Kit — AI 코딩 도구·기기를 바꿔도 규칙·맥락이 따라가는 포터빌리티 CLI (sync: Cursor·Claude·Windsurf·Copilot·Antigravity / cloud 백업)",
5
5
  "bin": {
6
6
  "vhk": "dist/index.js",