@polderlabs/bizar 6.2.0 → 6.2.1

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.
@@ -54,8 +54,13 @@ const REQUIRED_COMMANDS = [
54
54
  'team.md', 'test.md', 'validate.md',
55
55
  ];
56
56
 
57
+ // v6.2.1 — Cline-native hook scripts (executable, shebang, no extension).
58
+ // These are real Cline hooks (https://docs.cline.bot/customization/hooks).
59
+ // The previous v6.x hook contract was markdown behavioral files, which
60
+ // Cline silently ignored — leading to the "I see skills but no hooks"
61
+ // user report.
57
62
  const REQUIRED_HOOKS = [
58
- 'pre-tool-use.md', 'post-tool-use.md', 'README.md',
63
+ 'PreToolUse', 'PostToolUse', 'TaskStart', 'TaskResume', 'UserPromptSubmit',
59
64
  ];
60
65
 
61
66
  const REQUIRED_RUNTIME_DEPS = ['zod', '@cline/sdk', '@cline/core', '@cline/shared'];
@@ -222,7 +227,42 @@ const CHECKS = {
222
227
  if (missing.length > 0) {
223
228
  throw new Error(`missing: ${missing.join(', ')} — run \`bizar update\``);
224
229
  }
225
- return `all ${REQUIRED_HOOKS.length} hook file(s) present`;
230
+ // Verify hooks are executable (Cline silently skips non-executable
231
+ // hook files per the official contract). On Windows, chmod is a
232
+ // no-op so skip the check there.
233
+ if (process.platform !== 'win32') {
234
+ const { statSync } = await import('node:fs');
235
+ const nonExec = [];
236
+ for (const f of REQUIRED_HOOKS) {
237
+ try {
238
+ const st = statSync(join(dir, f));
239
+ // 0o755 = owner=rwx, group=r-x, other=r-x. Just check owner has x.
240
+ if ((st.mode & 0o100) === 0) nonExec.push(f);
241
+ } catch {
242
+ nonExec.push(f);
243
+ }
244
+ }
245
+ if (nonExec.length > 0) {
246
+ throw new Error(`hooks not executable: ${nonExec.join(', ')} — run \`chmod +x ~/.cline/hooks/*\``);
247
+ }
248
+ }
249
+ return `all ${REQUIRED_HOOKS.length} Cline-native hook file(s) executable in ${dir}`;
250
+ },
251
+
252
+ 'hooks-canonical-location': async () => {
253
+ // Cline's default global hooks location per the official docs:
254
+ // `~/Documents/Cline/Hooks/`. Verify our hooks are also there
255
+ // (Bizar install puts them in BOTH ~/.cline/hooks/ and
256
+ // ~/Documents/Cline/Hooks/ for max compatibility).
257
+ const canonical = join(homedir(), 'Documents', 'Cline', 'Hooks');
258
+ if (!existsSync(canonical)) {
259
+ return `optional: ${canonical} does not exist yet — \`bizar install\` creates it`;
260
+ }
261
+ const missing = REQUIRED_HOOKS.filter((f) => !existsSync(join(canonical, f)));
262
+ if (missing.length > 0) {
263
+ return `optional: ${missing.length} hook(s) missing in ${canonical} — re-run \`bizar install\``;
264
+ }
265
+ return `canonical hooks dir ${canonical} has all ${REQUIRED_HOOKS.length} hook(s)`;
226
266
  },
227
267
 
228
268
  'provider-config': async () => {
@@ -325,6 +365,7 @@ const CHECK_ORDER = [
325
365
  'skills-installed',
326
366
  'rules-installed',
327
367
  'hooks-installed',
368
+ 'hooks-canonical-location',
328
369
  'default-agent-set',
329
370
  'instructions-loaded',
330
371
  'provider-config',
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { test, describe, beforeEach, afterEach } from 'node:test';
12
12
  import assert from 'node:assert/strict';
13
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, chmodSync } from 'node:fs';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { Writable } from 'node:stream';
@@ -85,8 +85,10 @@ function makeFakeClineInstall(root) {
85
85
  }
86
86
  const hooksDir = join(root, 'hooks');
87
87
  mkdirSync(hooksDir, { recursive: true });
88
- for (const h of ['pre-tool-use.md', 'post-tool-use.md', 'README.md']) {
89
- writeFileSync(join(hooksDir, h), '# fake hook');
88
+ for (const h of ['PreToolUse', 'PostToolUse', 'TaskStart', 'TaskResume', 'UserPromptSubmit']) {
89
+ // Cline hooks must be executable scripts with a shebang line.
90
+ writeFileSync(join(hooksDir, h), '#!/usr/bin/env node\n// fake hook\n');
91
+ chmodSync(join(hooksDir, h), 0o755);
90
92
  }
91
93
  const pluginDir = join(root, 'plugins', 'bizar');
92
94
  mkdirSync(pluginDir, { recursive: true });
package/cli/provision.mjs CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  import chalk from 'chalk';
40
40
  import { execSync, spawn, spawnSync } from 'node:child_process';
41
- import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync, statSync, copyFileSync } from 'node:fs';
41
+ import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync, statSync, copyFileSync, chmodSync } from 'node:fs';
42
42
  import { homedir } from 'node:os';
