@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.
- package/dist/index.js +315 -31
- 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
|
|
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
|
|
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(
|
|
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(
|
|
5919
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
6233
|
+
console.log(chalk33.bold(`
|
|
5954
6234
|
${ko.agent.blockerTitle}
|
|
5955
6235
|
`));
|
|
5956
6236
|
if (!description || !description.trim()) {
|
|
5957
|
-
console.log(
|
|
5958
|
-
console.log(
|
|
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(
|
|
6244
|
+
console.log(chalk33.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
5965
6245
|
if (r.hardStopTripped) {
|
|
5966
|
-
console.log(
|
|
5967
|
-
console.log(
|
|
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(
|
|
6252
|
+
console.log(chalk33.bold(`
|
|
5973
6253
|
${ko.agent.learnTitle}
|
|
5974
6254
|
`));
|
|
5975
6255
|
if (!lesson || !lesson.trim()) {
|
|
5976
|
-
console.log(
|
|
5977
|
-
console.log(
|
|
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(
|
|
6263
|
+
console.log(chalk33.green(" \u2705 learnings.md append."));
|
|
5984
6264
|
console.log(
|
|
5985
|
-
|
|
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(
|
|
6269
|
+
console.log(chalk33.bold(`
|
|
5990
6270
|
${ko.agent.resumeTitle}
|
|
5991
6271
|
`));
|
|
5992
6272
|
if (!isHardStopActive()) {
|
|
5993
|
-
console.log(
|
|
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(
|
|
5999
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
6294
|
+
console.log(chalk33.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
6015
6295
|
} else {
|
|
6016
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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