dual-brain 2.0.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/CLAUDE.md +40 -0
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/hookify.orchestrator-cost.local.md +16 -0
- package/hookify.orchestrator-gate.local.md +19 -0
- package/hookify.orchestrator-route.local.md +23 -0
- package/hooks/budget-balancer.mjs +463 -0
- package/hooks/cost-logger.mjs +250 -0
- package/hooks/cost-report.mjs +344 -0
- package/hooks/dual-brain-review.mjs +302 -0
- package/hooks/dual-brain-think.mjs +321 -0
- package/hooks/enforce-tier.mjs +282 -0
- package/hooks/gpt-work-dispatcher.mjs +254 -0
- package/hooks/health-check.mjs +390 -0
- package/hooks/install-git-hooks.mjs +106 -0
- package/hooks/quality-gate.mjs +283 -0
- package/hooks/session-report.mjs +514 -0
- package/hooks/setup-wizard.mjs +130 -0
- package/hooks/test-orchestrator.mjs +316 -0
- package/install.mjs +153 -0
- package/orchestrator.json +215 -0
- package/package.json +38 -0
- package/review-rules.md +17 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* health-check.mjs — Dual-Brain Orchestrator Health Check
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node .claude/hooks/health-check.mjs
|
|
7
|
+
*
|
|
8
|
+
* Validates that all hooks are wired, configs are valid, and the system
|
|
9
|
+
* is functioning in a live session. Always exits 0 and outputs valid JSON.
|
|
10
|
+
*
|
|
11
|
+
* Checks:
|
|
12
|
+
* 1. orchestrator.json — exists and parses as valid JSON
|
|
13
|
+
* 2. pricing_verified — exists, warn if >30 days, fail if >90 days
|
|
14
|
+
* 3. model_intelligence — exists and covers all subscription models
|
|
15
|
+
* 4. hook scripts — enforce-tier, cost-logger, quality-gate, dual-brain-review readable
|
|
16
|
+
* 5. usage.jsonl active — recent entries (last 15 min) indicate PostToolUse hook is wired
|
|
17
|
+
* 6. codex CLI — found on PATH or known locations; auth status checked
|
|
18
|
+
* 7. git repo — working directory is inside a git repo
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, accessSync, readFileSync, constants } from "fs";
|
|
22
|
+
import { dirname, join, resolve } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import { spawnSync } from "child_process";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Paths
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const HOOKS_DIR = __dirname;
|
|
31
|
+
const CONFIG_FILE = join(__dirname, "..", "orchestrator.json");
|
|
32
|
+
const USAGE_FILE_LEGACY = join(__dirname, "usage.jsonl");
|
|
33
|
+
const USAGE_FILE_TODAY = join(__dirname, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
34
|
+
const WORKSPACE = join(__dirname, "..", "..");
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Status helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const STATUS = { pass: "pass", warn: "warn", fail: "fail" };
|
|
40
|
+
|
|
41
|
+
function check(name, status, detail) {
|
|
42
|
+
return { name, status, detail };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Check implementations
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/** 1. orchestrator.json — exists and parses as valid JSON */
|
|
50
|
+
function checkOrchestratorJson() {
|
|
51
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
52
|
+
return check("orchestrator.json", STATUS.fail, "file not found");
|
|
53
|
+
}
|
|
54
|
+
let config;
|
|
55
|
+
try {
|
|
56
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return check("orchestrator.json", STATUS.fail, `invalid JSON: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
if (!config.subscriptions || !config.tiers) {
|
|
61
|
+
return check("orchestrator.json", STATUS.warn, "parsed but missing expected keys");
|
|
62
|
+
}
|
|
63
|
+
return check("orchestrator.json", STATUS.pass, "valid");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 2. pricing_verified — exists in config, warn if >30 days, fail if >90 days */
|
|
67
|
+
function checkPricingVerified() {
|
|
68
|
+
let config;
|
|
69
|
+
try {
|
|
70
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return check("pricing_verified", STATUS.fail, "cannot read config");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const val = config.pricing_verified;
|
|
76
|
+
if (!val) {
|
|
77
|
+
return check("pricing_verified", STATUS.fail, "field missing from config");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ts = Date.parse(val);
|
|
81
|
+
if (isNaN(ts)) {
|
|
82
|
+
return check("pricing_verified", STATUS.fail, `not a valid date: ${val}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ageMs = Date.now() - ts;
|
|
86
|
+
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
87
|
+
|
|
88
|
+
if (ageDays > 90) {
|
|
89
|
+
return check("pricing_verified", STATUS.fail, `${ageDays} days ago — update pricing`);
|
|
90
|
+
}
|
|
91
|
+
if (ageDays > 30) {
|
|
92
|
+
return check("pricing_verified", STATUS.warn, `${ageDays} days ago — consider refreshing`);
|
|
93
|
+
}
|
|
94
|
+
return check("pricing_verified", STATUS.pass, `${ageDays} days ago`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 3. model_intelligence — exists and has entries for at least the subscription models */
|
|
98
|
+
function checkModelIntelligence() {
|
|
99
|
+
let config;
|
|
100
|
+
try {
|
|
101
|
+
config = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
102
|
+
} catch {
|
|
103
|
+
return check("model_intelligence", STATUS.fail, "cannot read config");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mi = config.model_intelligence;
|
|
107
|
+
if (!mi || typeof mi !== "object") {
|
|
108
|
+
return check("model_intelligence", STATUS.fail, "key missing from config");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Collect model keys from subscriptions
|
|
112
|
+
const subscriptionModels = new Set();
|
|
113
|
+
for (const provider of Object.values(config.subscriptions || {})) {
|
|
114
|
+
for (const key of Object.keys(provider.models || {})) {
|
|
115
|
+
subscriptionModels.add(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const miKeys = Object.keys(mi);
|
|
120
|
+
const missing = [...subscriptionModels].filter((m) => !mi[m]);
|
|
121
|
+
const entryCount = miKeys.length;
|
|
122
|
+
|
|
123
|
+
if (missing.length > 0) {
|
|
124
|
+
return check(
|
|
125
|
+
"model_intelligence",
|
|
126
|
+
STATUS.warn,
|
|
127
|
+
`${entryCount} models, missing: ${missing.join(", ")}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return check("model_intelligence", STATUS.pass, `${entryCount} models`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 4. Hook scripts readable */
|
|
134
|
+
function checkHookScripts() {
|
|
135
|
+
const hooks = [
|
|
136
|
+
"enforce-tier.mjs",
|
|
137
|
+
"cost-logger.mjs",
|
|
138
|
+
"quality-gate.mjs",
|
|
139
|
+
"dual-brain-review.mjs",
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const results = hooks.map((name) => {
|
|
143
|
+
const p = join(HOOKS_DIR, name);
|
|
144
|
+
try {
|
|
145
|
+
accessSync(p, constants.R_OK);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const readableCount = results.filter(Boolean).length;
|
|
153
|
+
const total = hooks.length;
|
|
154
|
+
|
|
155
|
+
if (readableCount === total) {
|
|
156
|
+
return check("hook scripts", STATUS.pass, `${readableCount}/${total} readable`);
|
|
157
|
+
}
|
|
158
|
+
if (readableCount === 0) {
|
|
159
|
+
return check("hook scripts", STATUS.fail, `0/${total} readable`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const missing = hooks.filter((_, i) => !results[i]);
|
|
163
|
+
return check(
|
|
164
|
+
"hook scripts",
|
|
165
|
+
STATUS.warn,
|
|
166
|
+
`${readableCount}/${total} readable, missing: ${missing.join(", ")}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 5. usage log active — check dated files and legacy for entries from last 15 minutes */
|
|
171
|
+
function checkUsageJsonl() {
|
|
172
|
+
const usageFile = existsSync(USAGE_FILE_TODAY) ? USAGE_FILE_TODAY
|
|
173
|
+
: existsSync(USAGE_FILE_LEGACY) ? USAGE_FILE_LEGACY
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
if (!usageFile) {
|
|
177
|
+
return check("usage log", STATUS.warn, "no usage files found — PostToolUse hook may not be wired");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let lines;
|
|
181
|
+
try {
|
|
182
|
+
lines = readFileSync(usageFile, "utf8").split("\n").filter(Boolean);
|
|
183
|
+
} catch {
|
|
184
|
+
return check("usage log", STATUS.warn, "file unreadable");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (lines.length === 0) {
|
|
188
|
+
return check("usage log", STATUS.warn, "file empty — PostToolUse hook may not be wired");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const fifteenMinAgo = Date.now() - 15 * 60 * 1000;
|
|
192
|
+
let recentCount = 0;
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
try {
|
|
196
|
+
const entry = JSON.parse(line);
|
|
197
|
+
if (entry.timestamp && Date.parse(entry.timestamp) >= fifteenMinAgo) {
|
|
198
|
+
recentCount++;
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (recentCount === 0) {
|
|
204
|
+
return check(
|
|
205
|
+
"usage log",
|
|
206
|
+
STATUS.warn,
|
|
207
|
+
`${lines.length} entries, none in last 15 min — PostToolUse hook may not be wired`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return check("usage log", STATUS.pass, `${recentCount} recent entries`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 6. Codex CLI available and authenticated */
|
|
215
|
+
function checkCodexCli() {
|
|
216
|
+
// Try which first
|
|
217
|
+
const whichResult = spawnSync("which", ["codex"], {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
220
|
+
timeout: 5_000,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const knownPaths = [
|
|
224
|
+
"/usr/local/bin/codex",
|
|
225
|
+
"/usr/bin/codex",
|
|
226
|
+
join(process.env.HOME || "/root", ".local/bin/codex"),
|
|
227
|
+
join(process.env.HOME || "/root", "bin/codex"),
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
let codexPath = null;
|
|
231
|
+
|
|
232
|
+
if (whichResult.status === 0 && whichResult.stdout.trim()) {
|
|
233
|
+
codexPath = whichResult.stdout.trim();
|
|
234
|
+
} else {
|
|
235
|
+
codexPath = knownPaths.find((p) => existsSync(p)) || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!codexPath) {
|
|
239
|
+
return check(
|
|
240
|
+
"codex CLI",
|
|
241
|
+
STATUS.warn,
|
|
242
|
+
"not found — dual-brain review won't work without Codex CLI"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Try `codex login status` with 5s timeout
|
|
247
|
+
const loginResult = spawnSync(codexPath, ["login", "status"], {
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
250
|
+
timeout: 5_000,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (loginResult.signal === "SIGTERM" || loginResult.status == null) {
|
|
254
|
+
return check("codex CLI", STATUS.warn, `found at ${codexPath} — auth check timed out`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const output = (loginResult.stdout + loginResult.stderr).toLowerCase();
|
|
258
|
+
|
|
259
|
+
if (loginResult.status === 0 || output.includes("logged in") || output.includes("authenticated")) {
|
|
260
|
+
return check("codex CLI", STATUS.pass, "authenticated");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (output.includes("not logged") || output.includes("unauthenticated") || output.includes("login")) {
|
|
264
|
+
return check("codex CLI", STATUS.warn, `found at ${codexPath} — not authenticated`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Unknown output — still found, just can't confirm auth
|
|
268
|
+
return check("codex CLI", STATUS.warn, `found at ${codexPath} — auth status unknown`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 7. Git repo — verify we're in a git repo */
|
|
272
|
+
function checkGitRepo() {
|
|
273
|
+
const result = spawnSync("git", ["-C", WORKSPACE, "status", "--porcelain"], {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
276
|
+
timeout: 5_000,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (result.status !== 0) {
|
|
280
|
+
return check("git repo", STATUS.fail, "not a git repository — quality gate needs this");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const dirty = (result.stdout || "").trim().length > 0;
|
|
284
|
+
return check("git repo", STATUS.pass, dirty ? "dirty" : "clean");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Table rendering
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
const ICON = {
|
|
292
|
+
pass: "✓",
|
|
293
|
+
warn: "⚠",
|
|
294
|
+
fail: "✗",
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const W = 54; // inner width between ║ chars
|
|
298
|
+
|
|
299
|
+
function pad(str, len, align = "left") {
|
|
300
|
+
str = String(str);
|
|
301
|
+
if (str.length >= len) return str.slice(0, len);
|
|
302
|
+
const spaces = " ".repeat(len - str.length);
|
|
303
|
+
return align === "right" ? spaces + str : str + spaces;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function boxLine(content) {
|
|
307
|
+
return `║ ${pad(content, W - 2)} ║`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function boxSep() {
|
|
311
|
+
return "╠" + "═".repeat(W) + "╣";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function boxTop() {
|
|
315
|
+
return "╔" + "═".repeat(W) + "╗";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function boxBot() {
|
|
319
|
+
return "╚" + "═".repeat(W) + "╝";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderTable(checks) {
|
|
323
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
324
|
+
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
325
|
+
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
326
|
+
|
|
327
|
+
const nameWidth = 20;
|
|
328
|
+
const detailWidth = W - 2 - 2 - nameWidth - 1; // icon + space + name + space + detail
|
|
329
|
+
|
|
330
|
+
const rows = checks.map((c) => {
|
|
331
|
+
const icon = ICON[c.status] || "?";
|
|
332
|
+
const name = pad(c.name, nameWidth);
|
|
333
|
+
const detail = pad(c.detail, detailWidth);
|
|
334
|
+
return boxLine(`${icon} ${name} ${detail}`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const summary = `${passCount} pass, ${warnCount} warn, ${failCount} fail`;
|
|
338
|
+
|
|
339
|
+
const lines = [
|
|
340
|
+
boxTop(),
|
|
341
|
+
boxLine(pad("Orchestrator Health Check", W - 2)),
|
|
342
|
+
boxSep(),
|
|
343
|
+
...rows,
|
|
344
|
+
boxSep(),
|
|
345
|
+
boxLine(summary),
|
|
346
|
+
boxBot(),
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Main
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
function main() {
|
|
357
|
+
const checks = [
|
|
358
|
+
checkOrchestratorJson(),
|
|
359
|
+
checkPricingVerified(),
|
|
360
|
+
checkModelIntelligence(),
|
|
361
|
+
checkHookScripts(),
|
|
362
|
+
checkUsageJsonl(),
|
|
363
|
+
checkCodexCli(),
|
|
364
|
+
checkGitRepo(),
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
// Print formatted table
|
|
368
|
+
console.log(renderTable(checks));
|
|
369
|
+
console.log();
|
|
370
|
+
|
|
371
|
+
// Build JSON summary
|
|
372
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
373
|
+
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
374
|
+
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
375
|
+
const healthy = failCount === 0;
|
|
376
|
+
|
|
377
|
+
const output = {
|
|
378
|
+
healthy,
|
|
379
|
+
pass: passCount,
|
|
380
|
+
warn: warnCount,
|
|
381
|
+
fail: failCount,
|
|
382
|
+
checks: checks.map((c) => ({ name: c.name, status: c.status, detail: c.detail })),
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
console.log(JSON.stringify(output, null, 2));
|
|
386
|
+
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
main();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// install-git-hooks.mjs — installs a git pre-commit hook that enforces the quality gate
|
|
3
|
+
// Usage: node .claude/hooks/install-git-hooks.mjs
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync, appendFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const MARKER = '# dual-brain-orchestrator';
|
|
10
|
+
|
|
11
|
+
// Shell block that runs the quality gate. Embedded as a template literal so the
|
|
12
|
+
// actual newlines are preserved when written to disk.
|
|
13
|
+
const GATE_BLOCK = `
|
|
14
|
+
${MARKER} quality gate
|
|
15
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
16
|
+
GATE_RESULT=$(node "$REPO_ROOT/.claude/hooks/quality-gate.mjs" 2>/dev/null)
|
|
17
|
+
GATE_STATUS=$(echo "$GATE_RESULT" | node -e "
|
|
18
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
19
|
+
try{console.log(JSON.parse(d).gate)}catch{console.log('error')}
|
|
20
|
+
})
|
|
21
|
+
")
|
|
22
|
+
|
|
23
|
+
case "$GATE_STATUS" in
|
|
24
|
+
pass|disabled)
|
|
25
|
+
exit 0
|
|
26
|
+
;;
|
|
27
|
+
issues_found)
|
|
28
|
+
echo ""
|
|
29
|
+
echo "╔══════════════════════════════════════════════════╗"
|
|
30
|
+
echo "║ Quality Gate: ISSUES FOUND ║"
|
|
31
|
+
echo "╠══════════════════════════════════════════════════╣"
|
|
32
|
+
echo "║ GPT review flagged issues in your changes. ║"
|
|
33
|
+
echo "║ Check .claude/reviews/ for details. ║"
|
|
34
|
+
echo "║ ║"
|
|
35
|
+
echo "║ To commit anyway: git commit --no-verify ║"
|
|
36
|
+
echo "╚══════════════════════════════════════════════════╝"
|
|
37
|
+
echo ""
|
|
38
|
+
exit 1
|
|
39
|
+
;;
|
|
40
|
+
needs_human_review)
|
|
41
|
+
echo ""
|
|
42
|
+
echo "╔══════════════════════════════════════════════════╗"
|
|
43
|
+
echo "║ Quality Gate: NEEDS HUMAN REVIEW ║"
|
|
44
|
+
echo "╠══════════════════════════════════════════════════╣"
|
|
45
|
+
echo "║ GPT review unavailable. Review your diff ║"
|
|
46
|
+
echo "║ manually before committing. ║"
|
|
47
|
+
echo "║ ║"
|
|
48
|
+
echo "║ To commit anyway: git commit --no-verify ║"
|
|
49
|
+
echo "╚══════════════════════════════════════════════════╝"
|
|
50
|
+
echo ""
|
|
51
|
+
exit 1
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
echo "[Quality Gate] Warning: gate returned '$GATE_STATUS'"
|
|
55
|
+
exit 0
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const FULL_SCRIPT = `#!/bin/sh
|
|
61
|
+
${GATE_BLOCK.trimStart()}`;
|
|
62
|
+
|
|
63
|
+
function getGitHooksDir() {
|
|
64
|
+
try {
|
|
65
|
+
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
66
|
+
return join(gitDir, 'hooks');
|
|
67
|
+
} catch {
|
|
68
|
+
console.error('Error: not inside a git repository (git rev-parse --git-dir failed).');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main() {
|
|
74
|
+
const hooksDir = getGitHooksDir();
|
|
75
|
+
const hookPath = join(hooksDir, 'pre-commit');
|
|
76
|
+
|
|
77
|
+
if (existsSync(hookPath)) {
|
|
78
|
+
const existing = readFileSync(hookPath, 'utf8');
|
|
79
|
+
|
|
80
|
+
if (existing.includes(MARKER)) {
|
|
81
|
+
console.log(
|
|
82
|
+
'Pre-commit hook already contains the dual-brain-orchestrator quality gate. Nothing to do.'
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Append our block without overwriting the user's existing hook.
|
|
88
|
+
console.log('Pre-commit hook already exists — appending quality gate block.');
|
|
89
|
+
appendFileSync(hookPath, GATE_BLOCK, 'utf8');
|
|
90
|
+
} else {
|
|
91
|
+
writeFileSync(hookPath, FULL_SCRIPT, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
chmodSync(hookPath, 0o755);
|
|
95
|
+
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('╔══════════════════════════════════════════════════╗');
|
|
98
|
+
console.log('║ Git Pre-Commit Hook Installed ║');
|
|
99
|
+
console.log('╠══════════════════════════════════════════════════╣');
|
|
100
|
+
console.log('║ Quality gate will run before every commit. ║');
|
|
101
|
+
console.log('║ Use --no-verify to bypass when needed. ║');
|
|
102
|
+
console.log('╚══════════════════════════════════════════════════╝');
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main();
|