@vibecodetown/mcp-server 2.1.4 → 2.2.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/build/auth/credential_store.js +146 -0
- package/build/auth/index.js +2 -0
- package/build/control_plane/gate.js +52 -70
- package/build/index.js +2 -0
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/templates.js +3 -3
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/briefing.js +2 -1
- package/build/tools/vibe_pm/finalize_work.js +40 -4
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/index.js +73 -2
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
- package/build/tools/vibe_pm/publish_mcp.js +271 -0
- package/build/tools/vibe_pm/run_app.js +48 -45
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/undo_last_task.js +16 -20
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +497 -32
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/auth/credential_store.ts
|
|
2
|
+
// Secure local credential storage for GitHub/NPM tokens
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { readFile, writeFile, mkdir, unlink, chmod } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".vibe-pm");
|
|
8
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
9
|
+
/**
|
|
10
|
+
* Credential Store - Secure local storage for API tokens
|
|
11
|
+
*
|
|
12
|
+
* Storage: ~/.vibe-pm/credentials.json (0o600 permissions)
|
|
13
|
+
* Override: VIBECODE_CREDENTIALS_FILE env var
|
|
14
|
+
*
|
|
15
|
+
* Priority for GitHub token:
|
|
16
|
+
* 1. GITHUB_TOKEN env var
|
|
17
|
+
* 2. GH_TOKEN env var
|
|
18
|
+
* 3. Stored credential
|
|
19
|
+
*
|
|
20
|
+
* Priority for NPM token:
|
|
21
|
+
* 1. NPM_TOKEN env var
|
|
22
|
+
* 2. Stored credential
|
|
23
|
+
*/
|
|
24
|
+
export class CredentialStore {
|
|
25
|
+
cache = null;
|
|
26
|
+
resolveCredentialsPath() {
|
|
27
|
+
const override = (process.env.VIBECODE_CREDENTIALS_FILE || "").trim();
|
|
28
|
+
return override || CREDENTIALS_FILE;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get GitHub token (env var takes priority)
|
|
32
|
+
*/
|
|
33
|
+
async getGitHubToken() {
|
|
34
|
+
// Env vars take priority
|
|
35
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
36
|
+
if (envToken)
|
|
37
|
+
return envToken.trim();
|
|
38
|
+
// Fall back to stored credential
|
|
39
|
+
const creds = await this.load();
|
|
40
|
+
return creds?.github_token || null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get NPM token (env var takes priority)
|
|
44
|
+
*/
|
|
45
|
+
async getNpmToken() {
|
|
46
|
+
// Env var takes priority
|
|
47
|
+
const envToken = process.env.NPM_TOKEN;
|
|
48
|
+
if (envToken)
|
|
49
|
+
return envToken.trim();
|
|
50
|
+
// Fall back to stored credential
|
|
51
|
+
const creds = await this.load();
|
|
52
|
+
return creds?.npm_token || null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Store GitHub token
|
|
56
|
+
*/
|
|
57
|
+
async setGitHubToken(token) {
|
|
58
|
+
const creds = (await this.load()) || {};
|
|
59
|
+
creds.github_token = token;
|
|
60
|
+
creds.updated_at = new Date().toISOString();
|
|
61
|
+
await this.save(creds);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Store NPM token
|
|
65
|
+
*/
|
|
66
|
+
async setNpmToken(token) {
|
|
67
|
+
const creds = (await this.load()) || {};
|
|
68
|
+
creds.npm_token = token;
|
|
69
|
+
creds.updated_at = new Date().toISOString();
|
|
70
|
+
await this.save(creds);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Remove a credential
|
|
74
|
+
*/
|
|
75
|
+
async remove(key) {
|
|
76
|
+
const creds = await this.load();
|
|
77
|
+
if (!creds)
|
|
78
|
+
return;
|
|
79
|
+
delete creds[key];
|
|
80
|
+
creds.updated_at = new Date().toISOString();
|
|
81
|
+
await this.save(creds);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clear all credentials
|
|
85
|
+
*/
|
|
86
|
+
async clear() {
|
|
87
|
+
this.cache = null;
|
|
88
|
+
const credPath = this.resolveCredentialsPath();
|
|
89
|
+
if (existsSync(credPath)) {
|
|
90
|
+
await unlink(credPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check what credentials are stored (without exposing values)
|
|
95
|
+
*/
|
|
96
|
+
async status() {
|
|
97
|
+
const creds = await this.load();
|
|
98
|
+
return {
|
|
99
|
+
github: !!creds?.github_token,
|
|
100
|
+
npm: !!creds?.npm_token,
|
|
101
|
+
path: this.resolveCredentialsPath(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load credentials from disk
|
|
106
|
+
*/
|
|
107
|
+
async load() {
|
|
108
|
+
if (this.cache)
|
|
109
|
+
return this.cache;
|
|
110
|
+
const credPath = this.resolveCredentialsPath();
|
|
111
|
+
if (!existsSync(credPath))
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
const data = await readFile(credPath, "utf-8");
|
|
115
|
+
this.cache = JSON.parse(data);
|
|
116
|
+
return this.cache;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Save credentials to disk with secure permissions
|
|
124
|
+
*/
|
|
125
|
+
async save(creds) {
|
|
126
|
+
this.cache = creds;
|
|
127
|
+
// Ensure config directory exists
|
|
128
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
129
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
130
|
+
}
|
|
131
|
+
const credPath = this.resolveCredentialsPath();
|
|
132
|
+
const data = JSON.stringify(creds, null, 2);
|
|
133
|
+
// Write with restricted permissions (owner read/write only)
|
|
134
|
+
await writeFile(credPath, data, { mode: 0o600 });
|
|
135
|
+
// Ensure permissions on existing file
|
|
136
|
+
await chmod(credPath, 0o600);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Singleton instance
|
|
140
|
+
let _store = null;
|
|
141
|
+
export function getCredentialStore() {
|
|
142
|
+
if (!_store) {
|
|
143
|
+
_store = new CredentialStore();
|
|
144
|
+
}
|
|
145
|
+
return _store;
|
|
146
|
+
}
|
package/build/auth/index.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* - Token caching and persistence
|
|
6
6
|
* - Offline JWT verification
|
|
7
7
|
* - Feature gating based on entitlements
|
|
8
|
+
* - Credential storage (GitHub/NPM tokens)
|
|
8
9
|
*/
|
|
9
10
|
export { TokenCache } from './token_cache.js';
|
|
10
11
|
export { TokenVerifier } from './token_verifier.js';
|
|
11
12
|
export { AuthGate } from './gate.js';
|
|
12
13
|
export { PUBLIC_KEY } from './public_key.js';
|
|
14
|
+
export { CredentialStore, getCredentialStore } from './credential_store.js';
|
|
13
15
|
// Re-export convenience functions
|
|
14
16
|
import { TokenCache } from './token_cache.js';
|
|
15
17
|
import { TokenVerifier } from './token_verifier.js';
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides gate enforcement for work orders before execution.
|
|
5
5
|
* All work orders must pass through the gate before any work can proceed.
|
|
6
|
+
*
|
|
7
|
+
* All subprocess invocations go through cli_invoker for centralized management.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import { invokeSystem } from "../runtime/cli_invoker.js";
|
|
8
10
|
import { WorkOrderV1Schema } from "../generated/work_order_v1.js";
|
|
9
11
|
import { GateResultV1Schema } from "../generated/gate_result_v1.js";
|
|
10
12
|
/**
|
|
@@ -30,76 +32,56 @@ export async function runSystemDesignGate(workOrder, options) {
|
|
|
30
32
|
pushBool("fail-fast", options.failFast);
|
|
31
33
|
pushBool("semgrep", options.semgrep);
|
|
32
34
|
pushBool("runtime", options.runtime);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
env: { ...process.env, PYTHONIOENCODING: "utf-8" },
|
|
49
|
-
});
|
|
50
|
-
let stdout = "";
|
|
51
|
-
let stderr = "";
|
|
52
|
-
child.stdout?.on("data", (data) => {
|
|
53
|
-
stdout += data.toString();
|
|
54
|
-
});
|
|
55
|
-
child.stderr?.on("data", (data) => {
|
|
56
|
-
stderr += data.toString();
|
|
57
|
-
});
|
|
58
|
-
child.on("close", (code) => {
|
|
59
|
-
// Exit code 0 = ALLOW, 2 = BLOCK, other = error
|
|
60
|
-
if (code === null) {
|
|
61
|
-
resolve({
|
|
62
|
-
success: false,
|
|
63
|
-
error: "Gate check timed out",
|
|
64
|
-
});
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (code !== 0 && code !== 2) {
|
|
68
|
-
resolve({
|
|
69
|
-
success: false,
|
|
70
|
-
error: `Gate check failed with code ${code}: ${stderr || stdout}`,
|
|
71
|
-
});
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const result = JSON.parse(stdout.trim());
|
|
76
|
-
const validated = GateResultV1Schema.safeParse(result);
|
|
77
|
-
if (!validated.success) {
|
|
78
|
-
resolve({
|
|
79
|
-
success: false,
|
|
80
|
-
error: `Invalid gate result: ${validated.error.message}`,
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
resolve({
|
|
85
|
-
success: true,
|
|
86
|
-
result: validated.data,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
resolve({
|
|
91
|
-
success: false,
|
|
92
|
-
error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
child.on("error", (err) => {
|
|
97
|
-
resolve({
|
|
98
|
-
success: false,
|
|
99
|
-
error: `Gate check process error: ${err.message}`,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
35
|
+
const args = [
|
|
36
|
+
"-m",
|
|
37
|
+
"vibecoding_helper",
|
|
38
|
+
"--repo-root",
|
|
39
|
+
repoRoot,
|
|
40
|
+
"gate-check",
|
|
41
|
+
...overrideFlags,
|
|
42
|
+
"--work-order",
|
|
43
|
+
workOrderJson,
|
|
44
|
+
"--format",
|
|
45
|
+
"json",
|
|
46
|
+
];
|
|
47
|
+
const result = await invokeSystem(pythonExe, args, repoRoot, {
|
|
48
|
+
env: { PYTHONIOENCODING: "utf-8" },
|
|
49
|
+
timeoutMs,
|
|
102
50
|
});
|
|
51
|
+
// Exit code 124 = timeout
|
|
52
|
+
if (result.exitCode === 124) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: "Gate check timed out",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Exit code 0 = ALLOW, 2 = BLOCK, other = error
|
|
59
|
+
if (result.exitCode !== 0 && result.exitCode !== 2) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: `Gate check failed with code ${result.exitCode}: ${result.stderr || result.stdout}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
67
|
+
const validated = GateResultV1Schema.safeParse(parsed);
|
|
68
|
+
if (!validated.success) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: `Invalid gate result: ${validated.error.message}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
result: validated.data,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
103
85
|
}
|
|
104
86
|
/**
|
|
105
87
|
* Execute a work order only if it passes the gate
|
package/build/index.js
CHANGED
|
@@ -71,6 +71,8 @@ server.tool("vibe_pm.export_output", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.export_o
|
|
|
71
71
|
server.tool("vibe_pm.search_oss", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.search_oss"], pm.searchOssInputSchema.shape, async (input) => pm.vibePmSearchOss(pm.searchOssInputSchema.parse(input)));
|
|
72
72
|
server.tool("vibe_pm.zoekt_evidence", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.zoekt_evidence"], pm.zoektEvidenceInputSchema.shape, async (input) => pm.vibePmZoektEvidence(pm.zoektEvidenceInputSchema.parse(input)));
|
|
73
73
|
server.tool("vibe_pm.gate", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.gate"], pm.gateInputSchema.shape, async (input) => pm.vibePmGate(pm.gateInputSchema.parse(input)));
|
|
74
|
+
server.tool("vibe_pm.save_rule", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.save_rule"], pm.saveRuleInputSchema.shape, async (input) => pm.vibePmSaveRule(pm.saveRuleInputSchema.parse(input)));
|
|
75
|
+
server.tool("vibe_pm.list_rules", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.list_rules"], pm.listRulesInputSchema.shape, async (input) => pm.vibePmListRules(pm.listRulesInputSchema.parse(input)));
|
|
74
76
|
// ============================================================
|
|
75
77
|
// OPTIONAL: react_perf.* Tools (Performance Analysis)
|
|
76
78
|
// ============================================================
|
package/build/local-mode/git.js
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Git utilities for local mode
|
|
3
|
+
*
|
|
4
|
+
* All git commands go through cli_invoker for centralized subprocess management.
|
|
5
|
+
*
|
|
6
|
+
* @module local-mode/git
|
|
7
|
+
*/
|
|
8
|
+
import { invokeGitSync } from "../runtime/cli_invoker.js";
|
|
9
|
+
/**
|
|
10
|
+
* Get the git repository root directory
|
|
11
|
+
*
|
|
12
|
+
* @param cwd - Current working directory
|
|
13
|
+
* @returns Git root path or null if not in a git repository
|
|
14
|
+
*/
|
|
2
15
|
export function getGitRoot(cwd) {
|
|
3
|
-
const result =
|
|
4
|
-
|
|
5
|
-
encoding: "utf-8",
|
|
6
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
7
|
-
});
|
|
8
|
-
if (result.status !== 0)
|
|
16
|
+
const result = invokeGitSync(["rev-parse", "--show-toplevel"], cwd);
|
|
17
|
+
if (result.exitCode !== 0)
|
|
9
18
|
return null;
|
|
10
|
-
const out =
|
|
19
|
+
const out = result.stdout.trim();
|
|
11
20
|
return out.length > 0 ? out : null;
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Get the configured git hooks path
|
|
24
|
+
*
|
|
25
|
+
* @param cwd - Current working directory (should be git root)
|
|
26
|
+
* @returns Configured hooks path or null if not set
|
|
27
|
+
*/
|
|
13
28
|
export function getGitHooksPath(cwd) {
|
|
14
|
-
const result =
|
|
15
|
-
|
|
16
|
-
encoding: "utf-8",
|
|
17
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
-
});
|
|
19
|
-
if (result.status !== 0)
|
|
29
|
+
const result = invokeGitSync(["config", "--local", "--get", "core.hooksPath"], cwd);
|
|
30
|
+
if (result.exitCode !== 0)
|
|
20
31
|
return null;
|
|
21
|
-
const out =
|
|
32
|
+
const out = result.stdout.trim();
|
|
22
33
|
return out.length > 0 ? out : null;
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Set the git hooks path
|
|
37
|
+
*
|
|
38
|
+
* @param cwd - Current working directory (should be git root)
|
|
39
|
+
* @param hooksPath - Path to hooks directory
|
|
40
|
+
* @returns Success or error
|
|
41
|
+
*/
|
|
24
42
|
export function setGitHooksPath(cwd, hooksPath) {
|
|
25
|
-
const result =
|
|
26
|
-
|
|
27
|
-
encoding: "utf-8",
|
|
28
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
-
});
|
|
30
|
-
if (result.status === 0)
|
|
43
|
+
const result = invokeGitSync(["config", "--local", "core.hooksPath", hooksPath], cwd);
|
|
44
|
+
if (result.exitCode === 0)
|
|
31
45
|
return { ok: true };
|
|
32
|
-
return { ok: false, error: (result.stderr
|
|
46
|
+
return { ok: false, error: (result.stderr || "git config failed").trim() };
|
|
33
47
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/local-mode/project-state.ts
|
|
2
|
+
// Per-folder project state persistence
|
|
3
|
+
// Stores workflow state, reminders, and pending actions
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { getVibeRepoPaths } from "./paths.js";
|
|
7
|
+
const DEFAULT_STATE = {
|
|
8
|
+
schema_version: 1,
|
|
9
|
+
phases: [],
|
|
10
|
+
pending_actions: [],
|
|
11
|
+
reminders: [],
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Load project state from .vibe/project_state.json
|
|
15
|
+
*/
|
|
16
|
+
export function loadProjectState(basePath = process.cwd()) {
|
|
17
|
+
const paths = getVibeRepoPaths(basePath);
|
|
18
|
+
const statePath = path.join(paths.vibeDir, "project_state.json");
|
|
19
|
+
if (!fs.existsSync(statePath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(statePath, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Save project state to .vibe/project_state.json
|
|
32
|
+
*/
|
|
33
|
+
export function saveProjectState(state, basePath = process.cwd()) {
|
|
34
|
+
const paths = getVibeRepoPaths(basePath);
|
|
35
|
+
const statePath = path.join(paths.vibeDir, "project_state.json");
|
|
36
|
+
// Ensure .vibe directory exists
|
|
37
|
+
if (!fs.existsSync(paths.vibeDir)) {
|
|
38
|
+
fs.mkdirSync(paths.vibeDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
state.updated_at = new Date().toISOString();
|
|
41
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get or create project state
|
|
45
|
+
*/
|
|
46
|
+
export function getProjectState(basePath = process.cwd()) {
|
|
47
|
+
const existing = loadProjectState(basePath);
|
|
48
|
+
if (existing) {
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
const projectId = path.basename(basePath).toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
52
|
+
return {
|
|
53
|
+
...DEFAULT_STATE,
|
|
54
|
+
project_id: projectId,
|
|
55
|
+
updated_at: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Add a pending action
|
|
60
|
+
*/
|
|
61
|
+
export function addPendingAction(action, basePath = process.cwd()) {
|
|
62
|
+
const state = getProjectState(basePath);
|
|
63
|
+
// Check for duplicate
|
|
64
|
+
const exists = state.pending_actions.some((a) => a.type === action.type && a.reason === action.reason);
|
|
65
|
+
if (exists)
|
|
66
|
+
return;
|
|
67
|
+
state.pending_actions.push({
|
|
68
|
+
...action,
|
|
69
|
+
created_at: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
saveProjectState(state, basePath);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove a pending action by type
|
|
75
|
+
*/
|
|
76
|
+
export function removePendingAction(type, basePath = process.cwd()) {
|
|
77
|
+
const state = getProjectState(basePath);
|
|
78
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== type);
|
|
79
|
+
saveProjectState(state, basePath);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all pending actions (for reminders)
|
|
83
|
+
*/
|
|
84
|
+
export function getPendingActions(basePath = process.cwd()) {
|
|
85
|
+
const state = loadProjectState(basePath);
|
|
86
|
+
return state?.pending_actions || [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Update last commit info
|
|
90
|
+
*/
|
|
91
|
+
export function updateLastCommit(info, basePath = process.cwd()) {
|
|
92
|
+
const state = getProjectState(basePath);
|
|
93
|
+
state.last_commit = info;
|
|
94
|
+
// If pushed, remove git_push pending action
|
|
95
|
+
if (info.pushed) {
|
|
96
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== "git_push");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Add git_push pending action if not pushed
|
|
100
|
+
const exists = state.pending_actions.some((a) => a.type === "git_push");
|
|
101
|
+
if (!exists) {
|
|
102
|
+
state.pending_actions.push({
|
|
103
|
+
type: "git_push",
|
|
104
|
+
reason: `커밋 ${info.hash.slice(0, 7)} 푸시 필요`,
|
|
105
|
+
created_at: new Date().toISOString(),
|
|
106
|
+
priority: "medium",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
saveProjectState(state, basePath);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update last MCP build info
|
|
114
|
+
*/
|
|
115
|
+
export function updateLastMcpBuild(info, basePath = process.cwd()) {
|
|
116
|
+
const state = getProjectState(basePath);
|
|
117
|
+
state.last_mcp_build = info;
|
|
118
|
+
// If published, remove mcp_build and npm_publish pending actions
|
|
119
|
+
if (info.published) {
|
|
120
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== "mcp_build" && a.type !== "npm_publish");
|
|
121
|
+
}
|
|
122
|
+
saveProjectState(state, basePath);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format pending actions as reminder text
|
|
126
|
+
*/
|
|
127
|
+
export function formatReminders(basePath = process.cwd()) {
|
|
128
|
+
const actions = getPendingActions(basePath);
|
|
129
|
+
if (actions.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
const lines = ["## 📋 대기 중인 작업"];
|
|
132
|
+
// Sort by priority
|
|
133
|
+
const sorted = [...actions].sort((a, b) => {
|
|
134
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
135
|
+
return order[a.priority] - order[b.priority];
|
|
136
|
+
});
|
|
137
|
+
for (const action of sorted) {
|
|
138
|
+
const emoji = action.priority === "high" ? "🔴" : action.priority === "medium" ? "🟡" : "🟢";
|
|
139
|
+
const tool = getToolForAction(action.type);
|
|
140
|
+
lines.push(`- ${emoji} ${action.reason}${tool ? ` → \`${tool}\`` : ""}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
function getToolForAction(type) {
|
|
145
|
+
const mapping = {
|
|
146
|
+
mcp_build: "vibe_pm.publish_mcp",
|
|
147
|
+
engine_build: null, // Manual
|
|
148
|
+
npm_publish: "vibe_pm.publish_mcp",
|
|
149
|
+
git_push: "vibe_pm.finalize_work",
|
|
150
|
+
review: "vibe_pm.inspect_code",
|
|
151
|
+
custom: null,
|
|
152
|
+
};
|
|
153
|
+
return mapping[type];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check and add MCP build pending action if needed
|
|
157
|
+
*/
|
|
158
|
+
export async function checkAndAddMcpBuildAction(basePath = process.cwd()) {
|
|
159
|
+
// Dynamically import to avoid circular dependency
|
|
160
|
+
const { needsMcpBuild } = await import("../tools/vibe_pm/publish_mcp.js");
|
|
161
|
+
try {
|
|
162
|
+
const needs = await needsMcpBuild(basePath);
|
|
163
|
+
if (needs) {
|
|
164
|
+
addPendingAction({
|
|
165
|
+
type: "mcp_build",
|
|
166
|
+
reason: "MCP 소스 변경됨 - 빌드 필요",
|
|
167
|
+
priority: "high",
|
|
168
|
+
}, basePath);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
@@ -763,9 +763,9 @@ jobs:
|
|
|
763
763
|
- name: Run Vibe guard
|
|
764
764
|
shell: bash
|
|
765
765
|
env:
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
VIBE_FAIL_ON_WARN: "
|
|
766
|
+
# P0-2: CI 환경에서는 WARN도 빌드 실패 처리 (기본값)
|
|
767
|
+
# 이전 동작이 필요하면 "false"로 변경
|
|
768
|
+
VIBE_FAIL_ON_WARN: "true"
|
|
769
769
|
run: |
|
|
770
770
|
set -euo pipefail
|
|
771
771
|
if [[ ! -f ".vibe/lib/validate.sh" ]]; then
|