@byh3071/vhk 1.7.1 → 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 +315 -31
  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
@@ -5731,6 +5739,276 @@ async function verify(opts = {}) {
5731
5739
  }
5732
5740
  }
5733
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
+
5734
6012
  // src/lib/risk-policy.ts
5735
6013
  var HIGH_RISK_ACTIONS = [
5736
6014
  "undo",
@@ -5896,6 +6174,8 @@ async function dispatchNlpRoute(route, input) {
5896
6174
  return mode();
5897
6175
  case "verify":
5898
6176
  return verify();
6177
+ case "review":
6178
+ return review();
5899
6179
  }
5900
6180
  }
5901
6181
  var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
@@ -5909,14 +6189,14 @@ function requiresConfirmation(route) {
5909
6189
  async function runNaturalLanguageRoute(input) {
5910
6190
  const route = routeNaturalLanguage(input);
5911
6191
  if (!route) {
5912
- console.log(chalk31.yellow(`
6192
+ console.log(chalk32.yellow(`
5913
6193
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
5914
6194
  `));
5915
6195
  return;
5916
6196
  }
5917
6197
  console.log("");
5918
- console.log(chalk31.cyan(` \u{1F4AC} "${input}"`));
5919
- console.log(chalk31.cyan(` \u2192 ${route.explanation}`));
6198
+ console.log(chalk32.cyan(` \u{1F4AC} "${input}"`));
6199
+ console.log(chalk32.cyan(` \u2192 ${route.explanation}`));
5920
6200
  if (requiresConfirmation(route)) {
5921
6201
  const { confirm } = await inquirer12.prompt([{
5922
6202
  type: "confirm",
@@ -5925,7 +6205,7 @@ async function runNaturalLanguageRoute(input) {
5925
6205
  default: true
5926
6206
  }]);
5927
6207
  if (!confirm) {
5928
- console.log(chalk31.dim(` ${ko.nlp.menuHint}`));
6208
+ console.log(chalk32.dim(` ${ko.nlp.menuHint}`));
5929
6209
  return;
5930
6210
  }
5931
6211
  }
@@ -5934,7 +6214,7 @@ async function runNaturalLanguageRoute(input) {
5934
6214
  if (riskAction) {
5935
6215
  await runGuarded(
5936
6216
  riskAction,
5937
- { channel: "nl", approved: false, log: (m) => console.log(chalk31.yellow(` ${m}`)) },
6217
+ { channel: "nl", approved: false, log: (m) => console.log(chalk32.yellow(` ${m}`)) },
5938
6218
  () => dispatchNlpRoute(route, input)
5939
6219
  );
5940
6220
  return;
@@ -5943,77 +6223,77 @@ async function runNaturalLanguageRoute(input) {
5943
6223
  }
5944
6224
 
5945
6225
  // src/commands/agent.ts
5946
- import chalk32 from "chalk";
6226
+ import chalk33 from "chalk";
5947
6227
  function activeGoalId() {
5948
6228
  const goals = listGoals("goals");
5949
6229
  const id = selectActiveId(goals);
5950
6230
  return id ?? void 0;
5951
6231
  }
5952
6232
  async function blocker(description) {
5953
- console.log(chalk32.bold(`
6233
+ console.log(chalk33.bold(`
5954
6234
  ${ko.agent.blockerTitle}
5955
6235
  `));
5956
6236
  if (!description || !description.trim()) {
5957
- console.log(chalk32.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5958
- 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"'));
5959
6239
  process.exitCode = 1;
5960
6240
  return;
5961
6241
  }
5962
6242
  const goalId = activeGoalId();
5963
6243
  const r = appendBlocker(description, goalId);
5964
- 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)`));
5965
6245
  if (r.hardStopTripped) {
5966
- console.log(chalk32.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5967
- 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."));
5968
6248
  process.exitCode = 2;
5969
6249
  }
5970
6250
  }
5971
6251
  async function learn(lesson) {
5972
- console.log(chalk32.bold(`
6252
+ console.log(chalk33.bold(`
5973
6253
  ${ko.agent.learnTitle}
5974
6254
  `));
5975
6255
  if (!lesson || !lesson.trim()) {
5976
- console.log(chalk32.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5977
- 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)"'));
5978
6258
  process.exitCode = 1;
5979
6259
  return;
5980
6260
  }
5981
6261
  const goalId = activeGoalId();
5982
6262
  appendLearning(lesson, goalId);
5983
- console.log(chalk32.green(" \u2705 learnings.md append."));
6263
+ console.log(chalk33.green(" \u2705 learnings.md append."));
5984
6264
  console.log(
5985
- 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.")
5986
6266
  );
5987
6267
  }
5988
6268
  async function resume(opts = {}) {
5989
- console.log(chalk32.bold(`
6269
+ console.log(chalk33.bold(`
5990
6270
  ${ko.agent.resumeTitle}
5991
6271
  `));
5992
6272
  if (!isHardStopActive()) {
5993
- 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."));
5994
6274
  return;
5995
6275
  }
5996
6276
  const reason = readHardStopReason();
5997
6277
  if (reason) {
5998
- console.log(chalk32.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5999
- 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 ")}`));
6000
6280
  console.log("");
6001
6281
  }
6002
6282
  if (!opts.confirm) {
6003
6283
  console.log(
6004
- chalk32.red(
6284
+ chalk33.red(
6005
6285
  " \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
6006
6286
  )
6007
6287
  );
6008
- 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"));
6009
6289
  process.exitCode = 1;
6010
6290
  return;
6011
6291
  }
6012
6292
  const removed = clearHardStop();
6013
6293
  if (removed) {
6014
- 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."));
6015
6295
  } else {
6016
- 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."));
6017
6297
  }
6018
6298
  }
6019
6299
 
@@ -6034,7 +6314,7 @@ async function guardCli(action, approved, run) {
6034
6314
  }]);
6035
6315
  return ok;
6036
6316
  },
6037
- log: (m) => console.log(chalk33.yellow(` ${m}`))
6317
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
6038
6318
  },
