@polderlabs/bizar 10.12.1 → 10.12.2
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/hooks/git-workflow-guard.mjs +14 -10
- package/.claude/hooks/path-ownership-guard.mjs +7 -11
- package/.claude/hooks/pretooluse-editwrite.mjs +17 -39
- package/.claude/hooks/simplify-guard.mjs +17 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli/commands/hook.mjs +5 -8
- package/package.json +1 -1
- package/packages/sdk/dist/version.d.ts +1 -1
- package/packages/sdk/dist/version.js +1 -1
- package/packages/sdk/package.json +1 -1
|
@@ -10,7 +10,6 @@ import { spawnSync } from 'node:child_process';
|
|
|
10
10
|
import { findGitCommand } from './git-command-parser.mjs';
|
|
11
11
|
|
|
12
12
|
const ALLOWED_COMMIT_TYPES = ['feat', 'fix', 'refactor', 'docs', 'style', 'test', 'build', 'chore'];
|
|
13
|
-
const AI_ATTRIBUTION = /Claude-Session:|Co-Authored-By:\s*(?:Claude|Codex|ChatGPT|OpenAI|Gemini|Cursor|Copilot)|Generated with[^\n]*Claude Code|claude\.ai\/code\/session/i;
|
|
14
13
|
const GH_GLOBAL_OPTION = String.raw`(?:(?:-R|--repo|--hostname)\s+(?:"[^"]*"|'[^']*'|\S+)|--(?:help|version))`;
|
|
15
14
|
|
|
16
15
|
function commandPattern(program, globalOption, subcommand) {
|
|
@@ -82,19 +81,24 @@ process.stdin.on('end', () => {
|
|
|
82
81
|
output('deny', 'Rebasing rewrites history. Use a merge or follow-up commit unless the user explicitly changes project policy.');
|
|
83
82
|
return;
|
|
84
83
|
}
|
|
85
|
-
if (AI_ATTRIBUTION.test(command) && (commit || hasGhCommand(command, 'pr', '\\w+'))) {
|
|
86
|
-
output('deny', 'Remove AI-attribution trailers or generated-by links from the commit or pull-request text.');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
84
|
if (commit) {
|
|
91
85
|
const message = commitMessage(command);
|
|
92
86
|
const subject = message.split('\n').find((line) => line.trim())?.trim() ?? '';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
// F-145 loosening: subject-shape check is a soft warning attached to
|
|
88
|
+
// the same hook response as the `ask` decision (the dispatcher
|
|
89
|
+
// expects one JSON line per leaf).
|
|
90
|
+
const conventional = subject && !new RegExp(`^(?:${ALLOWED_COMMIT_TYPES.join('|')})(\\([^)]+\\))?:\\s+\\S`, 'i').test(subject);
|
|
91
|
+
const payload = {
|
|
92
|
+
hookSpecificOutput: {
|
|
93
|
+
hookEventName: 'PreToolUse',
|
|
94
|
+
permissionDecision: 'ask',
|
|
95
|
+
permissionDecisionReason: `Create this local commit${subject ? `: ${subject}` : ''}?`,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
if (conventional) {
|
|
99
|
+
payload.hookSpecificOutput.additionalContext = `Conventional commit hint: prefer "type: subject" (types: ${ALLOWED_COMMIT_TYPES.join(', ')}).`;
|
|
96
100
|
}
|
|
97
|
-
|
|
101
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
98
102
|
return;
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// PreToolUse — deny edits outside the current task scope or inside a path
|
|
3
3
|
// leased by a sibling worktree.
|
|
4
|
+
//
|
|
5
|
+
// F-145 loosening: removed the per-edit `git worktree list --porcelain`
|
|
6
|
+
// fork and the `requireTask` gate. The hook is now a single in-memory
|
|
7
|
+
// ledger lookup. Edits inside the project are allowed by default; only
|
|
8
|
+
// an active lease held by another agent on the same path denies.
|
|
4
9
|
|
|
5
10
|
import { existsSync } from 'node:fs';
|
|
6
11
|
import { resolve } from 'node:path';
|
|
@@ -36,20 +41,11 @@ process.stdin.on('end', () => {
|
|
|
36
41
|
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
37
42
|
cwd,
|
|
38
43
|
encoding: 'utf8',
|
|
39
|
-
|
|
40
|
-
const worktrees = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
41
|
-
cwd,
|
|
42
|
-
encoding: 'utf8',
|
|
44
|
+
timeout: 3_000,
|
|
43
45
|
});
|
|
44
46
|
const repoRoot = top.status === 0 && top.stdout.trim()
|
|
45
47
|
? resolve(top.stdout.trim())
|
|
46
48
|
: cwd;
|
|
47
|
-
const mainWorktree = worktrees.status === 0
|
|
48
|
-
? /^worktree (.+)$/m.exec(worktrees.stdout)?.[1]
|
|
49
|
-
: null;
|
|
50
|
-
const requireTask = Boolean(
|
|
51
|
-
mainWorktree && resolve(mainWorktree) !== repoRoot,
|
|
52
|
-
);
|
|
53
49
|
|
|
54
50
|
let ledger;
|
|
55
51
|
try {
|
|
@@ -58,7 +54,7 @@ process.stdin.on('end', () => {
|
|
|
58
54
|
cwd,
|
|
59
55
|
filePath,
|
|
60
56
|
repoRoot,
|
|
61
|
-
requireTask,
|
|
57
|
+
requireTask: false,
|
|
62
58
|
});
|
|
63
59
|
if (!authorization.allowed) {
|
|
64
60
|
process.stdout.write(JSON.stringify({
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
// }}
|
|
28
28
|
//
|
|
29
29
|
// Behaviour:
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// Block writes to .env, .envrc, secrets/, credentials/, node_modules/.
|
|
31
|
+
// .env.example/.sample/.template are explicitly allowed (docs, not
|
|
32
|
+
// secrets). Lockfiles are allowed — they're package-manager output.
|
|
33
|
+
//
|
|
34
|
+
// F-145: removed the always-on `additionalContext` line. Every Write/
|
|
35
|
+
// Edit was emitting a tool/path note into the model's context — pure
|
|
36
|
+
// noise. The model already knows what tool and path it called.
|
|
36
37
|
|
|
37
38
|
'use strict';
|
|
38
39
|
|
|
@@ -47,25 +48,21 @@ process.stdin.on('end', () => {
|
|
|
47
48
|
const toolInput =
|
|
48
49
|
(input.tool_input && typeof input.tool_input === 'object') ? input.tool_input : {};
|
|
49
50
|
|
|
50
|
-
// Extract the "file_path" mapped onto Claude Code's Write/Edit/MultiEdit
|
|
51
|
-
// shapes. Content scanning was removed (debug artifacts are enforced at
|
|
52
|
-
// commit time via `make clean-check`, not write time).
|
|
53
51
|
let filePath = '';
|
|
54
|
-
if (
|
|
55
|
-
filePath = String(toolInput.file_path || '');
|
|
56
|
-
} else if (toolName === 'Edit') {
|
|
57
|
-
filePath = String(toolInput.file_path || '');
|
|
58
|
-
} else if (toolName === 'MultiEdit') {
|
|
52
|
+
if (/^(Write|Edit|MultiEdit)$/.test(toolName)) {
|
|
59
53
|
filePath = String(toolInput.file_path || '');
|
|
60
54
|
} else {
|
|
61
55
|
process.stdout.write('{}\n');
|
|
62
56
|
return;
|
|
63
57
|
}
|
|
64
58
|
|
|
59
|
+
if (!filePath) {
|
|
60
|
+
process.stdout.write('{}\n');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
65
64
|
const lowerPath = filePath.toLowerCase();
|
|
66
65
|
|
|
67
|
-
// 1. Hard block — secrets + protected paths. .env.example/.sample/.template
|
|
68
|
-
// and all lockfiles are explicitly allowed (docs / package-manager output).
|
|
69
66
|
const allowed = [
|
|
70
67
|
/\/\.env\.(example|sample|template|dist)$/i,
|
|
71
68
|
/\/(package-lock|yarn|pnpm-lock|bun)\.lock\w*$/i,
|
|
@@ -80,38 +77,19 @@ process.stdin.on('end', () => {
|
|
|
80
77
|
/\/node_modules\//,
|
|
81
78
|
];
|
|
82
79
|
|
|
83
|
-
const isAllowed =
|
|
84
|
-
|
|
85
|
-
if (filePath && !isAllowed && blocked.some((re) => re.test(lowerPath))) {
|
|
86
|
-
blockReason = filePath;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (blockReason) {
|
|
80
|
+
const isAllowed = allowed.some((re) => re.test(lowerPath));
|
|
81
|
+
if (!isAllowed && blocked.some((re) => re.test(lowerPath))) {
|
|
90
82
|
const out = {
|
|
91
83
|
hookSpecificOutput: {
|
|
92
84
|
hookEventName: 'PreToolUse',
|
|
93
85
|
permissionDecision: 'deny',
|
|
94
86
|
permissionDecisionReason:
|
|
95
|
-
`Bizar PreToolUse: refusing to write to protected path '${
|
|
87
|
+
`Bizar PreToolUse: refusing to write to protected path '${filePath}' (Bizar harness policy).`,
|
|
96
88
|
},
|
|
97
89
|
};
|
|
98
90
|
process.stdout.write(JSON.stringify(out) + '\n');
|
|
99
91
|
return;
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
|
|
103
|
-
// Debug-artifact warnings (console.log / debugger / .only()) live in
|
|
104
|
-
// `make clean-check` — duplicating them here would add noise without
|
|
105
|
-
// adding safety (they're caught at commit time, not write time).
|
|
106
|
-
const notes = [
|
|
107
|
-
`Bizar PreToolUse: tool=${toolName || 'unknown'} path=${filePath || '(no path)'}`,
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
const out = {
|
|
111
|
-
hookSpecificOutput: {
|
|
112
|
-
hookEventName: 'PreToolUse',
|
|
113
|
-
additionalContext: notes.join(' '),
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
process.stdout.write(JSON.stringify(out) + '\n');
|
|
94
|
+
process.stdout.write('{}\n');
|
|
117
95
|
});
|
|
@@ -17,7 +17,8 @@ import { join } from 'node:path';
|
|
|
17
17
|
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { findGitCommand } from './git-command-parser.mjs';
|
|
19
19
|
|
|
20
|
-
const FRESHNESS_WINDOW_MS =
|
|
20
|
+
const FRESHNESS_WINDOW_MS = 4 * 60 * 60 * 1000;
|
|
21
|
+
const TRIVIAL_PATH = /^(?:CHANGELOG\.md|package(-lock)?\.json|.*\/package(-lock)?\.json|\.claude-plugin\/plugin\.json|packages\/[^/]+\/src\/version\.ts)$/;
|
|
21
22
|
|
|
22
23
|
function marker(cwd) {
|
|
23
24
|
const result = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-dir'], {
|
|
@@ -50,6 +51,18 @@ function readMarker(path) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function isTrivialDiff(cwd) {
|
|
55
|
+
const result = spawnSync('git', ['diff', '--cached', '--name-only'], {
|
|
56
|
+
cwd,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
timeout: 5_000,
|
|
59
|
+
});
|
|
60
|
+
if (result.status !== 0) return false;
|
|
61
|
+
const paths = result.stdout.split('\n').map((p) => p.trim()).filter(Boolean);
|
|
62
|
+
if (paths.length === 0) return false;
|
|
63
|
+
return paths.every((p) => TRIVIAL_PATH.test(p));
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
let raw = '';
|
|
54
67
|
process.stdin.setEncoding('utf8');
|
|
55
68
|
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
@@ -70,6 +83,9 @@ process.stdin.on('end', () => {
|
|
|
70
83
|
const commandValue = input.tool_input?.command;
|
|
71
84
|
const command = Array.isArray(commandValue) ? commandValue.join(' ') : String(commandValue || '');
|
|
72
85
|
if (!findGitCommand(command, 'commit')) return;
|
|
86
|
+
|
|
87
|
+
if (isTrivialDiff(cwd)) return;
|
|
88
|
+
|
|
73
89
|
const approval = readMarker(mark);
|
|
74
90
|
const age = approval ? Date.now() - approval.timestamp : Infinity;
|
|
75
91
|
const fingerprint = stagedFingerprint(cwd);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "bizar-harness",
|
|
4
4
|
"displayName": "Bizar Harness",
|
|
5
|
-
"version": "10.12.
|
|
5
|
+
"version": "10.12.2",
|
|
6
6
|
"description": "Guarded multi-agent workflows for Claude Code with a single orchestrator, durable workflow state, and explicit human approval boundaries.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Polderlabs"
|
package/cli/commands/hook.mjs
CHANGED
|
@@ -16,8 +16,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
16
16
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
17
|
const HOOK_ROOT = resolve(PACKAGE_ROOT, '.claude', 'hooks');
|
|
18
18
|
const PRETOOL_SAFETY_LEAVES = new Set([
|
|
19
|
-
'pretooluse-editwrite', 'path-ownership-guard',
|
|
20
|
-
'pretooluse-bash', 'git-workflow-guard',
|
|
19
|
+
'pretooluse-editwrite', 'path-ownership-guard',
|
|
20
|
+
'pretooluse-bash', 'git-workflow-guard',
|
|
21
|
+
'agent-model-guard',
|
|
21
22
|
]);
|
|
22
23
|
|
|
23
24
|
export const HOOK_PROGRAMS = Object.freeze({
|
|
@@ -65,16 +66,13 @@ export const EVENT_CHAINS = Object.freeze({
|
|
|
65
66
|
'pre-tool-use': Object.freeze([
|
|
66
67
|
'pretooluse-editwrite',
|
|
67
68
|
'path-ownership-guard',
|
|
68
|
-
'content-style-guard',
|
|
69
69
|
'pretooluse-bash',
|
|
70
70
|
'git-workflow-guard',
|
|
71
|
-
'simplify-guard',
|
|
72
71
|
]),
|
|
73
72
|
'permission-request': Object.freeze(['permission-request-policy']),
|
|
74
73
|
'post-tool-use': Object.freeze([
|
|
75
74
|
'posttooluse-editwrite',
|
|
76
75
|
'auto-instinct',
|
|
77
|
-
'simplify-guard',
|
|
78
76
|
]),
|
|
79
77
|
'post-tool-use-failure': Object.freeze(['post-tool-use-failure-policy']),
|
|
80
78
|
'subagent-start': Object.freeze([
|
|
@@ -224,10 +222,10 @@ export function selectEventChain(eventKey, input = '') {
|
|
|
224
222
|
|
|
225
223
|
if (eventKey === 'pre-tool-use') {
|
|
226
224
|
if (/^(Write|Edit|MultiEdit)$/.test(toolName)) {
|
|
227
|
-
return ['pretooluse-editwrite', 'path-ownership-guard'
|
|
225
|
+
return ['pretooluse-editwrite', 'path-ownership-guard'];
|
|
228
226
|
}
|
|
229
227
|
if (toolName === 'Bash') {
|
|
230
|
-
return ['pretooluse-bash', 'git-workflow-guard'
|
|
228
|
+
return ['pretooluse-bash', 'git-workflow-guard'];
|
|
231
229
|
}
|
|
232
230
|
if (toolName === 'Agent') return ['agent-model-guard'];
|
|
233
231
|
return [];
|
|
@@ -236,7 +234,6 @@ export function selectEventChain(eventKey, input = '') {
|
|
|
236
234
|
if (eventKey === 'post-tool-use') {
|
|
237
235
|
if (/^(Write|Edit|MultiEdit)$/.test(toolName)) return ['posttooluse-editwrite'];
|
|
238
236
|
if (toolName === 'Bash') return ['auto-instinct'];
|
|
239
|
-
if (toolName === 'Skill') return ['simplify-guard'];
|
|
240
237
|
return [];
|
|
241
238
|
}
|
|
242
239
|
|
package/package.json
CHANGED