43
43
  import { join, dirname, sep } from 'node:path';
44
44
  import { fileURLToPath } from 'node:url';
@@ -1278,12 +1278,47 @@ export async function syncConfigExtras({ dryRun }) {
1278
1278
  }
1279
1279
 
1280
1280
  // ── Hooks ────────────────────────────────────────────────────
1281
+ // v6.2.1 — Cline hooks are real executable scripts (PreToolUse,
1282
+ // PostToolUse, TaskStart, TaskResume, UserPromptSubmit). They live in
1283
+ // `~/.cline/hooks/` AND `~/Documents/Cline/Hooks/`. Cline only loads
1284
+ // hooks from the second path (the first is for additional runtime
1285
+ // hook injection via `--hooks-dir`), but we install to BOTH so:
1286
+ // 1. `bizar validate` can verify the hooks are on disk.
1287
+ // 2. Power users who pass `--hooks-dir ~/.cline/hooks` get them.
1288
+ // 3. The official `~/Documents/Cline/Hooks/` location is canonical
1289
+ // and works without any extra config.
1290
+ //
1291
+ // Hooks must be `chmod +x` executable (Cline silently skips
1292
+ // non-executable hook files per the Cline hooks contract).
1281
1293
  const hooksSrc = join(REPO_ROOT, 'config', 'hooks');
1282
1294
  if (existsSync(hooksSrc)) {
1283
- const hooksDst = join(CLINE_DIR, 'hooks');
1284
- mkdirSync(hooksDst, { recursive: true });
1285
- await copyDirIfExists(hooksSrc, hooksDst);
1286
- counts.hooks = 1;
1295
+ const hookEntries = readdirSync(hooksSrc, { withFileTypes: true });
1296
+ const hookFiles = hookEntries
1297
+ .filter((e) => e.isFile() && !e.name.endsWith('.md') && !e.name.endsWith('.txt'))
1298
+ .map((e) => e.name);
1299
+
1300
+ // Install to ~/.cline/hooks/ (used by --hooks-dir override)
1301
+ const hooksDst1 = join(CLINE_DIR, 'hooks');
1302
+ mkdirSync(hooksDst1, { recursive: true });
1303
+ for (const hookFile of hookFiles) {
1304
+ const src = join(hooksSrc, hookFile);
1305
+ const dst = join(hooksDst1, hookFile);
1306
+ copyFileSync(src, dst);
1307
+ try { chmodSync(dst, 0o755); } catch { /* best-effort */ }
1308
+ }
1309
+
1310
+ // Also install to the canonical location: ~/Documents/Cline/Hooks/
1311
+ // (Cline's default global hooks directory per the official docs)
1312
+ const hooksDst2 = join(HOME, 'Documents', 'Cline', 'Hooks');
1313
+ mkdirSync(hooksDst2, { recursive: true });
1314
+ for (const hookFile of hookFiles) {
1315
+ const src = join(hooksSrc, hookFile);
1316
+ const dst = join(hooksDst2, hookFile);
1317
+ copyFileSync(src, dst);
1318
+ try { chmodSync(dst, 0o755); } catch { /* best-effort */ }
1319
+ }
1320
+
1321
+ counts.hooks = hookFiles.length;
1287
1322
  }
1288
1323
 