6039
6319
  run
6040
6320
  );
@@ -6047,7 +6327,7 @@ async function guardCliDefer(action, approved, run) {
6047
6327
  approved,
6048
6328
  // TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
6049
6329
  confirm: async () => !!process.stdout.isTTY,
6050
- log: (m) => console.log(chalk33.yellow(` ${m}`))
6330
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
6051
6331
  },
6052
6332
  run
6053
6333
  );
@@ -6086,6 +6366,7 @@ var KO_ALIASES = {
6086
6366
  memory: "\uAE30\uC5B5",
6087
6367
  brief: "\uBE0C\uB9AC\uD551",
6088
6368
  goal: "\uBAA9\uD45C",
6369
+ review: "\uAC80\uD1A0",
6089
6370
  blocker: "\uBE14\uB85C\uCEE4",
6090
6371
  learn: "\uAD50\uD6C8",
6091
6372
  resume: "\uC7AC\uAC1C"
@@ -6210,6 +6491,9 @@ program.command("mode [target]").alias("\uBAA8\uB4DC").description("Safety Mode
6210
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) => {
6211
6492
  await verify(opts);
6212
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
+ });
6213
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 () => {
6214
6498
  await contextShow();
6215
6499
  });
@@ -6324,9 +6608,9 @@ if (isMainModule) {
6324
6608
  }
6325
6609
  } catch (err) {
6326
6610
  if (isPromptAbortError(err)) {
6327
- 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)"));
6328
6612
  } else {
6329
- console.error(chalk33.red(`
6613
+ console.error(chalk34.red(`
6330
6614
  \u274C ${err instanceof Error ? err.message : String(err)}`));
6331
6615
  }
6332
6616
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "1.7.1",
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",