@vibecodetown/mcp-server 1.4.0 → 1.5.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.
@@ -1,7 +1,7 @@
1
1
  export declare const TOOL_GROUPS: {
2
2
  readonly mvp: readonly ["vibe_pm.briefing", "vibe_pm.get_decision", "vibe_pm.submit_decision", "vibe_pm.create_work_order", "vibe_pm.inspect_code", "vibe_pm.status", "vibe_pm.doctor", "vibe_pm.ingress"];
3
- readonly core: readonly ["vibe_pm.briefing", "vibe_pm.get_decision", "vibe_pm.submit_decision", "vibe_pm.create_work_order", "vibe_pm.inspect_code", "vibe_pm.status", "vibe_pm.doctor", "vibe_pm.ingress"];
4
- readonly ops: readonly ["vibe_pm.init_docs", "vibe_pm.ui_manifest", "vibe_pm.ui_resource", "vibe_pm.scaffold", "vibe_pm.workspace_recipe", "vibe_pm.setup", "vibe_pm.block_search"];
3
+ readonly core: readonly ["vibe_pm.briefing", "vibe_pm.get_decision", "vibe_pm.submit_decision", "vibe_pm.create_work_order", "vibe_pm.inspect_code", "vibe_pm.status", "vibe_pm.doctor", "vibe_pm.ingress", "vibe_pm.finalize_work", "vibe_pm.undo_last_task"];
4
+ readonly ops: readonly ["vibe_pm.init_docs", "vibe_pm.ui_manifest", "vibe_pm.ui_resource", "vibe_pm.scaffold", "vibe_pm.workspace_recipe", "vibe_pm.setup", "vibe_pm.block_search", "vibe_pm.skills", "vibe_pm.suggest_roots", "vibe_pm.check_resources", "vibe_pm.check_dormant", "vibe_pm.repair_plan", "vibe_pm.branch_pivot"];
5
5
  readonly bridge: readonly [];
6
6
  readonly memory: readonly ["vibe_pm.memory", "vibe_pm.memory_status", "vibe_pm.memory_sync", "vibe_pm.memory_retrieve"];
7
7
  readonly research: readonly [];
@@ -26,8 +26,10 @@ export const TOOL_GROUPS = {
26
26
  'vibe_pm.status',
27
27
  'vibe_pm.doctor',
28
28
  'vibe_pm.ingress',
29
+ 'vibe_pm.finalize_work', // 작업 마무리 + 커밋
30
+ 'vibe_pm.undo_last_task', // 마지막 작업 되돌리기
29
31
  ],
30
- // Ops: 설치/운영 + UI 도구 + 블록 검색 (AS-BUILT: init_docs + ui_manifest/ui_resource + scaffold + setup + block_search)
32
+ // Ops: 설치/운영 + UI 도구 + 블록 검색 + 진단/관리 도구
31
33
  ops: [
32
34
  'vibe_pm.init_docs',
33
35
  'vibe_pm.ui_manifest',
@@ -36,6 +38,12 @@ export const TOOL_GROUPS = {
36
38
  'vibe_pm.workspace_recipe',
37
39
  'vibe_pm.setup',
38
40
  'vibe_pm.block_search', // P38: DB FTS 블록 검색
41
+ 'vibe_pm.skills', // 스킬팩 조회
42
+ 'vibe_pm.suggest_roots', // 프로젝트 루트 탐색
43
+ 'vibe_pm.check_resources', // 시스템 리소스 점검
44
+ 'vibe_pm.check_dormant', // 프로젝트 비활성 감지
45
+ 'vibe_pm.repair_plan', // 복구 계획 생성
46
+ 'vibe_pm.branch_pivot', // 브랜치 전환 + 스냅샷
39
47
  ],
40
48
  // Bridge: 예약(AS-BUILT 미등록)
41
49
  bridge: [],
@@ -199,6 +199,25 @@ export async function registerAllTools(server, opts = {}) {
199
199
  if (registerIfEnabled(server, 'vibe_pm.block_snapshot', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.block_snapshot'], pm.blockSnapshotInputSchema.shape, async (input) => pm.vibePmBlockSnapshot(pm.blockSnapshotInputSchema.parse(input))))
200
200
  registeredCount++;
201
201
  // ============================================================
202
+ // Sprint 048: Gap Closure tools (skills, suggest_roots, check_resources, check_dormant, repair_plan, finalize_work, undo_last_task, branch_pivot)
203
+ // ============================================================
204
+ if (registerIfEnabled(server, 'vibe_pm.skills', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.skills'], pm.skillsInputSchema.shape, async (input) => pm.vibePmSkills(pm.skillsInputSchema.parse(input))))
205
+ registeredCount++;
206
+ if (registerIfEnabled(server, 'vibe_pm.suggest_roots', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.suggest_roots'], pm.suggestRootsInputSchema.shape, async (input) => pm.vibePmSuggestRoots(pm.suggestRootsInputSchema.parse(input))))
207
+ registeredCount++;
208
+ if (registerIfEnabled(server, 'vibe_pm.check_resources', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.check_resources'], pm.checkResourcesInputSchema.shape, async (input) => pm.vibePmCheckResources(pm.checkResourcesInputSchema.parse(input))))
209
+ registeredCount++;
210
+ if (registerIfEnabled(server, 'vibe_pm.check_dormant', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.check_dormant'], pm.checkDormantInputSchema.shape, async (input) => pm.vibePmCheckDormant(pm.checkDormantInputSchema.parse(input))))
211
+ registeredCount++;
212
+ if (registerIfEnabled(server, 'vibe_pm.repair_plan', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.repair_plan'], pm.repairPlanInputSchema.shape, async (input) => pm.vibePmRepairPlan(pm.repairPlanInputSchema.parse(input))))
213
+ registeredCount++;
214
+ if (registerIfEnabled(server, 'vibe_pm.finalize_work', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.finalize_work'], pm.finalizeWorkInputSchema.shape, async (input) => pm.vibePmFinalizeWork(pm.finalizeWorkInputSchema.parse(input))))
215
+ registeredCount++;
216
+ if (registerIfEnabled(server, 'vibe_pm.undo_last_task', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.undo_last_task'], pm.undoLastTaskInputSchema.shape, async (input) => pm.vibePmUndoLastTask(pm.undoLastTaskInputSchema.parse(input))))
217
+ registeredCount++;
218
+ if (registerIfEnabled(server, 'vibe_pm.branch_pivot', VIBE_PM_TOOL_DESCRIPTIONS['vibe_pm.branch_pivot'], pm.branchPivotInputSchema.shape, async (input) => pm.vibePmBranchPivot(pm.branchPivotInputSchema.parse(input))))
219
+ registeredCount++;
220
+ // ============================================================
202
221
  // Human Surface meta-tools (gated by VIBE_PROFILE=human)
203
222
  // ============================================================
204
223
  let humanCount = 0;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export declare const branchPivotInputSchema: z.ZodObject<{
3
+ target_branch: z.ZodString;
4
+ save_snapshot: z.ZodDefault<z.ZodBoolean>;
5
+ restore_snapshot: z.ZodDefault<z.ZodBoolean>;
6
+ workspace_root: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ target_branch: string;
9
+ save_snapshot: boolean;
10
+ restore_snapshot: boolean;
11
+ workspace_root?: string | undefined;
12
+ }, {
13
+ target_branch: string;
14
+ workspace_root?: string | undefined;
15
+ save_snapshot?: boolean | undefined;
16
+ restore_snapshot?: boolean | undefined;
17
+ }>;
18
+ export type BranchPivotInput = z.infer<typeof branchPivotInputSchema>;
19
+ export declare function branchPivot(input: BranchPivotInput): Promise<unknown>;
@@ -0,0 +1,85 @@
1
+ // vibe_pm.branch_pivot — Switch branches with optional .vibe/ snapshot save/restore
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ export const branchPivotInputSchema = z.object({
7
+ target_branch: z.string(),
8
+ save_snapshot: z.boolean().default(false),
9
+ restore_snapshot: z.boolean().default(false),
10
+ workspace_root: z.string().optional(),
11
+ });
12
+ export async function branchPivot(input) {
13
+ const workspaceRoot = input.workspace_root ?? process.cwd();
14
+ const vibePath = path.join(workspaceRoot, '.vibe');
15
+ const snapshotDir = path.join(vibePath, 'snapshots');
16
+ // Get current branch
17
+ let currentBranch = '';
18
+ try {
19
+ currentBranch = execSync('git branch --show-current', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim();
20
+ }
21
+ catch {
22
+ return { error: 'git 브랜치를 확인할 수 없습니다.' };
23
+ }
24
+ // Save .vibe/ snapshot before switching
25
+ if (input.save_snapshot) {
26
+ try {
27
+ const snapshotPath = path.join(snapshotDir, `branch_${currentBranch}`);
28
+ fs.mkdirSync(snapshotPath, { recursive: true });
29
+ // Copy key state files
30
+ const stateFiles = ['decision_log.v1.jsonl', 'fsm_state.json', 'config.json', 'dev_log.jsonl'];
31
+ for (const file of stateFiles) {
32
+ const src = path.join(vibePath, file);
33
+ if (fs.existsSync(src)) {
34
+ fs.copyFileSync(src, path.join(snapshotPath, file));
35
+ }
36
+ }
37
+ // Save metadata
38
+ fs.writeFileSync(path.join(snapshotPath, '_meta.json'), JSON.stringify({ branch: currentBranch, saved_at: new Date().toISOString() }));
39
+ }
40
+ catch (err) {
41
+ return { error: `스냅샷 저장 실패: ${err instanceof Error ? err.message : String(err)}` };
42
+ }
43
+ }
44
+ // Switch branch
45
+ try {
46
+ execSync(`git checkout ${input.target_branch}`, { cwd: workspaceRoot, timeout: 10000 });
47
+ }
48
+ catch (err) {
49
+ return { error: `브랜치 전환 실패: ${err instanceof Error ? err.message : String(err)}` };
50
+ }
51
+ // Restore .vibe/ snapshot for target branch
52
+ if (input.restore_snapshot) {
53
+ const snapshotPath = path.join(snapshotDir, `branch_${input.target_branch}`);
54
+ if (fs.existsSync(snapshotPath)) {
55
+ try {
56
+ const files = fs.readdirSync(snapshotPath).filter(f => !f.startsWith('_'));
57
+ for (const file of files) {
58
+ fs.copyFileSync(path.join(snapshotPath, file), path.join(vibePath, file));
59
+ }
60
+ }
61
+ catch {
62
+ // fail-safe: continue without restore
63
+ }
64
+ return {
65
+ status: 'switched_and_restored',
66
+ from_branch: currentBranch,
67
+ to_branch: input.target_branch,
68
+ snapshot_restored: true,
69
+ };
70
+ }
71
+ return {
72
+ status: 'switched',
73
+ from_branch: currentBranch,
74
+ to_branch: input.target_branch,
75
+ snapshot_restored: false,
76
+ warning: `브랜치 '${input.target_branch}'에 대한 스냅샷이 없습니다.`,
77
+ };
78
+ }
79
+ return {
80
+ status: 'switched',
81
+ from_branch: currentBranch,
82
+ to_branch: input.target_branch,
83
+ snapshot_saved: input.save_snapshot,
84
+ };
85
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const checkDormantInputSchema: z.ZodObject<{
3
+ workspace_root: z.ZodOptional<z.ZodString>;
4
+ threshold_days: z.ZodDefault<z.ZodNumber>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ threshold_days: number;
7
+ workspace_root?: string | undefined;
8
+ }, {
9
+ workspace_root?: string | undefined;
10
+ threshold_days?: number | undefined;
11
+ }>;
12
+ export type CheckDormantInput = z.infer<typeof checkDormantInputSchema>;
13
+ export declare function checkDormant(input: CheckDormantInput): Promise<unknown>;
@@ -0,0 +1,67 @@
1
+ // vibe_pm.check_dormant — Detect dormant/inactive projects
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ export const checkDormantInputSchema = z.object({
7
+ workspace_root: z.string().optional(),
8
+ threshold_days: z.number().int().min(1).default(7),
9
+ });
10
+ export async function checkDormant(input) {
11
+ const workspaceRoot = input.workspace_root ?? process.cwd();
12
+ const thresholdDays = input.threshold_days ?? 7;
13
+ const now = Date.now();
14
+ const msPerDay = 86400000;
15
+ let latestMs = 0;
16
+ let latestSource = 'none';
17
+ // Check .vibe/ directory mtime
18
+ try {
19
+ const vibePath = path.join(workspaceRoot, '.vibe');
20
+ const stat = fs.statSync(vibePath);
21
+ if (stat.mtimeMs > latestMs) {
22
+ latestMs = stat.mtimeMs;
23
+ latestSource = '.vibe directory';
24
+ }
25
+ }
26
+ catch { /* no .vibe */ }
27
+ // Check decision_log.v1.jsonl last line timestamp
28
+ try {
29
+ const logPath = path.join(workspaceRoot, '.vibe', 'decision_log.v1.jsonl');
30
+ const content = fs.readFileSync(logPath, 'utf8');
31
+ const lines = content.trim().split('\n').filter(Boolean);
32
+ if (lines.length > 0) {
33
+ const last = JSON.parse(lines[lines.length - 1]);
34
+ const ts = new Date(last.timestamp ?? last.ts ?? 0).getTime();
35
+ if (ts > latestMs) {
36
+ latestMs = ts;
37
+ latestSource = 'decision_log';
38
+ }
39
+ }
40
+ }
41
+ catch { /* no log */ }
42
+ // Check git log
43
+ try {
44
+ const gitDate = execSync('git log -1 --format=%ci', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim();
45
+ const gitMs = new Date(gitDate).getTime();
46
+ if (gitMs > latestMs) {
47
+ latestMs = gitMs;
48
+ latestSource = 'git commit';
49
+ }
50
+ }
51
+ catch { /* no git */ }
52
+ const daysInactive = latestMs > 0 ? Math.floor((now - latestMs) / msPerDay) : -1;
53
+ const isDormant = daysInactive < 0 || daysInactive >= thresholdDays;
54
+ let suggestedAction = '정상 활동 중';
55
+ if (isDormant) {
56
+ suggestedAction = daysInactive >= 30
57
+ ? '장기 미사용 프로젝트. 아카이브 또는 재활성화를 검토하세요.'
58
+ : `${daysInactive}일 비활성. vibe_pm.ingress로 컨텍스트를 재스캔하세요.`;
59
+ }
60
+ return {
61
+ is_dormant: isDormant,
62
+ days_inactive: daysInactive,
63
+ last_activity_source: latestSource,
64
+ suggested_action: suggestedAction,
65
+ threshold_days: thresholdDays,
66
+ };
67
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ export declare const checkResourcesInputSchema: z.ZodObject<{
3
+ workspace_root: z.ZodOptional<z.ZodString>;
4
+ }, "strip", z.ZodTypeAny, {
5
+ workspace_root?: string | undefined;
6
+ }, {
7
+ workspace_root?: string | undefined;
8
+ }>;
9
+ export type CheckResourcesInput = z.infer<typeof checkResourcesInputSchema>;
10
+ export declare function checkResources(input: CheckResourcesInput): Promise<unknown>;
@@ -0,0 +1,30 @@
1
+ // vibe_pm.check_resources — System resource check (CPU, memory, disk)
2
+ import { z } from 'zod';
3
+ import * as os from 'node:os';
4
+ import * as fs from 'node:fs';
5
+ export const checkResourcesInputSchema = z.object({
6
+ workspace_root: z.string().optional(),
7
+ });
8
+ export async function checkResources(input) {
9
+ const workspaceRoot = input.workspace_root ?? process.cwd();
10
+ const GB = 1024 * 1024 * 1024;
11
+ let diskTotal = 0;
12
+ let diskFree = 0;
13
+ try {
14
+ const stat = fs.statfsSync(workspaceRoot);
15
+ diskTotal = (stat.blocks * stat.bsize) / GB;
16
+ diskFree = (stat.bavail * stat.bsize) / GB;
17
+ }
18
+ catch {
19
+ // statfs not available on all platforms
20
+ }
21
+ return {
22
+ cpu_cores: os.cpus().length,
23
+ cpu_load_1m: os.loadavg()[0],
24
+ memory_total_gb: +(os.totalmem() / GB).toFixed(2),
25
+ memory_free_gb: +(os.freemem() / GB).toFixed(2),
26
+ disk_total_gb: +diskTotal.toFixed(2),
27
+ disk_free_gb: +diskFree.toFixed(2),
28
+ workspace_root: workspaceRoot,
29
+ };
30
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export declare const finalizeWorkInputSchema: z.ZodObject<{
3
+ summary: z.ZodOptional<z.ZodString>;
4
+ commit_type: z.ZodDefault<z.ZodEnum<["feat", "fix", "refactor", "chore", "docs", "test"]>>;
5
+ confirm: z.ZodDefault<z.ZodBoolean>;
6
+ workspace_root: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ commit_type: "fix" | "docs" | "test" | "feat" | "refactor" | "chore";
9
+ confirm: boolean;
10
+ workspace_root?: string | undefined;
11
+ summary?: string | undefined;
12
+ }, {
13
+ workspace_root?: string | undefined;
14
+ summary?: string | undefined;
15
+ commit_type?: "fix" | "docs" | "test" | "feat" | "refactor" | "chore" | undefined;
16
+ confirm?: boolean | undefined;
17
+ }>;
18
+ export type FinalizeWorkInput = z.infer<typeof finalizeWorkInputSchema>;
19
+ export declare function finalizeWork(input: FinalizeWorkInput): Promise<unknown>;
@@ -0,0 +1,78 @@
1
+ // vibe_pm.finalize_work — Finalize work session with dev log + commit message
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ export const finalizeWorkInputSchema = z.object({
7
+ summary: z.string().optional(),
8
+ commit_type: z.enum(['feat', 'fix', 'refactor', 'chore', 'docs', 'test']).default('feat'),
9
+ confirm: z.boolean().default(false),
10
+ workspace_root: z.string().optional(),
11
+ });
12
+ export async function finalizeWork(input) {
13
+ const workspaceRoot = input.workspace_root ?? process.cwd();
14
+ const commitType = input.commit_type ?? 'feat';
15
+ // Get changed files from git
16
+ let changedFiles = [];
17
+ let diffSummary = '';
18
+ try {
19
+ changedFiles = execSync('git diff --name-only HEAD', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' })
20
+ .trim().split('\n').filter(Boolean);
21
+ const stagedFiles = execSync('git diff --cached --name-only', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' })
22
+ .trim().split('\n').filter(Boolean);
23
+ changedFiles = [...new Set([...changedFiles, ...stagedFiles])];
24
+ diffSummary = execSync('git diff --stat HEAD', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim();
25
+ }
26
+ catch {
27
+ // no git or no changes
28
+ }
29
+ if (changedFiles.length === 0) {
30
+ return { status: 'no_changes', message: '변경된 파일이 없습니다.' };
31
+ }
32
+ // Infer scope from changed files
33
+ const dirs = changedFiles.map(f => f.split('/')[0]);
34
+ const uniqueDirs = [...new Set(dirs)];
35
+ const scope = uniqueDirs.length === 1 ? uniqueDirs[0] : uniqueDirs.length <= 3 ? uniqueDirs.join(',') : 'multi';
36
+ const summary = input.summary ?? `${changedFiles.length}개 파일 변경`;
37
+ const commitMessage = `${commitType}(${scope}): ${summary}`;
38
+ // Append to dev_log
39
+ try {
40
+ const devLogPath = path.join(workspaceRoot, '.vibe', 'dev_log.jsonl');
41
+ const logEntry = {
42
+ timestamp: new Date().toISOString(),
43
+ action: 'finalize_work',
44
+ summary,
45
+ changed_files: changedFiles.length,
46
+ commit_type: commitType,
47
+ committed: input.confirm,
48
+ };
49
+ fs.mkdirSync(path.dirname(devLogPath), { recursive: true });
50
+ fs.appendFileSync(devLogPath, JSON.stringify(logEntry) + '\n');
51
+ }
52
+ catch {
53
+ // fail-safe
54
+ }
55
+ // Execute commit if confirmed
56
+ if (input.confirm) {
57
+ try {
58
+ execSync('git add -A', { cwd: workspaceRoot, timeout: 10000 });
59
+ execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { cwd: workspaceRoot, timeout: 10000 });
60
+ return {
61
+ status: 'committed',
62
+ commit_message: commitMessage,
63
+ changed_files: changedFiles,
64
+ diff_summary: diffSummary,
65
+ };
66
+ }
67
+ catch (err) {
68
+ return { status: 'commit_failed', error: err instanceof Error ? err.message : String(err) };
69
+ }
70
+ }
71
+ return {
72
+ status: 'dry_run',
73
+ commit_message: commitMessage,
74
+ changed_files: changedFiles,
75
+ diff_summary: diffSummary,
76
+ hint: 'confirm: true를 전달하면 실제로 커밋합니다.',
77
+ };
78
+ }
@@ -603,6 +603,95 @@ export declare function defineVibePmTools(opts?: {
603
603
  until?: string | undefined;
604
604
  retention_days?: number | undefined;
605
605
  }>;
606
+ skillsInputSchema: import("zod").ZodObject<{
607
+ mode: import("zod").ZodDefault<import("zod").ZodEnum<["list", "detail"]>>;
608
+ skill_id: import("zod").ZodOptional<import("zod").ZodString>;
609
+ }, "strip", import("zod").ZodTypeAny, {
610
+ mode: "list" | "detail";
611
+ skill_id?: string | undefined;
612
+ }, {
613
+ mode?: "list" | "detail" | undefined;
614
+ skill_id?: string | undefined;
615
+ }>;
616
+ suggestRootsInputSchema: import("zod").ZodObject<{
617
+ scan_path: import("zod").ZodOptional<import("zod").ZodString>;
618
+ max_depth: import("zod").ZodDefault<import("zod").ZodNumber>;
619
+ }, "strip", import("zod").ZodTypeAny, {
620
+ max_depth: number;
621
+ scan_path?: string | undefined;
622
+ }, {
623
+ scan_path?: string | undefined;
624
+ max_depth?: number | undefined;
625
+ }>;
626
+ checkResourcesInputSchema: import("zod").ZodObject<{
627
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
628
+ }, "strip", import("zod").ZodTypeAny, {
629
+ workspace_root?: string | undefined;
630
+ }, {
631
+ workspace_root?: string | undefined;
632
+ }>;
633
+ checkDormantInputSchema: import("zod").ZodObject<{
634
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
635
+ threshold_days: import("zod").ZodDefault<import("zod").ZodNumber>;
636
+ }, "strip", import("zod").ZodTypeAny, {
637
+ threshold_days: number;
638
+ workspace_root?: string | undefined;
639
+ }, {
640
+ workspace_root?: string | undefined;
641
+ threshold_days?: number | undefined;
642
+ }>;
643
+ repairPlanInputSchema: import("zod").ZodObject<{
644
+ run_id: import("zod").ZodOptional<import("zod").ZodString>;
645
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
646
+ }, "strip", import("zod").ZodTypeAny, {
647
+ run_id?: string | undefined;
648
+ workspace_root?: string | undefined;
649
+ }, {
650
+ run_id?: string | undefined;
651
+ workspace_root?: string | undefined;
652
+ }>;
653
+ finalizeWorkInputSchema: import("zod").ZodObject<{
654
+ summary: import("zod").ZodOptional<import("zod").ZodString>;
655
+ commit_type: import("zod").ZodDefault<import("zod").ZodEnum<["feat", "fix", "refactor", "chore", "docs", "test"]>>;
656
+ confirm: import("zod").ZodDefault<import("zod").ZodBoolean>;
657
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
658
+ }, "strip", import("zod").ZodTypeAny, {
659
+ commit_type: "fix" | "docs" | "test" | "feat" | "refactor" | "chore";
660
+ confirm: boolean;
661
+ workspace_root?: string | undefined;
662
+ summary?: string | undefined;
663
+ }, {
664
+ workspace_root?: string | undefined;
665
+ summary?: string | undefined;
666
+ commit_type?: "fix" | "docs" | "test" | "feat" | "refactor" | "chore" | undefined;
667
+ confirm?: boolean | undefined;
668
+ }>;
669
+ undoLastTaskInputSchema: import("zod").ZodObject<{
670
+ confirm: import("zod").ZodDefault<import("zod").ZodBoolean>;
671
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
672
+ }, "strip", import("zod").ZodTypeAny, {
673
+ confirm: boolean;
674
+ workspace_root?: string | undefined;
675
+ }, {
676
+ workspace_root?: string | undefined;
677
+ confirm?: boolean | undefined;
678
+ }>;
679
+ branchPivotInputSchema: import("zod").ZodObject<{
680
+ target_branch: import("zod").ZodString;
681
+ save_snapshot: import("zod").ZodDefault<import("zod").ZodBoolean>;
682
+ restore_snapshot: import("zod").ZodDefault<import("zod").ZodBoolean>;
683
+ workspace_root: import("zod").ZodOptional<import("zod").ZodString>;
684
+ }, "strip", import("zod").ZodTypeAny, {
685
+ target_branch: string;
686
+ save_snapshot: boolean;
687
+ restore_snapshot: boolean;
688
+ workspace_root?: string | undefined;
689
+ }, {
690
+ target_branch: string;
691
+ workspace_root?: string | undefined;
692
+ save_snapshot?: boolean | undefined;
693
+ restore_snapshot?: boolean | undefined;
694
+ }>;
606
695
  vibePmBriefing: (input: {
607
696
  mode: "mvp_fast" | "balanced" | "quality_first";
608
697
  project_brief: string;
@@ -813,6 +902,41 @@ export declare function defineVibePmTools(opts?: {
813
902
  until?: string | undefined;
814
903
  retention_days?: number | undefined;
815
904
  }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
905
+ vibePmSkills: (input: {
906
+ mode: "list" | "detail";
907
+ skill_id?: string | undefined;
908
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
909
+ vibePmSuggestRoots: (input: {
910
+ max_depth: number;
911
+ scan_path?: string | undefined;
912
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
913
+ vibePmCheckResources: (input: {
914
+ workspace_root?: string | undefined;
915
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
916
+ vibePmCheckDormant: (input: {
917
+ threshold_days: number;
918
+ workspace_root?: string | undefined;
919
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
920
+ vibePmRepairPlan: (input: {
921
+ run_id?: string | undefined;
922
+ workspace_root?: string | undefined;
923
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
924
+ vibePmFinalizeWork: (input: {
925
+ commit_type: "fix" | "docs" | "test" | "feat" | "refactor" | "chore";
926
+ confirm: boolean;
927
+ workspace_root?: string | undefined;
928
+ summary?: string | undefined;
929
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
930
+ vibePmUndoLastTask: (input: {
931
+ confirm: boolean;
932
+ workspace_root?: string | undefined;
933
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
934
+ vibePmBranchPivot: (input: {
935
+ target_branch: string;
936
+ save_snapshot: boolean;
937
+ restore_snapshot: boolean;
938
+ workspace_root?: string | undefined;
939
+ }, extra?: unknown) => Promise<import("./tool_handler_factory.js").CallToolResult>;
816
940
  ensureDocsStructure: typeof ensureDocsStructure;
817
941
  };
818
942
  export { VIBE_PM_TOOL_DESCRIPTIONS } from './tool_descriptions.js';
@@ -39,6 +39,14 @@ import { dashboard, dashboardInputSchema, } from './dashboard.js';
39
39
  import { blockSearch, blockSearchInputSchema, } from './block_search.js';
40
40
  import { blockEvents, blockEventsInputSchema, } from './block_events.js';
41
41
  import { blockSnapshot, blockSnapshotInputSchema, } from './block_snapshot.js';
42
+ import { skills, skillsInputSchema } from './skills_tool.js';
43
+ import { suggestRoots, suggestRootsInputSchema } from './suggest_roots.js';
44
+ import { checkResources, checkResourcesInputSchema } from './check_resources.js';
45
+ import { checkDormant, checkDormantInputSchema } from './check_dormant.js';
46
+ import { repairPlan, repairPlanInputSchema } from './repair_plan.js';
47
+ import { finalizeWork, finalizeWorkInputSchema } from './finalize_work.js';
48
+ import { undoLastTask, undoLastTaskInputSchema } from './undo_last_task.js';
49
+ import { branchPivot, branchPivotInputSchema } from './branch_pivot.js';
42
50
  // Extracted modules
43
51
  import { createToolHandler, setKernelDepsForTools, resolveRunDir, toolResult, toStructuredContent, formatStatusSummary, readNestedString, getProjectOrRunId, fsmTransition, } from './tool_handler_factory.js';
44
52
  // ============================================================
@@ -375,6 +383,78 @@ const vibePmBlockSnapshot = createToolHandler({
375
383
  error: { code: 'block_snapshot_failed', recovery: 'mode 또는 snapshot_id를 확인하세요.' },
376
384
  });
377
385
  // ============================================================
386
+ // Skills Tool
387
+ // ============================================================
388
+ const vibePmSkills = createToolHandler({
389
+ toolName: 'vibe_pm.skills',
390
+ inputSchema: skillsInputSchema,
391
+ handler: input => skills(input),
392
+ error: { code: 'skills_failed', recovery: 'mode를 확인하세요 (list/detail).' },
393
+ });
394
+ // ============================================================
395
+ // Suggest Roots Tool
396
+ // ============================================================
397
+ const vibePmSuggestRoots = createToolHandler({
398
+ toolName: 'vibe_pm.suggest_roots',
399
+ inputSchema: suggestRootsInputSchema,
400
+ handler: input => suggestRoots(input),
401
+ error: { code: 'suggest_roots_failed', recovery: 'scan_path를 확인하세요.' },
402
+ });
403
+ // ============================================================
404
+ // Check Resources Tool
405
+ // ============================================================
406
+ const vibePmCheckResources = createToolHandler({
407
+ toolName: 'vibe_pm.check_resources',
408
+ inputSchema: checkResourcesInputSchema,
409
+ handler: input => checkResources(input),
410
+ error: { code: 'check_resources_failed' },
411
+ });
412
+ // ============================================================
413
+ // Check Dormant Tool
414
+ // ============================================================
415
+ const vibePmCheckDormant = createToolHandler({
416
+ toolName: 'vibe_pm.check_dormant',
417
+ inputSchema: checkDormantInputSchema,
418
+ handler: input => checkDormant(input),
419
+ error: { code: 'check_dormant_failed', recovery: 'workspace_root를 확인하세요.' },
420
+ });
421
+ // ============================================================
422
+ // Repair Plan Tool
423
+ // ============================================================
424
+ const vibePmRepairPlan = createToolHandler({
425
+ toolName: 'vibe_pm.repair_plan',
426
+ inputSchema: repairPlanInputSchema,
427
+ handler: input => repairPlan(input),
428
+ error: { code: 'repair_plan_failed', recovery: 'inspect_code를 먼저 실행하세요.' },
429
+ });
430
+ // ============================================================
431
+ // Finalize Work Tool
432
+ // ============================================================
433
+ const vibePmFinalizeWork = createToolHandler({
434
+ toolName: 'vibe_pm.finalize_work',
435
+ inputSchema: finalizeWorkInputSchema,
436
+ handler: input => finalizeWork(input),
437
+ error: { code: 'finalize_work_failed', recovery: 'workspace_root를 확인하세요.' },
438
+ });
439
+ // ============================================================
440
+ // Undo Last Task Tool
441
+ // ============================================================
442
+ const vibePmUndoLastTask = createToolHandler({
443
+ toolName: 'vibe_pm.undo_last_task',
444
+ inputSchema: undoLastTaskInputSchema,
445
+ handler: input => undoLastTask(input),
446
+ error: { code: 'undo_last_task_failed', recovery: 'GO 판정 기록이 있는지 확인하세요.' },
447
+ });
448
+ // ============================================================
449
+ // Branch Pivot Tool
450
+ // ============================================================
451
+ const vibePmBranchPivot = createToolHandler({
452
+ toolName: 'vibe_pm.branch_pivot',
453
+ inputSchema: branchPivotInputSchema,
454
+ handler: input => branchPivot(input),
455
+ error: { code: 'branch_pivot_failed', recovery: 'target_branch를 확인하세요.' },
456
+ });
457
+ // ============================================================
378
458
  // Export
379
459
  // ============================================================
380
460
  export function defineVibePmTools(opts) {
@@ -411,6 +491,14 @@ export function defineVibePmTools(opts) {
411
491
  blockEventsInputSchema,
412
492
  blockSnapshotInputSchema,
413
493
  decisionHistoryInputSchema: DecisionHistoryInputSchema,
494
+ skillsInputSchema,
495
+ suggestRootsInputSchema,
496
+ checkResourcesInputSchema,
497
+ checkDormantInputSchema,
498
+ repairPlanInputSchema,
499
+ finalizeWorkInputSchema,
500
+ undoLastTaskInputSchema,
501
+ branchPivotInputSchema,
414
502
  // Tool handlers
415
503
  vibePmBriefing,
416
504
  vibePmGetDecision,
@@ -440,6 +528,14 @@ export function defineVibePmTools(opts) {
440
528
  vibePmBlockEvents,
441
529
  vibePmBlockSnapshot,
442
530
  vibePmDecisionHistory,
531
+ vibePmSkills,
532
+ vibePmSuggestRoots,
533
+ vibePmCheckResources,
534
+ vibePmCheckDormant,
535
+ vibePmRepairPlan,
536
+ vibePmFinalizeWork,
537
+ vibePmUndoLastTask,
538
+ vibePmBranchPivot,
443
539
  // Docs structure helper
444
540
  ensureDocsStructure,
445
541
  };
@@ -16,7 +16,7 @@ import { appendDecisionLogEvent, readCurrentWorkOrderId, readCurrentDecisionId,
16
16
  import { readLastCheckSync } from '../bitnet-cache.js';
17
17
  import { computePhaseAndMissing } from '../phase.js';
18
18
  import { computeAndPersistStage } from '../stage-computer.js';
19
- import { attachNudge } from '../nudge.js';
19
+ import { attachNudge, buildNudgeContext } from '../nudge.js';
20
20
  import { readScorecard } from '../scorecard/index.js';
21
21
  import { runPreprocess } from '../../../preprocess.js';
22
22
  import { isPolicyEnabled, loadPolicyConfig, analyzeForPolicy, } from '../policy/index.js';
@@ -288,7 +288,16 @@ export async function inspectCode(input, progress, log) {
288
288
  }
289
289
  // Determine review result from signal_level
290
290
  const signalLevel = oneLoopResult.signal_level ?? oneLoopResult.signal ?? 'LEVEL_2';
291
- const { result: reviewResult, verdict } = signalToReview(signalLevel);
291
+ let { result: reviewResult, verdict } = signalToReview(signalLevel);
292
+ // 3-Strike auto-BLOCK: escalate FIX → BLOCK after 3 consecutive FIX verdicts
293
+ {
294
+ const nudgeCtx = buildNudgeContext(basePath);
295
+ if (nudgeCtx.consecutive_fix_count >= 3 && reviewResult === 'FIX') {
296
+ reviewResult = 'BLOCK';
297
+ verdict = '동일 문제 3회 반복 — 접근 방식 재검토 필요';
298
+ logger.warn({ consecutive_fix: nudgeCtx.consecutive_fix_count }, '3-strike auto-BLOCK');
299
+ }
300
+ }
292
301
  // === INJECTION POINT C: Final Verdict ===
293
302
  const finalBottleneck = reviewResult !== 'GO' ? inferBottleneck(oneLoopResult.issues) : undefined;
294
303
  await appendScorecardSafe(basePath, run_id, {
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const repairPlanInputSchema: z.ZodObject<{
3
+ run_id: z.ZodOptional<z.ZodString>;
4
+ workspace_root: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ run_id?: string | undefined;
7
+ workspace_root?: string | undefined;
8
+ }, {
9
+ run_id?: string | undefined;
10
+ workspace_root?: string | undefined;
11
+ }>;
12
+ export type RepairPlanInput = z.infer<typeof repairPlanInputSchema>;
13
+ export declare function repairPlan(input: RepairPlanInput): Promise<unknown>;
@@ -0,0 +1,67 @@
1
+ // vibe_pm.repair_plan — Generate structured repair plan from last inspect_code result
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { generateRepairPrompt, formatIssueType } from './pm_language.js';
6
+ export const repairPlanInputSchema = z.object({
7
+ run_id: z.string().optional(),
8
+ workspace_root: z.string().optional(),
9
+ });
10
+ export async function repairPlan(input) {
11
+ const workspaceRoot = input.workspace_root ?? process.cwd();
12
+ // Find the most recent inspect result from decision_log
13
+ const logPath = path.join(workspaceRoot, '.vibe', 'decision_log.v1.jsonl');
14
+ let lastInspect = null;
15
+ try {
16
+ const content = fs.readFileSync(logPath, 'utf8');
17
+ const lines = content.trim().split('\n').filter(Boolean);
18
+ for (let i = lines.length - 1; i >= 0; i--) {
19
+ const entry = JSON.parse(lines[i]);
20
+ if (entry.event_type === 'inspect_verdict') {
21
+ if (!input.run_id || entry.run_id === input.run_id) {
22
+ lastInspect = entry;
23
+ break;
24
+ }
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ return { error: 'decision_log를 읽을 수 없습니다. inspect_code를 먼저 실행하세요.' };
30
+ }
31
+ if (!lastInspect) {
32
+ return { error: 'inspect_verdict 기록이 없습니다. inspect_code를 먼저 실행하세요.' };
33
+ }
34
+ const verdict = lastInspect.verdict;
35
+ if (verdict === 'GO') {
36
+ return { problem_summary: '이슈 없음', repair_steps: [], repair_prompt: null, next_tool: 'vibe_pm.status' };
37
+ }
38
+ const issuesSummary = lastInspect.issues_summary ?? '';
39
+ const bottleneck = lastInspect.bottleneck ?? 'unknown';
40
+ const runId = lastInspect.run_id ?? '';
41
+ // Parse issues from summary
42
+ const issueParts = issuesSummary.split(';').map(s => s.trim()).filter(Boolean);
43
+ const repairSteps = issueParts.map((part, i) => {
44
+ const [type, ...rest] = part.split(':');
45
+ return {
46
+ step: i + 1,
47
+ issue_type: type?.trim() ?? 'unknown',
48
+ description: rest.join(':').trim() || part,
49
+ action: `${formatIssueType((type?.trim() ?? 'unknown')).name} 수정`,
50
+ };
51
+ });
52
+ const repairData = {
53
+ projectName: runId,
54
+ mode: 'balanced',
55
+ problem: issuesSummary || `${bottleneck} 관련 문제`,
56
+ cause: `주요 병목: ${bottleneck}`,
57
+ fixDirection: repairSteps.map(s => s.action),
58
+ verifyCriteria: ['빌드 성공', '테스트 통과', '범위 내 변경'],
59
+ };
60
+ return {
61
+ problem_summary: `${verdict} — ${bottleneck} (${issueParts.length}건)`,
62
+ repair_steps: repairSteps,
63
+ repair_prompt: generateRepairPrompt(repairData),
64
+ next_tool: verdict === 'FIX' ? 'vibe_pm.inspect_code' : 'vibe_pm.get_decision',
65
+ run_id: runId,
66
+ };
67
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const skillsInputSchema: z.ZodObject<{
3
+ mode: z.ZodDefault<z.ZodEnum<["list", "detail"]>>;
4
+ skill_id: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ mode: "list" | "detail";
7
+ skill_id?: string | undefined;
8
+ }, {
9
+ mode?: "list" | "detail" | undefined;
10
+ skill_id?: string | undefined;
11
+ }>;
12
+ export type SkillsInput = z.infer<typeof skillsInputSchema>;
13
+ export declare function skills(input: SkillsInput): Promise<unknown>;
@@ -0,0 +1,57 @@
1
+ // vibe_pm.skills — Skill pack listing and detail
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { listActivatedSkills } from '../../bootstrap/skills-installer.js';
6
+ export const skillsInputSchema = z.object({
7
+ mode: z.enum(['list', 'detail']).default('list'),
8
+ skill_id: z.string().optional(),
9
+ });
10
+ export async function skills(input) {
11
+ const skillsDir = path.resolve(__dirname, '../../../../skills');
12
+ const skillpackPath = path.join(skillsDir, 'skillpack.json');
13
+ let entries = [];
14
+ try {
15
+ const raw = fs.readFileSync(skillpackPath, 'utf8');
16
+ const parsed = JSON.parse(raw);
17
+ entries = Array.isArray(parsed.skills) ? parsed.skills : [];
18
+ }
19
+ catch {
20
+ return { skills: [], error: 'skillpack.json을 읽을 수 없습니다.' };
21
+ }
22
+ // Get activation state
23
+ let activated = [];
24
+ try {
25
+ activated = await listActivatedSkills();
26
+ }
27
+ catch {
28
+ // fail-safe
29
+ }
30
+ const activatedSet = new Set(activated);
31
+ if (input.mode === 'detail' && input.skill_id) {
32
+ const skill = entries.find(s => s.id === input.skill_id);
33
+ if (!skill) {
34
+ return { error: `스킬 '${input.skill_id}'을(를) 찾을 수 없습니다.` };
35
+ }
36
+ return {
37
+ skill: {
38
+ id: skill.id,
39
+ name: skill.name,
40
+ description: skill.description ?? '',
41
+ required_tools: skill.required_tools ?? [],
42
+ version: skill.version ?? '1.0.0',
43
+ activated: activatedSet.has(skill.id),
44
+ },
45
+ };
46
+ }
47
+ return {
48
+ skills: entries.map(s => ({
49
+ id: s.id,
50
+ name: s.name,
51
+ description: s.description ?? '',
52
+ activated: activatedSet.has(s.id),
53
+ })),
54
+ total: entries.length,
55
+ activated_count: entries.filter(s => activatedSet.has(s.id)).length,
56
+ };
57
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export declare const suggestRootsInputSchema: z.ZodObject<{
3
+ scan_path: z.ZodOptional<z.ZodString>;
4
+ max_depth: z.ZodDefault<z.ZodNumber>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ max_depth: number;
7
+ scan_path?: string | undefined;
8
+ }, {
9
+ scan_path?: string | undefined;
10
+ max_depth?: number | undefined;
11
+ }>;
12
+ export type SuggestRootsInput = z.infer<typeof suggestRootsInputSchema>;
13
+ interface Root {
14
+ path: string;
15
+ type: string;
16
+ confidence: number;
17
+ }
18
+ export declare function suggestRoots(input: SuggestRootsInput): Promise<{
19
+ roots: Root[];
20
+ }>;
21
+ export {};
@@ -0,0 +1,56 @@
1
+ // vibe_pm.suggest_roots — Discover project roots by marker files
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ export const suggestRootsInputSchema = z.object({
6
+ scan_path: z.string().optional(),
7
+ max_depth: z.number().int().min(1).max(10).default(5),
8
+ });
9
+ const MARKERS = {
10
+ 'package.json': 'node',
11
+ 'Cargo.toml': 'rust',
12
+ 'go.mod': 'go',
13
+ 'pyproject.toml': 'python',
14
+ 'pom.xml': 'java',
15
+ 'build.gradle': 'java',
16
+ 'CMakeLists.txt': 'cmake',
17
+ };
18
+ export async function suggestRoots(input) {
19
+ const scanPath = input.scan_path ?? process.cwd();
20
+ const maxDepth = input.max_depth ?? 5;
21
+ const roots = [];
22
+ function walk(dir, depth) {
23
+ if (depth > maxDepth)
24
+ return;
25
+ let entries;
26
+ try {
27
+ entries = fs.readdirSync(dir, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ for (const entry of entries) {
33
+ if (!entry.isFile())
34
+ continue;
35
+ const markerType = MARKERS[entry.name];
36
+ if (markerType) {
37
+ roots.push({
38
+ path: dir,
39
+ type: markerType,
40
+ confidence: Math.max(0.1, 1 - depth * 0.15),
41
+ });
42
+ return; // don't recurse into found root's children for same marker
43
+ }
44
+ }
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory())
47
+ continue;
48
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'target')
49
+ continue;
50
+ walk(path.join(dir, entry.name), depth + 1);
51
+ }
52
+ }
53
+ walk(scanPath, 0);
54
+ roots.sort((a, b) => b.confidence - a.confidence);
55
+ return { roots };
56
+ }
@@ -28,4 +28,12 @@ export const VIBE_PM_TOOL_DESCRIPTIONS = {
28
28
  'vibe_pm.block_search': '블록 검색(FTS). mode: search(전문 검색), chain(체인 내 블록), recent(최근 블록). block_types로 필터링 가능.',
29
29
  'vibe_pm.block_events': '블록 이벤트 조회/폴링. mode: recent(최근 이벤트), poll(cursor 기반 폴링). kinds로 이벤트 종류 필터링.',
30
30
  'vibe_pm.block_snapshot': '블록 스토어 스냅샷. mode: create(생성), restore(복원), list(목록), get(상세), delete(삭제).',
31
+ 'vibe_pm.skills': '스킬팩 조회. mode: list(전체 목록), detail(특정 스킬 상세). 활성화 상태 포함.',
32
+ 'vibe_pm.suggest_roots': '프로젝트 루트 자동 탐색. package.json/Cargo.toml 등 마커 파일 기반 추천.',
33
+ 'vibe_pm.check_resources': '시스템 리소스 점검. CPU, 메모리, 디스크 사용량 확인.',
34
+ 'vibe_pm.check_dormant': '프로젝트 비활성 감지. .vibe/git 활동 기반 휴면 상태 판단.',
35
+ 'vibe_pm.repair_plan': '복구 계획 생성. 최근 inspect_code 결과에서 구조화된 수정 프롬프트 생성.',
36
+ 'vibe_pm.finalize_work': '작업 마무리. dev_log 기록 + conventional commit 메시지 생성. confirm: true로 실제 커밋.',
37
+ 'vibe_pm.undo_last_task': '마지막 작업 되돌리기. GO 판정 기록의 관련 커밋을 git revert. confirm: true로 실행.',
38
+ 'vibe_pm.branch_pivot': '브랜치 전환. .vibe/ 상태 스냅샷 저장/복원 지원.',
31
39
  };
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const undoLastTaskInputSchema: z.ZodObject<{
3
+ confirm: z.ZodDefault<z.ZodBoolean>;
4
+ workspace_root: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ confirm: boolean;
7
+ workspace_root?: string | undefined;
8
+ }, {
9
+ workspace_root?: string | undefined;
10
+ confirm?: boolean | undefined;
11
+ }>;
12
+ export type UndoLastTaskInput = z.infer<typeof undoLastTaskInputSchema>;
13
+ export declare function undoLastTask(input: UndoLastTaskInput): Promise<unknown>;
@@ -0,0 +1,87 @@
1
+ // vibe_pm.undo_last_task — Revert the most recent task's git commits
2
+ import { z } from 'zod';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ export const undoLastTaskInputSchema = z.object({
7
+ confirm: z.boolean().default(false),
8
+ workspace_root: z.string().optional(),
9
+ });
10
+ export async function undoLastTask(input) {
11
+ const workspaceRoot = input.workspace_root ?? process.cwd();
12
+ // Find last task from decision_log
13
+ const logPath = path.join(workspaceRoot, '.vibe', 'decision_log.v1.jsonl');
14
+ let lastTask = null;
15
+ try {
16
+ const content = fs.readFileSync(logPath, 'utf8');
17
+ const lines = content.trim().split('\n').filter(Boolean);
18
+ for (let i = lines.length - 1; i >= 0; i--) {
19
+ const entry = JSON.parse(lines[i]);
20
+ if (entry.event_type === 'inspect_verdict' && entry.verdict === 'GO') {
21
+ lastTask = entry;
22
+ break;
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ return { error: 'decision_log를 읽을 수 없습니다.' };
28
+ }
29
+ if (!lastTask) {
30
+ return { error: 'GO 판정 기록이 없습니다. 되돌릴 작업이 없습니다.' };
31
+ }
32
+ // Find commits associated with this run_id (between this GO and previous verdict)
33
+ const runId = lastTask.run_id;
34
+ const timestamp = lastTask.timestamp;
35
+ let commitsToRevert = [];
36
+ try {
37
+ const since = new Date(timestamp);
38
+ since.setMinutes(since.getMinutes() - 30); // look back 30 mins before GO
39
+ const commits = execSync(`git log --since="${since.toISOString()}" --until="${timestamp}" --format=%H`, { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim().split('\n').filter(Boolean);
40
+ commitsToRevert = commits;
41
+ }
42
+ catch {
43
+ // fallback: revert HEAD
44
+ try {
45
+ const head = execSync('git rev-parse HEAD', { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim();
46
+ commitsToRevert = [head];
47
+ }
48
+ catch {
49
+ return { error: 'git 커밋을 찾을 수 없습니다.' };
50
+ }
51
+ }
52
+ if (commitsToRevert.length === 0) {
53
+ return { error: '되돌릴 커밋이 없습니다.', run_id: runId };
54
+ }
55
+ // Preview diff
56
+ let previewDiff = '';
57
+ try {
58
+ const oldest = commitsToRevert[commitsToRevert.length - 1];
59
+ previewDiff = execSync(`git diff ${oldest}^..HEAD --stat`, { cwd: workspaceRoot, timeout: 5000, encoding: 'utf8' }).trim();
60
+ }
61
+ catch {
62
+ previewDiff = '(diff 미리보기 실패)';
63
+ }
64
+ if (input.confirm) {
65
+ try {
66
+ for (const hash of commitsToRevert) {
67
+ execSync(`git revert --no-commit ${hash}`, { cwd: workspaceRoot, timeout: 10000 });
68
+ }
69
+ execSync(`git commit -m "revert: undo task ${runId}"`, { cwd: workspaceRoot, timeout: 10000 });
70
+ return {
71
+ status: 'reverted',
72
+ commits_reverted: commitsToRevert,
73
+ run_id: runId,
74
+ };
75
+ }
76
+ catch (err) {
77
+ return { status: 'revert_failed', error: err instanceof Error ? err.message : String(err) };
78
+ }
79
+ }
80
+ return {
81
+ status: 'dry_run',
82
+ commits_to_revert: commitsToRevert,
83
+ preview_diff: previewDiff,
84
+ run_id: runId,
85
+ hint: 'confirm: true를 전달하면 실제로 되돌립니다.',
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodetown/mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Vibe PM - AI Project Manager MCP Server for non-technical founders",
5
5
  "type": "module",
6
6
  "main": "./build/mcp/index.js",