1289
1324
  // ── Rules ────────────────────────────────────────────────────
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // PostToolUse — Bizar harness hook.
3
+ //
4
+ // Runs AFTER a tool completes. Reads JSON from stdin, outputs JSON to
5
+ // stdout. This hook does NOT block (post-tool actions can't undo the
6
+ // call); it just adds context for the AI's next decision.
7
+ //
8
+ // Responsibilities:
9
+ // 1. Log tool latency for performance observability (append-only JSONL).
10
+ // 2. Surface any pre-tool warnings (e.g. console.log detected) AFTER
11
+ // the tool ran, so the model gets a second chance to fix it.
12
+ // 3. Remind the AI to run /test before claiming "done".
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ let raw = '';
21
+ process.stdin.setEncoding('utf8');
22
+ process.stdin.on('data', (chunk) => { raw += chunk; });
23
+ process.stdin.on('end', () => {
24
+ let input = {};
25
+ try { input = JSON.parse(raw); } catch { input = {}; }
26
+ const post = input.postToolUse || {};
27
+ const toolName = String(post.toolName || '');
28
+ const success = post.success !== false;
29
+ const executionTimeMs = Number(post.executionTimeMs) || 0;
30
+ const params = (post.parameters && typeof post.parameters === 'object') ? post.parameters : {};
31
+
32
+ // 1. Append-only JSONL log under ~/.config/bizar/hook-logs/
33
+ try {
34
+ const logDir = path.join(os.homedir(), '.config', 'bizar', 'hook-logs');
35
+ fs.mkdirSync(logDir, { recursive: true });
36
+ const today = new Date().toISOString().slice(0, 10);
37
+ const logFile = path.join(logDir, `post-tool-use-${today}.jsonl`);
38
+ const line = JSON.stringify({
39
+ ts: new Date().toISOString(),
40
+ tool: toolName,
41
+ success,
42
+ executionTimeMs,
43
+ sessionId: input.taskId || null,
44
+ });
45
+ fs.appendFileSync(logFile, line + '\n');
46
+ } catch { /* best-effort */ }
47
+
48
+ const out = { cancel: false, contextModification: '' };
49
+ const notes = [];
50
+
51
+ if (!success) {
52
+ notes.push(`Bizar PostToolUse: tool '${toolName}' failed. Investigate the error before proceeding.`);
53
+ } else if (executionTimeMs > 5000) {
54
+ notes.push(`Bizar PostToolUse: tool '${toolName}' took ${executionTimeMs}ms — consider whether this is worth the cost.`);
55
+ }
56
+
57
+ // Surface a reminder for write_to_file / apply_patch to src/.
58
+ const path = params.path || params.file_path || params.filePath || params.target_file || '';
59
+ if (/^write_to_file$|^apply_patch$|^replace_in_file$|^edit$/i.test(toolName) &&
60
+ /\/src\/|\/plugins\/|\/packages\//.test(String(path))) {
61
+ notes.push("Bizar PostToolUse: if you're done editing, run `/test` to gate the change with tests before claiming complete.");
62
+ }
63
+
64
+ out.contextModification = notes.join(' ');
65
+ process.stdout.write(JSON.stringify(out) + '\n');
66
+ });
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse — Bizar harness hook.
3
+ //
4
+ // Cline hook script (https://docs.cline.bot/customization/hooks).
5
+ // Runs BEFORE any tool call. Reads JSON from stdin, outputs JSON to
6
+ // stdout. Returns `{ cancel: true, errorMessage: "..." }` to block
7
+ // the tool call, or `{ cancel: false, contextModification: "..." }`
8
+ // to add context for the next AI decision.
9
+ //
10
+ // This hook enforces three Bizar harness invariants:
11
+ // 1. Block writes to .env, secrets, .lock files, node_modules.
12
+ // 2. Warn (don't block) on console.log / debugger / .only() in src/.
13
+ // 3. Always return a small context line for the next AI decision.
14
+
15
+ 'use strict';
16
+
17
+ let raw = '';
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', (chunk) => { raw += chunk; });
20
+ process.stdin.on('end', () => {
21
+ let input = {};
22
+ try { input = JSON.parse(raw); } catch { input = {}; }
23
+ const pre = input.preToolUse || {};
24
+ const toolName = String(pre.toolName || '');
25
+ const params = (pre.parameters && typeof pre.parameters === 'object') ? pre.parameters : {};
26
+
27
+ const out = { cancel: false, contextModification: '' };
28
+ const notes = [];
29
+
30
+ // Extract the most common "path" parameter (varies by tool name).
31
+ const path =
32
+ params.path ||
33
+ params.file_path ||
34
+ params.filePath ||
35
+ params.target_file ||
36
+ params.targetFile ||
37
+ params.notebook_path ||
38
+ '';
39
+
40
+ const lower = String(path).toLowerCase();
41
+
42
+ // 1. Hard block — secrets + protected paths.
43
+ const blocked = [
44
+ /\/\.env(\.|$|\/)/,
45
+ /\/\.envrc$/,
46
+ /\/secrets?\//,
47
+ /\/credentials?/,
48
+ /\/node_modules\//,
49
+ /\.(lock|lockb)$/,
50
+ /\/package-lock\.json$/,
51
+ /\/bun\.lockb?$/,
52
+ /\/yarn\.lock$/,
53
+ ];
54
+ if (path && blocked.some((re) => re.test(lower))) {
55
+ out.cancel = true;
56
+ out.errorMessage = `PreToolUse: refusing to write to protected path '${path}' (Bizar harness policy).`;
57
+ process.stdout.write(JSON.stringify(out) + '\n');
58
+ return;
59
+ }
60
+
61
+ // 2. Warn — debug artifacts in src/.
62
+ if (path && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(lower)) {
63
+ if (/\/src\//.test(lower) || /\/plugins\//.test(lower) || /\/packages\//.test(lower)) {
64
+ const text = JSON.stringify(params);
65
+ if (/\bconsole\.(log|debug|warn)\b/.test(text) && !/\bconsole\.(error|info)\b/.test(text)) {
66
+ notes.push('Heads up: console.log detected in src — `make clean-check` will fail.');
67
+ }
68
+ if (/\bdebugger\b/.test(text)) {
69
+ notes.push('Heads up: debugger statement detected — remove before committing.');
70
+ }
71
+ if (/\.only\s*\(/.test(text) && !/^[\s\S]*\.skip\s*\(/m.test(text)) {
72
+ notes.push('Heads up: .only() detected — `make clean-check` will fail.');
73
+ }
74
+ }
75
+ }
76
+
77
+ // 3. Always add a small context line for the AI's next decision.
78
+ notes.push(`Bizar PreToolUse: tool=${toolName || 'unknown'} path=${path || '(no path)'}`);
79
+
80
+ out.contextModification = notes.join(' ');
81
+ process.stdout.write(JSON.stringify(out) + '\n');
82
+ });
@@ -1,29 +1,68 @@
1
- # Hook System
1
+ # Bizar Harness — Cline Hooks
2
2
 
3
- BizarHarness uses **behavioral hooks** — agent-level instructions that simulate hook behavior. Since cline doesn't have a native hook runtime, all hooks are implemented as agent behavioral patterns.
3
+ This directory contains Cline-native hook scripts. They are NOT markdown
4
+ behavioral patterns — they are real Cline hook executables that run at
5
+ specific lifecycle events.
4
6
 
5
- ## Available Hooks
7
+ ## Format
6
8
 
7
- | Hook | Type | Trigger | Behavior |
8
- |------|------|---------|----------|
9
- | `pre-tool-use` | Behavioral | Before any edit/write | Check for secrets, verify permissions |
10
- | `post-tool-use` | Behavioral | After any edit/write | Auto-lint, auto-format, auto-test |
11
- | `session-start` | Behavioral | On session start | Read .bizar/, Hindsight recall, stack detection |
12
- | `session-end` | Behavioral | On task completion | Append to AGENTS_SELF_IMPROVEMENT.md |
13
- | `pre-commit` | Behavioral | Before git commit | Check for console.log, .env leaks, lint |
14
- | `post-implement` | Behavioral | After parallel implementation | Run test gate, verify both agents' outputs |
9
+ Cline hooks are executable scripts (no extension) with a shebang line:
15
10
 
16
- ## How It Works
11
+ ```bash
12
+ #!/usr/bin/env node
13
+ ```
17
14
 
18
- Each hook is enforced by agent instructions in the agent `.md` files:
15
+ They live in two locations:
16
+ - **Global**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
17
+ - **Workspace**: `.clinerules/hooks/` (applies to one project)
19
18
 
20
- - **Pre-tool-use**: All implementation agents check for exposed secrets before any `write` or `edit` call
21
- - **Post-tool-use**: After file modifications, agents auto-run the linter and formatter
22
- - **Session-start**: Odin reads `.bizar/PROJECT.md` and Hindsight; Heimdall runs `bizar init` if missing
23
- - **Session-end**: Odin dispatches Heimdall to auto-extract self-improvement entries
24
- - **Pre-commit**: Hermod checks for console.log and .env before allowing git commits
25
- - **Post-implement**: After Thor+Tyr complete, Odin routes to Thor for test gate
19
+ The `bizar install` / `bizar update` commands install these scripts to
20
+ **both** locations with `chmod +x` so Cline picks them up.
26
21
 
27
- ## Customization
22
+ ## What each hook does
28
23
 
29
- To disable a hook, remove its instruction from the relevant agent `.md` file(s) in `~/.config/cline/agents/`.
24
+ | Hook | Stage | Job |
25
+ |------|-------|-----|
26
+ | `TaskStart` | New task | Prime the AI with `.bizar/PROJECT.md` + memory-vault search |
27
+ | `TaskResume` | Existing task resumed | Re-read project state, check `git log` since last run |
28
+ | `UserPromptSubmit` | User submits prompt | Tag the prompt for routing (`/team`, `/plow-through`, etc.) |
29
+ | `PreToolUse` | Before every tool | Block writes to `.env`, `secrets/`, `node_modules/`, lockfiles; warn on `console.log`/`debugger`/`.only()` in `src/` |
30
+ | `PostToolUse` | After every tool | Log latency to `~/.config/bizar/hook-logs/`, remind to run `/test` after edits |
31
+
32
+ ## I/O contract
33
+
34
+ Cline invokes each hook with **JSON on stdin** and reads **JSON on stdout**.
35
+
36
+ Input shape (Cline → hook):
37
+
38
+ ```json
39
+ {
40
+ "clineVersion": "3.0.39",
41
+ "hookName": "PreToolUse",
42
+ "taskId": "...",
43
+ "preToolUse": { "toolName": "write_to_file", "parameters": { "path": "src/foo.ts", ... } }
44
+ }
45
+ ```
46
+
47
+ Output shape (hook → Cline):
48
+
49
+ ```json
50
+ {
51
+ "cancel": false,
52
+ "contextModification": "small note for the next AI decision"
53
+ }
54
+ ```
55
+
56
+ Set `cancel: true` to block the tool call. The optional `errorMessage`
57
+ is shown to the user.
58
+
59
+ ## Disabling a hook
60
+
61
+ Rename the file to add a `.disabled` suffix (e.g. `PreToolUse.disabled`)
62
+ or remove the executable bit (`chmod -x PreToolUse`). Cline silently
63
+ skips hooks it can't execute.
64
+
65
+ ## See also
66
+
67
+ - [Cline hooks documentation](https://docs.cline.bot/customization/hooks)
68
+ - `cli/commands/validate.mjs` — `bizar validate` includes a `hooks-installed` check
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // TaskResume — Bizar harness hook.
3
+ //
4
+ // Runs when an existing task is resumed. Reads JSON from stdin, outputs
5
+ // JSON to stdout. Used to remind the model of previous progress and to
6
+ // detect scope drift since the last run.
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ let raw = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', (chunk) => { raw += chunk; });
17
+ process.stdin.on('end', () => {
18
+ let input = {};
19
+ try { input = JSON.parse(raw); } catch { input = {}; }
20
+ const task = input.taskResume || {};
21
+ const previous = task.previousState || {};
22
+
23
+ try {
24
+ const logDir = path.join(os.homedir(), '.config', 'bizar', 'hook-logs');
25
+ fs.mkdirSync(logDir, { recursive: true });
26
+ const today = new Date().toISOString().slice(0, 10);
27
+ const logFile = path.join(logDir, `task-resume-${today}.jsonl`);
28
+ fs.appendFileSync(logFile, JSON.stringify({
29
+ ts: new Date().toISOString(),
30
+ taskId: input.taskId || null,
31
+ lastMessageTs: previous.lastMessageTs || null,
32
+ messageCount: previous.messageCount || null,
33
+ }) + '\n');
34
+ } catch { /* best-effort */ }
35
+
36
+ const notes = [
37
+ 'Bizar TaskResume: re-read `.bizar/PROJECT.md` and `PROGRESS.md` before continuing.',
38
+ 'Bizar TaskResume: check `git log --oneline -10` to see what changed since the last run.',
39
+ ];
40
+ if (previous.conversationHistoryDeleted) {
41
+ notes.push('Bizar TaskResume: WARNING — conversation history was deleted. Context from previous run is gone; verify scope before continuing.');
42
+ }
43
+
44
+ process.stdout.write(JSON.stringify({
45
+ cancel: false,
46
+ contextModification: notes.join(' '),
47
+ }) + '\n');
48
+ });
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // TaskStart — Bizar harness hook.
3
+ //
4
+ // Runs when a NEW task is started (not when resuming). Reads JSON from
5
+ // stdin, outputs JSON to stdout. Used to prime the AI with project
6
+ // context and to record the start of a session.
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ let raw = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', (chunk) => { raw += chunk; });
17
+ process.stdin.on('end', () => {
18
+ let input = {};
19
+ try { input = JSON.parse(raw); } catch { input = {}; }
20
+ const task = input.taskStart || {};
21
+ const initial = String(task.taskMetadata?.initialTask || '').slice(0, 500);
22
+
23
+ // Log task start to ~/.config/bizar/hook-logs/
24
+ try {
25
+ const logDir = path.join(os.homedir(), '.config', 'bizar', 'hook-logs');
26
+ fs.mkdirSync(logDir, { recursive: true });
27
+ const today = new Date().toISOString().slice(0, 10);
28
+ const logFile = path.join(logDir, `task-start-${today}.jsonl`);
29
+ fs.appendFileSync(logFile, JSON.stringify({
30
+ ts: new Date().toISOString(),
31
+ taskId: input.taskId || null,
32
+ clineVersion: input.clineVersion || null,
33
+ initial: initial,
34
+ }) + '\n');
35
+ } catch { /* best-effort */ }
36
+
37
+ const notes = [
38
+ 'Bizar TaskStart: read `.bizar/PROJECT.md` (if present) before any routing decision.',
39
+ 'Bizar TaskStart: if memory vault exists, run `bizar memory search "<topic>"` first.',
40
+ 'Bizar TaskStart: Odin dispatches to subagents via `task` (sync) or `bizar_spawn_background` (async).',
41
+ ];
42
+
43
+ process.stdout.write(JSON.stringify({
44
+ cancel: false,
45
+ contextModification: notes.join(' '),
46
+ }) + '\n');
47
+ });
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit — Bizar harness hook.
3
+ //
4
+ // Runs when the user submits a prompt. Reads JSON from stdin, outputs
5
+ // JSON to stdout. Used to lightly tag user prompts for routing and to
6
+ // add a "Plow Through" prompt hint when ambiguity is detected.
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ let raw = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', (chunk) => { raw += chunk; });
17
+ process.stdin.on('end', () => {
18
+ let input = {};
19
+ try { input = JSON.parse(raw); } catch { input = {}; }
20
+ const submit = input.userPromptSubmit || {};
21
+ const prompt = String(submit.prompt || '').trim();
22
+
23
+ // Always log (no PII redaction needed — the prompt is the user's).
24
+ try {
25
+ const logDir = path.join(os.homedir(), '.config', 'bizar', 'hook-logs');
26
+ fs.mkdirSync(logDir, { recursive: true });
27
+ const today = new Date().toISOString().slice(0, 10);
28
+ const logFile = path.join(logDir, `user-prompt-${today}.jsonl`);
29
+ fs.appendFileSync(logFile, JSON.stringify({
30
+ ts: new Date().toISOString(),
31
+ taskId: input.taskId || null,
32
+ promptLength: prompt.length,
33
+ }) + '\n');
34
+ } catch { /* best-effort */ }
35
+
36
+ const notes = [];
37
+ if (prompt.length === 0) {
38
+ notes.push('Bizar UserPromptSubmit: empty prompt — wait for actual user input.');
39
+ } else if (prompt.startsWith('/team')) {
40
+ notes.push('Bizar UserPromptSubmit: /team — Odin will spawn a coordinated agent team. Confirm disjoint file scopes before dispatching.');
41
+ } else if (prompt.startsWith('/plow-through')) {
42
+ notes.push('Bizar UserPromptSubmit: /plow-through — autonomous mode. Dispatch 2+ parallel agents when possible. Run /test before claiming done.');
43
+ } else if (prompt.startsWith('/test')) {
44
+ notes.push('Bizar UserPromptSubmit: /test — auto-detects runner (jest/vitest/bun/pytest/cargo/go). Streams output to the user.');
45
+ } else if (prompt.startsWith('/validate')) {
46
+ notes.push('Bizar UserPromptSubmit: /validate — runs 21-point health check on the Bizar install.');
47
+ } else {
48
+ notes.push('Bizar UserPromptSubmit: if the request is complex, decompose into 2+ parallel subagent tasks (Odin) or spawn a /team.');
49
+ }
50
+
51
+ process.stdout.write(JSON.stringify({
52
+ cancel: false,
53
+ contextModification: notes.join(' '),
54
+ }) + '\n');
55
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "6.2.0",
3
+ "version": "6.2.1",
4
4
  "description": "Norse-pantheon multi-agent system for cline — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, cline plugin, and typed SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-plugin",
3
- "version": "6.2.0",
3
+ "version": "6.2.1",
4
4
  "description": "Bizar Norse-pantheon multi-agent plugin for Cline — 14 agents across 4 cost tiers with cost-aware routing and plans.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -249,6 +249,39 @@ try {
249
249
  record('config/rules/ scannable', false, err.message);
250
250
  }
251
251
 
252
+ // ── 6.5. Verify config/hooks/ has Cline-native executable hooks ────
253
+ // Cline hooks are real scripts (not markdown). They must have a
254
+ // shebang line and the right names. The previous v6.x format used
255
+ // markdown behavioral files which Cline ignored.
256
+ try {
257
+ const { readFileSync, readdirSync, existsSync } = await import('node:fs');
258
+ const hooksDir = join(REPO_ROOT, 'config', 'hooks');
259
+ if (!existsSync(hooksDir)) {
260
+ record('config/hooks/ present', false, `${hooksDir} missing`);
261
+ } else {
262
+ const files = readdirSync(hooksDir);
263
+ const required = ['PreToolUse', 'PostToolUse', 'TaskStart', 'TaskResume', 'UserPromptSubmit'];
264
+ const missing = required.filter((f) => !files.includes(f));
265
+ if (missing.length > 0) {
266
+ record('config/hooks/ has 5 Cline-native hook scripts', false, `missing: ${missing.join(', ')}`);
267
+ } else {
268
+ // Verify each has a shebang line (Cline requires it).
269
+ const noShebang = [];
270
+ for (const f of required) {
271
+ const first = readFileSync(join(hooksDir, f), 'utf8').split('\n')[0] || '';
272
+ if (!first.startsWith('#!')) noShebang.push(f);
273
+ }
274
+ if (noShebang.length > 0) {
275
+ record('Cline hooks have shebang', false, `no shebang: ${noShebang.join(', ')}`);
276
+ } else {
277
+ record('config/hooks/ has 5 Cline-native executable hooks', true, required.join(', '));
278
+ }
279
+ }
280
+ }
281
+ } catch (err) {
282
+ record('config/hooks/ scannable', false, err.message);
283
+ }
284
+
252
285
  // ── 7. Verify plugin index.ts is well-formed ───────────────────
253
286
  try {
254
287
  const { readFileSync } = await import('node:fs');
@@ -1,16 +0,0 @@
1
- # Post-Tool-Use Hook
2
-
3
- Enforced by: All implementation agents (Heimdall, Thor, Tyr, Vidarr)
4
-
5
- ## Checklist
6
-
7
- After every `write` or `edit` operation:
8
-
9
- 1. **Format**: Run the project formatter (Prettier, Black, ruff, gofmt, rustfmt)
10
- 2. **Lint**: Run the linter on the modified file
11
- 3. **Typecheck**: Run the type checker (tsc, mypy, etc.) if applicable
12
- 4. **Quick test**: If modifying tests, run just the affected test file
13
-
14
- ## Enforcement
15
-
16
- Run these automatically. Do not ask for permission — the hook enforces quality.
@@ -1,16 +0,0 @@
1
- # Pre-Tool-Use Hook
2
-
3
- Enforced by: All implementation agents (Heimdall, Thor, Tyr, Vidarr)
4
-
5
- ## Checklist
6
-
7
- Before every `write` or `edit` operation, check:
8
-
9
- 1. **Secrets**: Does the content contain API keys, tokens, or passwords? (`sk-...`, `AIza...`, etc.)
10
- 2. **Console.log**: Is debug logging being committed? Use proper logging instead.
11
- 3. **Permissions**: Are you writing to a file you should not modify? (e.g., `.env`, `node_modules/`, `*.lock`)
12
- 4. **Size**: Is the file too large? Consider splitting into smaller modules.
13
-
14
- ## Enforcement
15
-
16
- Block the operation if any check fails. Fix the issue first, then proceed.