@vibecodetown/mcp-server 2.1.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/LICENSE +21 -0
- package/README.md +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- package/package.json +76 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
export const VALIDATE_SH_TEMPLATE = `#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Vibe PM Validation Library (SSOT)
|
|
4
|
+
# Hooks/CLI/CI call validate_changes().
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# Repo Root (be resilient to hook cwd)
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
14
|
+
cd "$REPO_ROOT" || exit 1
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Config
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
VIBE_DIR=".vibe"
|
|
21
|
+
CONFIG_FILE="$VIBE_DIR/config.json"
|
|
22
|
+
STATE_DIR="$VIBE_DIR/state"
|
|
23
|
+
WORK_ORDER_FILE="$STATE_DIR/current_work_order.json"
|
|
24
|
+
STALE_HOURS=24
|
|
25
|
+
OUTPUT_FORMAT="\${VIBE_OUTPUT_FORMAT:-text}"
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Colors
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
RED='\\033[0;31m'
|
|
32
|
+
YELLOW='\\033[1;33m'
|
|
33
|
+
GREEN='\\033[0;32m'
|
|
34
|
+
NC='\\033[0m'
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Logging
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
log_info() { echo -e "\${GREEN}[VIBE]\${NC} $1"; }
|
|
41
|
+
log_warn() { echo -e "\${YELLOW}[VIBE WARN]\${NC} $1"; }
|
|
42
|
+
log_error() { echo -e "\${RED}[VIBE BLOCK]\${NC} $1"; }
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# JSON Helpers (jq optional)
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
json_get() {
|
|
49
|
+
local file="$1"
|
|
50
|
+
local key_path="$2"
|
|
51
|
+
local default="$3"
|
|
52
|
+
|
|
53
|
+
if [ ! -f "$file" ]; then
|
|
54
|
+
echo "$default"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if command -v jq &> /dev/null; then
|
|
59
|
+
local value
|
|
60
|
+
value="$(jq -r "$key_path // empty" "$file" 2>/dev/null || true)"
|
|
61
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
62
|
+
echo "$value"
|
|
63
|
+
else
|
|
64
|
+
echo "$default"
|
|
65
|
+
fi
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
node - <<'NODE' "$file" "$key_path" "$default" 2>/dev/null || echo "$default"
|
|
70
|
+
const fs = require("fs");
|
|
71
|
+
const file = process.argv[2];
|
|
72
|
+
const keyPath = (process.argv[3] || "").replace(/^\\./, "");
|
|
73
|
+
const defaultValue = process.argv[4] ?? "";
|
|
74
|
+
let doc;
|
|
75
|
+
try { doc = JSON.parse(fs.readFileSync(file, "utf8")); } catch { process.stdout.write(defaultValue); process.exit(0); }
|
|
76
|
+
let cur = doc;
|
|
77
|
+
for (const part of keyPath.split(".").filter(Boolean)) {
|
|
78
|
+
if (cur && typeof cur === "object" && Object.prototype.hasOwnProperty.call(cur, part)) cur = cur[part];
|
|
79
|
+
else { cur = undefined; break; }
|
|
80
|
+
}
|
|
81
|
+
if (cur === undefined || cur === null || cur === "") { process.stdout.write(defaultValue); process.exit(0); }
|
|
82
|
+
process.stdout.write(String(cur));
|
|
83
|
+
NODE
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
json_get_array() {
|
|
87
|
+
local file="$1"
|
|
88
|
+
local key_path="$2"
|
|
89
|
+
|
|
90
|
+
if [ ! -f "$file" ]; then
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if command -v jq &> /dev/null; then
|
|
95
|
+
jq -r "$key_path[]? // empty" "$file" 2>/dev/null || true
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
node - <<'NODE' "$file" "$key_path" 2>/dev/null || true
|
|
100
|
+
const fs = require("fs");
|
|
101
|
+
const file = process.argv[2];
|
|
102
|
+
const keyPath = (process.argv[3] || "").replace(/^\\./, "");
|
|
103
|
+
let doc;
|
|
104
|
+
try { doc = JSON.parse(fs.readFileSync(file, "utf8")); } catch { process.exit(0); }
|
|
105
|
+
let cur = doc;
|
|
106
|
+
for (const part of keyPath.split(".").filter(Boolean)) {
|
|
107
|
+
if (cur && typeof cur === "object" && Object.prototype.hasOwnProperty.call(cur, part)) cur = cur[part];
|
|
108
|
+
else { cur = undefined; break; }
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(cur)) process.exit(0);
|
|
111
|
+
for (const item of cur) {
|
|
112
|
+
if (typeof item === "string" && item.length) console.log(item);
|
|
113
|
+
}
|
|
114
|
+
NODE
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get_config() {
|
|
118
|
+
local key_path="$1"
|
|
119
|
+
local default="$2"
|
|
120
|
+
json_get "$CONFIG_FILE" "$key_path" "$default"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get_config_array() {
|
|
124
|
+
local key_path="$1"
|
|
125
|
+
json_get_array "$CONFIG_FILE" "$key_path"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# =============================================================================
|
|
129
|
+
# Zone Matching (git pathspec)
|
|
130
|
+
# =============================================================================
|
|
131
|
+
|
|
132
|
+
matches_pathspec() {
|
|
133
|
+
local file="$1"
|
|
134
|
+
local pattern="$2"
|
|
135
|
+
|
|
136
|
+
local ps="$pattern"
|
|
137
|
+
if [[ "$pattern" != ":(glob)"* && "$pattern" != ":(icase)"* && "$pattern" != ":(exclude)"* ]]; then
|
|
138
|
+
ps=":(glob)$pattern"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if git ls-files --cached --others --exclude-standard --full-name -- "$ps" 2>/dev/null | grep -Fxq "$file"; then
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get_file_zone() {
|
|
149
|
+
local file="$1"
|
|
150
|
+
|
|
151
|
+
local black_patterns="$(get_config_array ".policy.safety_zones.black")"
|
|
152
|
+
local green_patterns="$(get_config_array ".policy.safety_zones.green")"
|
|
153
|
+
local yellow_patterns="$(get_config_array ".policy.safety_zones.yellow")"
|
|
154
|
+
local red_patterns="$(get_config_array ".policy.safety_zones.red")"
|
|
155
|
+
|
|
156
|
+
if [ -z "$black_patterns$green_patterns$yellow_patterns$red_patterns" ]; then
|
|
157
|
+
black_patterns=$'.vibe/state/**\\n.vibe/decisions/**\\n.vibe/chroma/**\\n.git/**'
|
|
158
|
+
green_patterns=$'src/**\\ntests/**\\nlib/**\\nscripts/**'
|
|
159
|
+
yellow_patterns=$'docs/**\\nREADME.md\\n*.md'
|
|
160
|
+
red_patterns=$'config/**\\n.env\\n.env.*\\nsecrets/**\\ncredentials.*\\n*.pem\\n*.key'
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
for pattern in $black_patterns; do
|
|
164
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
165
|
+
echo "black"
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
for pattern in $red_patterns; do
|
|
171
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
172
|
+
echo "red"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
for pattern in $yellow_patterns; do
|
|
178
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
179
|
+
echo "yellow"
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
done
|
|
183
|
+
|
|
184
|
+
for pattern in $green_patterns; do
|
|
185
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
186
|
+
echo "green"
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
done
|
|
190
|
+
|
|
191
|
+
echo "unspecified"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# =============================================================================
|
|
195
|
+
# Work Order Validation
|
|
196
|
+
# =============================================================================
|
|
197
|
+
|
|
198
|
+
has_work_order() {
|
|
199
|
+
[ -f "$WORK_ORDER_FILE" ]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
is_work_order_stale() {
|
|
203
|
+
if ! has_work_order; then
|
|
204
|
+
return 1
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
local threshold
|
|
208
|
+
threshold="$(get_config ".policy.stale_hours" "24")"
|
|
209
|
+
if ! [[ "$threshold" =~ ^[0-9]+$ ]]; then
|
|
210
|
+
threshold=24
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
local created_at
|
|
214
|
+
created_at="$(json_get "$WORK_ORDER_FILE" ".created_at" "")"
|
|
215
|
+
if [ -z "$created_at" ]; then
|
|
216
|
+
return 1
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
local created_epoch
|
|
220
|
+
created_epoch="$(node -e 'const d=new Date(process.argv[1]); const n=Number.isFinite(d.getTime())?Math.floor(d.getTime()/1000):0; process.stdout.write(String(n));' "$created_at" 2>/dev/null || echo "0")"
|
|
221
|
+
if [ "$created_epoch" -le 0 ]; then
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
local now_epoch
|
|
226
|
+
now_epoch="$(node -e 'process.stdout.write(String(Math.floor(Date.now()/1000)));')"
|
|
227
|
+
|
|
228
|
+
local diff_hours=$(( (now_epoch - created_epoch) / 3600 ))
|
|
229
|
+
[ "$diff_hours" -ge "$threshold" ]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
check_do_not_touch() {
|
|
233
|
+
local file="$1"
|
|
234
|
+
if ! has_work_order; then
|
|
235
|
+
return 1
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
local patterns
|
|
239
|
+
patterns="$(json_get_array "$WORK_ORDER_FILE" ".do_not_touch")"
|
|
240
|
+
for pattern in $patterns; do
|
|
241
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
done
|
|
245
|
+
|
|
246
|
+
return 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
check_scope_violation() {
|
|
250
|
+
local file="$1"
|
|
251
|
+
if ! has_work_order; then
|
|
252
|
+
return 1
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
local excludes
|
|
256
|
+
excludes="$(json_get_array "$WORK_ORDER_FILE" ".scope.exclude")"
|
|
257
|
+
for pattern in $excludes; do
|
|
258
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
done
|
|
262
|
+
|
|
263
|
+
local includes
|
|
264
|
+
includes="$(json_get_array "$WORK_ORDER_FILE" ".scope.include")"
|
|
265
|
+
if [ -z "$includes" ]; then
|
|
266
|
+
return 1
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
for pattern in $includes; do
|
|
270
|
+
if matches_pathspec "$file" "$pattern"; then
|
|
271
|
+
return 1
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
274
|
+
|
|
275
|
+
return 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# =============================================================================
|
|
279
|
+
# Main Validation Function
|
|
280
|
+
# =============================================================================
|
|
281
|
+
|
|
282
|
+
# JSON output (machine readable)
|
|
283
|
+
emit_validation_json() {
|
|
284
|
+
local exit_code="$1"; shift
|
|
285
|
+
local enforcement="$1"; shift
|
|
286
|
+
local no_ticket_no_work="$1"; shift
|
|
287
|
+
local stale_hours="$1"; shift
|
|
288
|
+
local has_work_order_val="$1"; shift
|
|
289
|
+
local work_order_stale_val="$1"; shift
|
|
290
|
+
local green_list="$1"; shift
|
|
291
|
+
local yellow_list="$1"; shift
|
|
292
|
+
local red_list="$1"; shift
|
|
293
|
+
local black_list="$1"; shift
|
|
294
|
+
local do_not_touch_list="$1"; shift
|
|
295
|
+
local scope_violation_list="$1"; shift
|
|
296
|
+
local changed_list="$1"; shift
|
|
297
|
+
|
|
298
|
+
VIBE_EXIT_CODE="$exit_code" \
|
|
299
|
+
VIBE_ENFORCEMENT="$enforcement" \
|
|
300
|
+
VIBE_NO_TICKET_NO_WORK="$no_ticket_no_work" \
|
|
301
|
+
VIBE_STALE_HOURS="$stale_hours" \
|
|
302
|
+
VIBE_HAS_WORK_ORDER="$has_work_order_val" \
|
|
303
|
+
VIBE_WORK_ORDER_STALE="$work_order_stale_val" \
|
|
304
|
+
VIBE_CHANGED_FILES="$changed_list" \
|
|
305
|
+
VIBE_GREEN_FILES="$green_list" \
|
|
306
|
+
VIBE_YELLOW_FILES="$yellow_list" \
|
|
307
|
+
VIBE_RED_FILES="$red_list" \
|
|
308
|
+
VIBE_BLACK_FILES="$black_list" \
|
|
309
|
+
VIBE_DO_NOT_TOUCH_FILES="$do_not_touch_list" \
|
|
310
|
+
VIBE_SCOPE_VIOLATION_FILES="$scope_violation_list" \
|
|
311
|
+
node - <<'NODE'
|
|
312
|
+
const env = process.env;
|
|
313
|
+
const nl = String.fromCharCode(10);
|
|
314
|
+
const bs = String.fromCharCode(92);
|
|
315
|
+
const bsN = bs + "n";
|
|
316
|
+
|
|
317
|
+
const normalize = (v) => String(v ?? "").split(bsN).join(nl);
|
|
318
|
+
const list = (name) =>
|
|
319
|
+
normalize(env[name] ?? "")
|
|
320
|
+
.split(nl)
|
|
321
|
+
.map((s) => s.trim())
|
|
322
|
+
.filter(Boolean);
|
|
323
|
+
|
|
324
|
+
const exitCode = parseInt(env.VIBE_EXIT_CODE || "0", 10);
|
|
325
|
+
const status = exitCode === 0 ? "PASS" : exitCode === 1 ? "WARN" : "BLOCK";
|
|
326
|
+
const staleHours = parseInt(env.VIBE_STALE_HOURS || "24", 10);
|
|
327
|
+
|
|
328
|
+
const out = {
|
|
329
|
+
format_version: 1,
|
|
330
|
+
status,
|
|
331
|
+
exit_code: exitCode,
|
|
332
|
+
policy: {
|
|
333
|
+
enforcement: env.VIBE_ENFORCEMENT || "warn",
|
|
334
|
+
no_ticket_no_work: env.VIBE_NO_TICKET_NO_WORK === "true",
|
|
335
|
+
stale_hours: Number.isFinite(staleHours) ? staleHours : 24,
|
|
336
|
+
},
|
|
337
|
+
has_work_order: env.VIBE_HAS_WORK_ORDER === "true",
|
|
338
|
+
work_order_stale: env.VIBE_WORK_ORDER_STALE === "true",
|
|
339
|
+
files: {
|
|
340
|
+
changed: list("VIBE_CHANGED_FILES"),
|
|
341
|
+
green: list("VIBE_GREEN_FILES"),
|
|
342
|
+
yellow: list("VIBE_YELLOW_FILES"),
|
|
343
|
+
red: list("VIBE_RED_FILES"),
|
|
344
|
+
black: list("VIBE_BLACK_FILES"),
|
|
345
|
+
do_not_touch: list("VIBE_DO_NOT_TOUCH_FILES"),
|
|
346
|
+
scope_violation: list("VIBE_SCOPE_VIOLATION_FILES"),
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
process.stdout.write(JSON.stringify(out, null, 2));
|
|
351
|
+
NODE
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Exit codes: 0=pass, 1=warn, 2=block
|
|
355
|
+
validate_changes() {
|
|
356
|
+
local changed_files="$1"
|
|
357
|
+
|
|
358
|
+
local enforcement
|
|
359
|
+
enforcement="$(get_config ".policy.enforcement" "warn")"
|
|
360
|
+
|
|
361
|
+
local has_warning=false
|
|
362
|
+
local has_block=false
|
|
363
|
+
|
|
364
|
+
local green_files=""
|
|
365
|
+
local yellow_files=""
|
|
366
|
+
local red_files=""
|
|
367
|
+
local black_files=""
|
|
368
|
+
local do_not_touch_files=""
|
|
369
|
+
local scope_violation_files=""
|
|
370
|
+
|
|
371
|
+
local green_list=""
|
|
372
|
+
local yellow_list=""
|
|
373
|
+
local red_list=""
|
|
374
|
+
local black_list=""
|
|
375
|
+
local do_not_touch_list=""
|
|
376
|
+
local scope_violation_list=""
|
|
377
|
+
|
|
378
|
+
local no_ticket_no_work
|
|
379
|
+
no_ticket_no_work="$(get_config ".policy.no_ticket_no_work" "true")"
|
|
380
|
+
|
|
381
|
+
while IFS= read -r file; do
|
|
382
|
+
if [ -z "$file" ]; then
|
|
383
|
+
continue
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
# 1) do_not_touch (always BLOCK when present)
|
|
387
|
+
if check_do_not_touch "$file"; then
|
|
388
|
+
do_not_touch_files="$do_not_touch_files\\n - $file"
|
|
389
|
+
do_not_touch_list="$do_not_touch_list\\n$file"
|
|
390
|
+
has_block=true
|
|
391
|
+
continue
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# 2) scope violation (warn/block per enforcement)
|
|
395
|
+
if check_scope_violation "$file"; then
|
|
396
|
+
scope_violation_files="$scope_violation_files\\n - $file"
|
|
397
|
+
scope_violation_list="$scope_violation_list\\n$file"
|
|
398
|
+
has_warning=true
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# 3) zone checks
|
|
402
|
+
local zone
|
|
403
|
+
zone="$(get_file_zone "$file")"
|
|
404
|
+
case "$zone" in
|
|
405
|
+
black)
|
|
406
|
+
black_files="$black_files\\n - $file"
|
|
407
|
+
black_list="$black_list\\n$file"
|
|
408
|
+
has_block=true
|
|
409
|
+
;;
|
|
410
|
+
green)
|
|
411
|
+
green_files="$green_files\\n - $file"
|
|
412
|
+
green_list="$green_list\\n$file"
|
|
413
|
+
;;
|
|
414
|
+
red)
|
|
415
|
+
red_files="$red_files\\n - $file"
|
|
416
|
+
red_list="$red_list\\n$file"
|
|
417
|
+
if [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
418
|
+
has_block=true
|
|
419
|
+
fi
|
|
420
|
+
;;
|
|
421
|
+
yellow|unspecified)
|
|
422
|
+
yellow_files="$yellow_files\\n - $file"
|
|
423
|
+
yellow_list="$yellow_list\\n$file"
|
|
424
|
+
if [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
425
|
+
has_warning=true
|
|
426
|
+
fi
|
|
427
|
+
;;
|
|
428
|
+
esac
|
|
429
|
+
done <<< "$changed_files"
|
|
430
|
+
|
|
431
|
+
local exit_code=0
|
|
432
|
+
|
|
433
|
+
if [ -n "$black_files" ]; then
|
|
434
|
+
exit_code=2
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
if [ -n "$do_not_touch_files" ]; then
|
|
438
|
+
exit_code=2
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
if [ -n "$red_files" ] && [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
442
|
+
exit_code=2
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
if [ -n "$scope_violation_files" ]; then
|
|
446
|
+
if [ "$enforcement" = "block" ]; then
|
|
447
|
+
exit_code=2
|
|
448
|
+
elif [ "$exit_code" -lt 1 ]; then
|
|
449
|
+
exit_code=1
|
|
450
|
+
fi
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
if [ -n "$yellow_files" ] && [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
454
|
+
if [ "$enforcement" = "block" ]; then
|
|
455
|
+
exit_code=2
|
|
456
|
+
elif [ "$exit_code" -lt 1 ]; then
|
|
457
|
+
exit_code=1
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
local has_work_order_val="false"
|
|
462
|
+
if has_work_order; then
|
|
463
|
+
has_work_order_val="true"
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
local work_order_stale_val="false"
|
|
467
|
+
if has_work_order && is_work_order_stale; then
|
|
468
|
+
work_order_stale_val="true"
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
local stale_hours
|
|
472
|
+
stale_hours="$(get_config ".policy.stale_hours" "24")"
|
|
473
|
+
if ! [[ "$stale_hours" =~ ^[0-9]+$ ]]; then
|
|
474
|
+
stale_hours="24"
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
478
|
+
emit_validation_json \
|
|
479
|
+
"$exit_code" \
|
|
480
|
+
"$enforcement" \
|
|
481
|
+
"$no_ticket_no_work" \
|
|
482
|
+
"$stale_hours" \
|
|
483
|
+
"$has_work_order_val" \
|
|
484
|
+
"$work_order_stale_val" \
|
|
485
|
+
"$green_list" \
|
|
486
|
+
"$yellow_list" \
|
|
487
|
+
"$red_list" \
|
|
488
|
+
"$black_list" \
|
|
489
|
+
"$do_not_touch_list" \
|
|
490
|
+
"$scope_violation_list" \
|
|
491
|
+
"$changed_files"
|
|
492
|
+
return $exit_code
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
if [ -n "$black_files" ]; then
|
|
496
|
+
log_error "BLACK zone files (BLOCKED):$black_files"
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
if [ -n "$do_not_touch_files" ]; then
|
|
500
|
+
log_error "DO_NOT_TOUCH violation (BLOCKED):$do_not_touch_files"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
if [ -n "$red_files" ] && [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
504
|
+
log_error "RED zone files without work order (BLOCKED):$red_files"
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
if [ -n "$scope_violation_files" ]; then
|
|
508
|
+
log_warn "Scope violation:$scope_violation_files"
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
if [ -n "$yellow_files" ] && [ "$no_ticket_no_work" = "true" ] && ! has_work_order; then
|
|
512
|
+
log_warn "YELLOW zone files without work order:$yellow_files"
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
if has_work_order && is_work_order_stale; then
|
|
516
|
+
local threshold
|
|
517
|
+
threshold="$(get_config ".policy.stale_hours" "24")"
|
|
518
|
+
log_warn "Work order is stale (>\${threshold}h). Consider refreshing."
|
|
519
|
+
if [ "$exit_code" -lt 1 ]; then
|
|
520
|
+
exit_code=1
|
|
521
|
+
fi
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
525
|
+
log_info "Validation passed."
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
return $exit_code
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# =============================================================================
|
|
532
|
+
# CLI Entrypoint
|
|
533
|
+
# =============================================================================
|
|
534
|
+
|
|
535
|
+
if [[ "\${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
536
|
+
while [[ $# -gt 0 ]]; do
|
|
537
|
+
case "$1" in
|
|
538
|
+
--format)
|
|
539
|
+
OUTPUT_FORMAT="$2"
|
|
540
|
+
shift 2
|
|
541
|
+
;;
|
|
542
|
+
*)
|
|
543
|
+
shift
|
|
544
|
+
;;
|
|
545
|
+
esac
|
|
546
|
+
done
|
|
547
|
+
|
|
548
|
+
CHANGED="$(
|
|
549
|
+
(
|
|
550
|
+
git diff --name-only 2>/dev/null
|
|
551
|
+
git diff --name-only --cached 2>/dev/null
|
|
552
|
+
git ls-files --others --exclude-standard 2>/dev/null
|
|
553
|
+
) | sort -u
|
|
554
|
+
)"
|
|
555
|
+
validate_changes "$CHANGED"
|
|
556
|
+
exit $?
|
|
557
|
+
fi
|
|
558
|
+
`;
|
|
559
|
+
export const GITLEAKS_CONFIG_TEMPLATE = `title = "Vibe PM - Gitleaks config"
|
|
560
|
+
|
|
561
|
+
# Use built-in rules by default; add allowlist paths only when needed.
|
|
562
|
+
|
|
563
|
+
[allowlist]
|
|
564
|
+
description = "Allowlist for known false positives"
|
|
565
|
+
paths = [
|
|
566
|
+
# Example: "fixtures/**"
|
|
567
|
+
]
|
|
568
|
+
`;
|
|
569
|
+
export const PRE_PUSH_HOOK_TEMPLATE = `#!/usr/bin/env bash
|
|
570
|
+
#
|
|
571
|
+
# Vibe PM pre-push hook
|
|
572
|
+
# Enforces "No Ticket No Work" policy
|
|
573
|
+
#
|
|
574
|
+
|
|
575
|
+
set -e
|
|
576
|
+
|
|
577
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
578
|
+
source "$SCRIPT_DIR/../lib/validate.sh"
|
|
579
|
+
|
|
580
|
+
# Control plane off-limits (applies only to the Vibe PM repo)
|
|
581
|
+
PROTECTED_PATH_REGEX='^(\\.vibe/|vibecoding_helper/|adapters/|engines/|schemas/|fixtures/|docs/ssot/|docs/DEV_SPEC/implemented/|runs/|config/semgrep/|config/gitleaks/|policy/|scripts/generate-contracts\\.sh|scripts/generate-contract-lock\\.py|schemas/contracts\\.(version|lock)\\.json)'
|
|
582
|
+
GITLEAKS_CONFIG="config/gitleaks/.gitleaks.toml"
|
|
583
|
+
|
|
584
|
+
have_gitleaks() {
|
|
585
|
+
command -v gitleaks >/dev/null 2>&1
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
is_control_plane_repo() {
|
|
589
|
+
if [ -f "$REPO_ROOT/vibecoding_helper.spec" ]; then
|
|
590
|
+
return 0
|
|
591
|
+
fi
|
|
592
|
+
if [ -d "$REPO_ROOT/vibecoding_helper" ] && [ -d "$REPO_ROOT/adapters/mcp-ts" ]; then
|
|
593
|
+
return 0
|
|
594
|
+
fi
|
|
595
|
+
return 1
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
main() {
|
|
599
|
+
if [ ! -d "$VIBE_DIR" ]; then
|
|
600
|
+
log_info "Not a Vibe-managed repo. Skipping checks."
|
|
601
|
+
exit 0
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
local enabled
|
|
605
|
+
enabled="$(get_config ".hooks.pre_push.enabled" "true")"
|
|
606
|
+
if [ "$enabled" != "true" ]; then
|
|
607
|
+
log_info "pre-push hook disabled. Skipping."
|
|
608
|
+
exit 0
|
|
609
|
+
fi
|
|
610
|
+
|
|
611
|
+
local gitleaks_required
|
|
612
|
+
gitleaks_required="$(get_config ".hooks.pre_push.gitleaks_required" "false")"
|
|
613
|
+
|
|
614
|
+
local remote="$1"
|
|
615
|
+
local url="$2"
|
|
616
|
+
local zero_sha="0000000000000000000000000000000000000000"
|
|
617
|
+
local gitleaks_warned="false"
|
|
618
|
+
|
|
619
|
+
while read local_ref local_oid remote_ref remote_oid; do
|
|
620
|
+
if [ "$local_oid" = "$zero_sha" ]; then
|
|
621
|
+
continue
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
local changed_files
|
|
625
|
+
local log_opts
|
|
626
|
+
if [ "$remote_oid" = "0000000000000000000000000000000000000000" ]; then
|
|
627
|
+
local default_branch
|
|
628
|
+
default_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
|
|
629
|
+
if [ -z "$default_branch" ]; then
|
|
630
|
+
default_branch="main"
|
|
631
|
+
fi
|
|
632
|
+
changed_files="$(git diff --name-only "origin/$default_branch"..."$local_oid" 2>/dev/null || git ls-files)"
|
|
633
|
+
if git rev-parse --verify "origin/$default_branch" >/dev/null 2>&1; then
|
|
634
|
+
log_opts="origin/$default_branch..$local_oid"
|
|
635
|
+
else
|
|
636
|
+
log_opts="$local_oid"
|
|
637
|
+
fi
|
|
638
|
+
else
|
|
639
|
+
changed_files="$(git diff --name-only "$remote_oid" "$local_oid")"
|
|
640
|
+
log_opts="$remote_oid..$local_oid"
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
echo ""
|
|
644
|
+
echo "=============================================="
|
|
645
|
+
echo " VIBE PM: Pre-push Validation"
|
|
646
|
+
echo "=============================================="
|
|
647
|
+
echo ""
|
|
648
|
+
|
|
649
|
+
if is_control_plane_repo; then
|
|
650
|
+
if echo "$changed_files" | grep -E "$PROTECTED_PATH_REGEX" >/dev/null 2>&1; then
|
|
651
|
+
log_error "Control plane paths are off-limits (push blocked):"
|
|
652
|
+
echo "$changed_files" | grep -E "$PROTECTED_PATH_REGEX" || true
|
|
653
|
+
exit 1
|
|
654
|
+
fi
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
set +e
|
|
658
|
+
validate_changes "$changed_files"
|
|
659
|
+
local result=$?
|
|
660
|
+
set -e
|
|
661
|
+
|
|
662
|
+
case $result in
|
|
663
|
+
0)
|
|
664
|
+
log_info "Pre-push checks passed."
|
|
665
|
+
;;
|
|
666
|
+
1)
|
|
667
|
+
log_warn "Push allowed with warnings."
|
|
668
|
+
echo ""
|
|
669
|
+
echo "Consider creating a work order: vibe ticket"
|
|
670
|
+
echo ""
|
|
671
|
+
;;
|
|
672
|
+
2)
|
|
673
|
+
log_error "Push BLOCKED. Create a work order first."
|
|
674
|
+
echo ""
|
|
675
|
+
echo "Next action:"
|
|
676
|
+
echo " vibe ticket"
|
|
677
|
+
echo ""
|
|
678
|
+
echo "Or to skip (not recommended):"
|
|
679
|
+
echo " git push --no-verify"
|
|
680
|
+
echo ""
|
|
681
|
+
exit 1
|
|
682
|
+
;;
|
|
683
|
+
esac
|
|
684
|
+
|
|
685
|
+
if have_gitleaks; then
|
|
686
|
+
if ! gitleaks detect --config "$GITLEAKS_CONFIG" --redact --no-banner --log-opts "$log_opts" --exit-code 1; then
|
|
687
|
+
log_error "gitleaks detected secrets in the push range."
|
|
688
|
+
exit 1
|
|
689
|
+
fi
|
|
690
|
+
else
|
|
691
|
+
if [ "$gitleaks_required" = "true" ]; then
|
|
692
|
+
log_error "gitleaks is required but not installed. Install gitleaks or disable requirement."
|
|
693
|
+
exit 1
|
|
694
|
+
fi
|
|
695
|
+
if [ "$gitleaks_warned" != "true" ]; then
|
|
696
|
+
log_warn "gitleaks not installed. Skipping secret scan."
|
|
697
|
+
gitleaks_warned="true"
|
|
698
|
+
fi
|
|
699
|
+
fi
|
|
700
|
+
done
|
|
701
|
+
|
|
702
|
+
exit 0
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
main "$@"
|
|
706
|
+
`;
|
|
707
|
+
export const GITHUB_ACTIONS_LOCAL_MODE_GUARD_WORKFLOW_TEMPLATE = `name: Vibe PM Local Mode Guard
|
|
708
|
+
|
|
709
|
+
on:
|
|
710
|
+
pull_request:
|
|
711
|
+
push:
|
|
712
|
+
branches: [ main ]
|
|
713
|
+
|
|
714
|
+
jobs:
|
|
715
|
+
vibe-local-mode-guard:
|
|
716
|
+
runs-on: ubuntu-latest
|
|
717
|
+
timeout-minutes: 5
|
|
718
|
+
steps:
|
|
719
|
+
- name: Checkout
|
|
720
|
+
uses: actions/checkout@v4
|
|
721
|
+
with:
|
|
722
|
+
fetch-depth: 0
|
|
723
|
+
|
|
724
|
+
- name: Determine diff range
|
|
725
|
+
shell: bash
|
|
726
|
+
run: |
|
|
727
|
+
set -euo pipefail
|
|
728
|
+
python - <<'PY'
|
|
729
|
+
import json, os, subprocess
|
|
730
|
+
|
|
731
|
+
event_path = os.environ.get("GITHUB_EVENT_PATH")
|
|
732
|
+
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
|
|
733
|
+
head = os.environ.get("GITHUB_SHA", "")
|
|
734
|
+
base = ""
|
|
735
|
+
|
|
736
|
+
if event_path and os.path.exists(event_path):
|
|
737
|
+
with open(event_path, "r", encoding="utf-8") as f:
|
|
738
|
+
event = json.load(f)
|
|
739
|
+
if event_name == "pull_request":
|
|
740
|
+
base = (event.get("pull_request", {}) or {}).get("base", {}).get("sha", "")
|
|
741
|
+
elif event_name == "push":
|
|
742
|
+
base = event.get("before", "")
|
|
743
|
+
|
|
744
|
+
if (not base) or (set(base) == {"0"}):
|
|
745
|
+
base = subprocess.check_output(["git", "rev-list", "--max-parents=0", head]).decode().strip()
|
|
746
|
+
|
|
747
|
+
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as f:
|
|
748
|
+
f.write(f"VIBE_BASE_SHA={base}\\n")
|
|
749
|
+
f.write(f"VIBE_HEAD_SHA={head}\\n")
|
|
750
|
+
|
|
751
|
+
print(f"Base: {base}")
|
|
752
|
+
print(f"Head: {head}")
|
|
753
|
+
PY
|
|
754
|
+
|
|
755
|
+
- name: Compute changed files
|
|
756
|
+
shell: bash
|
|
757
|
+
run: |
|
|
758
|
+
set -euo pipefail
|
|
759
|
+
git diff --name-only "$VIBE_BASE_SHA" "$VIBE_HEAD_SHA" | sort -u > /tmp/vibe_changed_files.txt
|
|
760
|
+
echo "Changed files:"
|
|
761
|
+
cat /tmp/vibe_changed_files.txt || true
|
|
762
|
+
|
|
763
|
+
- name: Run Vibe guard
|
|
764
|
+
shell: bash
|
|
765
|
+
env:
|
|
766
|
+
# Default: WARN does not fail the job (seatbelt philosophy).
|
|
767
|
+
# Opt-in strict mode: set to "true" in the generated workflow.
|
|
768
|
+
VIBE_FAIL_ON_WARN: "false"
|
|
769
|
+
run: |
|
|
770
|
+
set -euo pipefail
|
|
771
|
+
if [[ ! -f ".vibe/lib/validate.sh" ]]; then
|
|
772
|
+
echo "::error title=Vibe guard missing::Missing .vibe/lib/validate.sh. Run 'vibe init' and commit generated files."
|
|
773
|
+
exit 1
|
|
774
|
+
fi
|
|
775
|
+
source ".vibe/lib/validate.sh"
|
|
776
|
+
CHANGED="$(cat /tmp/vibe_changed_files.txt)"
|
|
777
|
+
set +e
|
|
778
|
+
echo "::group::Vibe guard output"
|
|
779
|
+
output="$(validate_changes "$CHANGED" 2>&1)"
|
|
780
|
+
rc=$?
|
|
781
|
+
echo "$output"
|
|
782
|
+
echo "::endgroup::"
|
|
783
|
+
set -e
|
|
784
|
+
|
|
785
|
+
clean="$(echo "$output" | sed -r 's/\\x1B\\[[0-9;]*[mK]//g')"
|
|
786
|
+
{
|
|
787
|
+
echo "## Vibe PM Local Mode Guard"
|
|
788
|
+
echo ""
|
|
789
|
+
echo "**Changed files**"
|
|
790
|
+
echo ""
|
|
791
|
+
echo '~~~'
|
|
792
|
+
cat /tmp/vibe_changed_files.txt || true
|
|
793
|
+
echo '~~~'
|
|
794
|
+
echo ""
|
|
795
|
+
echo "**Validator output**"
|
|
796
|
+
echo ""
|
|
797
|
+
echo '~~~'
|
|
798
|
+
echo "$clean"
|
|
799
|
+
echo '~~~'
|
|
800
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
801
|
+
|
|
802
|
+
escape_anno() {
|
|
803
|
+
local s="$1"
|
|
804
|
+
s="\${s//'%'/'%25'}"
|
|
805
|
+
s="\${s//$'\\n'/'%0A'}"
|
|
806
|
+
s="\${s//$'\\r'/'%0D'}"
|
|
807
|
+
echo "$s"
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
emit_annotation() {
|
|
811
|
+
local sev="$1"
|
|
812
|
+
local file="$2"
|
|
813
|
+
local msg="$3"
|
|
814
|
+
|
|
815
|
+
msg="$(escape_anno "$msg")"
|
|
816
|
+
file="$(escape_anno "$file")"
|
|
817
|
+
|
|
818
|
+
if [[ -n "$file" ]]; then
|
|
819
|
+
echo "::\${sev} file=\${file}::\${msg}"
|
|
820
|
+
else
|
|
821
|
+
echo "::\${sev}::\${msg}"
|
|
822
|
+
fi
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
current_sev=""
|
|
826
|
+
current_msg=""
|
|
827
|
+
while IFS= read -r line; do
|
|
828
|
+
if [[ "$line" =~ ^\\[VIBE\\ WARN\\][[:space:]](.*)$ ]]; then
|
|
829
|
+
current_sev="warning"
|
|
830
|
+
current_msg="\${BASH_REMATCH[1]}"
|
|
831
|
+
current_msg="\${current_msg%:}"
|
|
832
|
+
continue
|
|
833
|
+
fi
|
|
834
|
+
if [[ "$line" =~ ^\\[VIBE\\ BLOCK\\][[:space:]](.*)$ ]]; then
|
|
835
|
+
current_sev="error"
|
|
836
|
+
current_msg="\${BASH_REMATCH[1]}"
|
|
837
|
+
current_msg="\${current_msg%:}"
|
|
838
|
+
continue
|
|
839
|
+
fi
|
|
840
|
+
if [[ -n "$current_sev" && "$line" =~ ^[[:space:]]*-[[:space:]](.+)$ ]]; then
|
|
841
|
+
emit_annotation "$current_sev" "\${BASH_REMATCH[1]}" "$current_msg"
|
|
842
|
+
fi
|
|
843
|
+
done <<< "$clean"
|
|
844
|
+
|
|
845
|
+
if [[ $rc -eq 2 ]]; then
|
|
846
|
+
echo "::error title=Vibe guard blocked::Unsafe changes blocked. Create a work order first."
|
|
847
|
+
exit 1
|
|
848
|
+
fi
|
|
849
|
+
if [[ $rc -eq 1 ]]; then
|
|
850
|
+
echo "::warning title=Vibe guard warnings::Warnings found. Merge allowed, but review recommended."
|
|
851
|
+
if [[ "$VIBE_FAIL_ON_WARN" == "true" ]]; then
|
|
852
|
+
echo "::error title=Vibe guard strict mode::WARN treated as failure (VIBE_FAIL_ON_WARN=true)."
|
|
853
|
+
exit 1
|
|
854
|
+
fi
|
|
855
|
+
fi
|
|
856
|
+
`;
|