@polderlabs/bizar 10.7.0 → 10.7.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/agents/_shared/AGENT_BASELINE.md +266 -0
- package/.claude/agents/_shared/CLAUDE_TOOLS.md +412 -0
- package/.claude/agents/_shared/SKILLS.md +109 -0
- package/.claude/agents/brand-designer.md +55 -0
- package/.claude/agents/exec-assistant.md +34 -0
- package/.claude/agents/help-desk.md +44 -0
- package/.claude/agents/it-lead.md +53 -0
- package/.claude/agents/knowledge-manager.md +49 -0
- package/.claude/agents/office-coordinator.md +39 -0
- package/.claude/agents/office-greeter.md +53 -0
- package/.claude/agents/office-manager.md +287 -0
- package/.claude/agents/principal-engineer.md +58 -0
- package/.claude/agents/qa-reviewer.md +51 -0
- package/.claude/agents/research-analyst.md +53 -0
- package/.claude/agents/senior-engineer.md +55 -0
- package/.claude/agents/support-tech.md +87 -0
- package/.claude/agents/vp-engineering.md +54 -0
- package/.claude/commands/audit.md +48 -0
- package/.claude/commands/bizar.md +22 -0
- package/.claude/commands/cron.md +36 -0
- package/.claude/commands/explain.md +17 -0
- package/.claude/commands/goal.md +99 -0
- package/.claude/commands/init.md +15 -0
- package/.claude/commands/learn.md +46 -0
- package/.claude/commands/plan.md +35 -0
- package/.claude/commands/plow-through.md +50 -0
- package/.claude/commands/pr-review.md +49 -0
- package/.claude/commands/setup-provider.md +96 -0
- package/.claude/commands/spec.md +47 -0
- package/.claude/commands/sprint.md +43 -0
- package/.claude/commands/tailscale-serve.md +100 -0
- package/.claude/commands/team.md +132 -0
- package/.claude/commands/test.md +62 -0
- package/.claude/commands/validate.md +68 -0
- package/.claude/commands/visual-plan.md +24 -0
- package/.claude/hooks/README.md +108 -0
- package/.claude/hooks/__tests__/pretooluse-editwrite.test.mjs +146 -0
- package/.claude/hooks/__tests__/sessionend-recall.test.mjs +256 -0
- package/.claude/hooks/__tests__/sessionstart-prime.test.mjs +325 -0
- package/.claude/hooks/__tests__/thinking-route.test.mjs +319 -0
- package/.claude/hooks/auto-instinct.sh +81 -0
- package/.claude/hooks/learning-extract.mjs +92 -0
- package/.claude/hooks/post-merge-audit.sh +93 -0
- package/.claude/hooks/posttooluse-editwrite.mjs +91 -0
- package/.claude/hooks/pretooluse-bash.mjs +87 -0
- package/.claude/hooks/pretooluse-editwrite.mjs +117 -0
- package/.claude/hooks/sessionend-recall.mjs +384 -0
- package/.claude/hooks/sessionstart-prime.mjs +278 -0
- package/.claude/hooks/thinking-route.mjs +314 -0
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +167 -0
- package/.claude/skills/9router/SKILL.md +80 -0
- package/.claude/skills/9router-chat/SKILL.md +73 -0
- package/.claude/skills/9router-embeddings/SKILL.md +69 -0
- package/.claude/skills/9router-image/SKILL.md +86 -0
- package/.claude/skills/9router-stt/SKILL.md +79 -0
- package/.claude/skills/9router-tts/SKILL.md +80 -0
- package/.claude/skills/9router-web-fetch/SKILL.md +99 -0
- package/.claude/skills/9router-web-search/SKILL.md +91 -0
- package/.claude/skills/agent-browser/SKILL.md +64 -0
- package/.claude/skills/bizar/README.md +9 -0
- package/.claude/skills/bizar/SKILL.md +447 -0
- package/.claude/skills/cpp-coding-standards/README.md +28 -0
- package/.claude/skills/cpp-coding-standards/SKILL.md +634 -0
- package/.claude/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/.claude/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/.claude/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/.claude/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/.claude/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/.claude/skills/cpp-testing/README.md +28 -0
- package/.claude/skills/cpp-testing/SKILL.md +304 -0
- package/.claude/skills/cpp-testing/references/coverage.md +370 -0
- package/.claude/skills/cpp-testing/references/framework-compare.md +175 -0
- package/.claude/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/.claude/skills/cpp-testing/references/mocking.md +364 -0
- package/.claude/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/.claude/skills/cubesandbox/SKILL.md +148 -0
- package/.claude/skills/de-sloppify/SKILL.md +38 -0
- package/.claude/skills/de-sloppify/cleanup.mjs +253 -0
- package/.claude/skills/de-sloppify/cleanup.test.mjs +189 -0
- package/.claude/skills/embedded-esp-idf/README.md +41 -0
- package/.claude/skills/embedded-esp-idf/SKILL.md +439 -0
- package/.claude/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/.claude/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/.claude/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/.claude/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/.claude/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/.claude/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/.claude/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/.claude/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/.claude/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/.claude/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/.claude/skills/glyph/SKILL.md +163 -0
- package/.claude/skills/harness-engineering/SKILL.md +142 -0
- package/.claude/skills/lightrag/SKILL.md +81 -0
- package/.claude/skills/memory-protocol/SKILL.md +105 -0
- package/.claude/skills/obsidian/SKILL.md +306 -0
- package/.claude/skills/read-the-damn-docs/SKILL.md +113 -0
- package/.claude/skills/self-improvement/SKILL.md +64 -0
- package/.claude/skills/skillopt/SKILL.md +129 -0
- package/.claude/skills/thinking-archetypes/SKILL.md +90 -0
- package/.claude/skills/thinking-bayesian/SKILL.md +267 -0
- package/.claude/skills/thinking-bounded-rationality/SKILL.md +406 -0
- package/.claude/skills/thinking-circle-of-competence/SKILL.md +216 -0
- package/.claude/skills/thinking-cynefin/SKILL.md +70 -0
- package/.claude/skills/thinking-debiasing/SKILL.md +192 -0
- package/.claude/skills/thinking-dual-process/SKILL.md +282 -0
- package/.claude/skills/thinking-effectuation/SKILL.md +366 -0
- package/.claude/skills/thinking-feedback-loops/SKILL.md +464 -0
- package/.claude/skills/thinking-fermi-estimation/SKILL.md +263 -0
- package/.claude/skills/thinking-first-principles/SKILL.md +167 -0
- package/.claude/skills/thinking-five-whys-plus/SKILL.md +139 -0
- package/.claude/skills/thinking-inversion/SKILL.md +195 -0
- package/.claude/skills/thinking-jobs-to-be-done/SKILL.md +363 -0
- package/.claude/skills/thinking-kepner-tregoe/SKILL.md +154 -0
- package/.claude/skills/thinking-leverage-points/SKILL.md +390 -0
- package/.claude/skills/thinking-lindy-effect/SKILL.md +331 -0
- package/.claude/skills/thinking-map-territory/SKILL.md +111 -0
- package/.claude/skills/thinking-margin-of-safety/SKILL.md +330 -0
- package/.claude/skills/thinking-model-combination/SKILL.md +406 -0
- package/.claude/skills/thinking-model-router/SKILL.md +360 -0
- package/.claude/skills/thinking-model-selection/SKILL.md +341 -0
- package/.claude/skills/thinking-occams-razor/SKILL.md +129 -0
- package/.claude/skills/thinking-ooda/SKILL.md +127 -0
- package/.claude/skills/thinking-opportunity-cost/SKILL.md +360 -0
- package/.claude/skills/thinking-pre-mortem/SKILL.md +170 -0
- package/.claude/skills/thinking-probabilistic/SKILL.md +324 -0
- package/.claude/skills/thinking-red-team/SKILL.md +142 -0
- package/.claude/skills/thinking-regret-minimization/SKILL.md +335 -0
- package/.claude/skills/thinking-reversibility/SKILL.md +326 -0
- package/.claude/skills/thinking-scientific-method/SKILL.md +162 -0
- package/.claude/skills/thinking-second-order/SKILL.md +184 -0
- package/.claude/skills/thinking-socratic/SKILL.md +198 -0
- package/.claude/skills/thinking-steel-manning/SKILL.md +332 -0
- package/.claude/skills/thinking-systems/SKILL.md +238 -0
- package/.claude/skills/thinking-theory-of-constraints/SKILL.md +338 -0
- package/.claude/skills/thinking-thought-experiment/SKILL.md +354 -0
- package/.claude/skills/thinking-triz/SKILL.md +171 -0
- package/.claude/skills/thinking-via-negativa/SKILL.md +358 -0
- package/bizar-dash/skills/publishing/SKILL.md +2 -1
- package/cli/install/postinstall.mjs +54 -28
- package/cli/install/postinstall.test.mjs +98 -0
- package/cli/provision.mjs +29 -0
- package/install.sh +7 -0
- package/package.json +7 -2
- package/scripts/git-hooks/__tests__/commit-msg.test.mjs +61 -0
- package/scripts/git-hooks/commit-msg +38 -0
- package/scripts/install-hooks.sh +9 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pretooluse-bash.mjs — Claude Code PreToolUse hook (matcher: Bash).
|
|
4
|
+
*
|
|
5
|
+
* Runs the Bizar dangerous-pattern scanner on Bash commands before they
|
|
6
|
+
* execute. Blocks destructive patterns (rm -rf, sudo, SSRF, etc.) via
|
|
7
|
+
* the 36-pattern DANGEROUS_PATTERNS regex list maintained in
|
|
8
|
+
* `@polderlabs/bizar-sdk/dist/dangerous-patterns.js`.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code PreToolUse input schema:
|
|
11
|
+
* { session_id, cwd, hook_event_name: "PreToolUse", tool_name: "Bash", tool_input: { command, description } }
|
|
12
|
+
*
|
|
13
|
+
* Claude Code PreToolUse output schema:
|
|
14
|
+
* { hookSpecificOutput: { hookEventName, permissionDecision, permissionDecisionReason }, additionalContext? }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
let raw = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
let input = {};
|
|
24
|
+
try { input = JSON.parse(raw); } catch { input = {}; }
|
|
25
|
+
const toolInput = (input.tool_input && typeof input.tool_input === 'object') ? input.tool_input : {};
|
|
26
|
+
const command = String(toolInput.command || '');
|
|
27
|
+
|
|
28
|
+
// Inline dangerous-pattern scanner. Mirrors
|
|
29
|
+
// `packages/sdk/src/dangerous-patterns.ts` — kept inline so this hook
|
|
30
|
+
// works without importing the SDK at runtime.
|
|
31
|
+
// For the `rm` family, the path argument must be EXACTLY the dangerous
|
|
32
|
+
// root — not a prefix of a deeper path. `/home/drb0rk/projects/foo.mjs`
|
|
33
|
+
// is a legitimate delete, not "wipe home directory". The old patterns
|
|
34
|
+
// matched any path STARTING with `/home` and so blocked every file
|
|
35
|
+
// delete under the project root (false positive).
|
|
36
|
+
const DANGEROUS = [
|
|
37
|
+
{ name: 'rm-rf-root', pattern: /\brm\s+(-\w*r\w*f\w*\s+)*\/(?:\s|$|;|\|)/i, decision: 'deny', reason: 'Recursive delete of root filesystem' },
|
|
38
|
+
{ name: 'rm-rf-system', pattern: /\brm\s+(-\w*r\w*f\w*\s+)*\/(?:etc|var|usr|boot)\b(?:\s|$|;|\|)/i, decision: 'deny', reason: 'Recursive delete of system directory' },
|
|
39
|
+
{ name: 'rm-rf-wildcard', pattern: /\brm\s+-\w*r\w*f\w*\s+\*/i, decision: 'ask', reason: 'Recursive delete with wildcard' },
|
|
40
|
+
{ name: 'rm-rf-home', pattern: /\brm\s+(-\w*r\w*f\w*\s+)*~?\/(?:\s|$|;|\|)/i, decision: 'ask', reason: 'Recursive delete of home directory' },
|
|
41
|
+
{ name: 'rm-rf-home-exact', pattern: /\brm\s+(-\w*r\w*f\w*\s+)*\/home(?:\s|$|;|\|)/i, decision: 'ask', reason: 'Recursive delete of /home (the directory, not a sub-path)' },
|
|
42
|
+
{ name: 'mkfs', pattern: /\bmkfs(\.\w+)?\s+\/dev\//i, decision: 'deny', reason: 'Format filesystem' },
|
|
43
|
+
{ name: 'dd-of-dev', pattern: /\bdd\s+.*of=\/dev\//i, decision: 'deny', reason: 'dd to raw device' },
|
|
44
|
+
{ name: 'sudo', pattern: /(^|\s|;|&&|\|\|)sudo\b/i, decision: 'ask', reason: 'Sudo escalation' },
|
|
45
|
+
{ name: 'su-root', pattern: /\bsu\s+-?\s*root\b/i, decision: 'ask', reason: 'su to root' },
|
|
46
|
+
{ name: 'curl-metadata', pattern: /169\.254\.169\.254/i, decision: 'deny', reason: 'AWS metadata IP' },
|
|
47
|
+
{ name: 'curl-google-metadata', pattern: /metadata\.google\.internal/i, decision: 'deny', reason: 'GCP metadata hostname' },
|
|
48
|
+
{ name: 'curl-azure-metadata', pattern: /metadata\.azure\.com/i, decision: 'deny', reason: 'Azure metadata hostname' },
|
|
49
|
+
{ name: 'nc-backdoor', pattern: /\bnc\s+(-[a-z]*\s+)*-[a-z]*e\b/i, decision: 'deny', reason: 'nc execute backdoor' },
|
|
50
|
+
{ name: 'wget-pipe-shell', pattern: /\bwget\s+.*\|\s*(ba)?sh\b/i, decision: 'deny', reason: 'wget piped to shell' },
|
|
51
|
+
{ name: 'curl-pipe-shell', pattern: /\bcurl\s+.*\|\s*(ba)?sh\b/i, decision: 'deny', reason: 'curl piped to shell' },
|
|
52
|
+
{ name: 'kill-init', pattern: /\bkill\s+(-9\s+)?1\b/i, decision: 'deny', reason: 'Kill PID 1 (init)' },
|
|
53
|
+
{ name: 'shutdown', pattern: /\b(shutdown|halt|poweroff|reboot)\b/i, decision: 'deny', reason: 'System shutdown' },
|
|
54
|
+
{ name: 'xmrig', pattern: /\b(xmrig|minerd|cpuminer|cgminer)\b/i, decision: 'deny', reason: 'Crypto miner' },
|
|
55
|
+
{ name: 'read-shadow', pattern: /\/etc\/(shadow|passwd|sudoers)\b/i, decision: 'ask', reason: 'Read sensitive system file' },
|
|
56
|
+
{ name: 'read-ssh', pattern: /\.ssh\//i, decision: 'deny', reason: 'Read SSH keys' },
|
|
57
|
+
{ name: 'read-aws-creds', pattern: /~\/\.aws\/credentials/i, decision: 'deny', reason: 'Read AWS credentials' },
|
|
58
|
+
{ name: 'path-traversal', pattern: /(\.\.\/){2,}/i, decision: 'ask', reason: 'Multiple path traversal' },
|
|
59
|
+
{ name: 'git-force-push-main', pattern: /\bgit\s+push\s+(-f|--force)(\s+--.*)?\s+origin\s+(main|master)\b/i, decision: 'deny', reason: 'Force push to main/master' },
|
|
60
|
+
{ name: 'git-reset-hard', pattern: /\bgit\s+reset\s+--hard\b/i, decision: 'ask', reason: 'git reset --hard' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const p of DANGEROUS) {
|
|
64
|
+
if (p.pattern.test(command)) {
|
|
65
|
+
const decision = p.decision === 'deny' ? 'deny' : 'ask';
|
|
66
|
+
const out = {
|
|
67
|
+
hookSpecificOutput: {
|
|
68
|
+
hookEventName: 'PreToolUse',
|
|
69
|
+
permissionDecision: decision,
|
|
70
|
+
permissionDecisionReason: `dangerous_pattern:${p.name}: ${p.reason}`,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// No dangerous pattern — allow.
|
|
79
|
+
const out = {
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: 'PreToolUse',
|
|
82
|
+
permissionDecision: 'allow',
|
|
83
|
+
},
|
|
84
|
+
additionalContext: `Bizar PreToolUse(Bash): scanned ${command.length} chars, no dangerous pattern.`,
|
|
85
|
+
};
|
|
86
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
87
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse — Bizar harness hook (Claude Code format).
|
|
3
|
+
//
|
|
4
|
+
// Runs BEFORE any tool call against `Write`, `Edit`, or `MultiEdit`.
|
|
5
|
+
// (Bash is handled by `pretooluse-bash.mjs`, registered separately.)
|
|
6
|
+
//
|
|
7
|
+
// Claude Code stdin shape:
|
|
8
|
+
// {
|
|
9
|
+
// "session_id": "...",
|
|
10
|
+
// "transcript_path": "...",
|
|
11
|
+
// "cwd": "...",
|
|
12
|
+
// "hook_event_name": "PreToolUse",
|
|
13
|
+
// "tool_name": "Write",
|
|
14
|
+
// "tool_input": { "file_path": "...", "content": "..." }
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// Claude Code stdout shape:
|
|
18
|
+
// { "hookSpecificOutput": {
|
|
19
|
+
// "hookEventName": "PreToolUse",
|
|
20
|
+
// "permissionDecision": "deny",
|
|
21
|
+
// "permissionDecisionReason": "..."
|
|
22
|
+
// }}
|
|
23
|
+
// ── or ──
|
|
24
|
+
// { "hookSpecificOutput": {
|
|
25
|
+
// "hookEventName": "PreToolUse",
|
|
26
|
+
// "additionalContext": "note for the model"
|
|
27
|
+
// }}
|
|
28
|
+
//
|
|
29
|
+
// Behaviour:
|
|
30
|
+
// 1. 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
|
+
// 2. Always return a small context line for the next AI decision.
|
|
34
|
+
// (console.log / debugger / .only() are enforced by `make clean-check`
|
|
35
|
+
// at commit time — duplicate-warning here adds noise without safety.)
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
let raw = '';
|
|
40
|
+
process.stdin.setEncoding('utf8');
|
|
41
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
42
|
+
process.stdin.on('end', () => {
|
|
43
|
+
let input = {};
|
|
44
|
+
try { input = JSON.parse(raw || '{}'); } catch { input = {}; }
|
|
45
|
+
|
|
46
|
+
const toolName = String(input.tool_name || '');
|
|
47
|
+
const toolInput =
|
|
48
|
+
(input.tool_input && typeof input.tool_input === 'object') ? input.tool_input : {};
|
|
49
|
+
|
|
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
|
+
let filePath = '';
|
|
54
|
+
if (toolName === 'Write') {
|
|
55
|
+
filePath = String(toolInput.file_path || '');
|
|
56
|
+
} else if (toolName === 'Edit') {
|
|
57
|
+
filePath = String(toolInput.file_path || '');
|
|
58
|
+
} else if (toolName === 'MultiEdit') {
|
|
59
|
+
filePath = String(toolInput.file_path || '');
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write('{}\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lowerPath = filePath.toLowerCase();
|
|
66
|
+
|
|
67
|
+
// 1. Hard block — secrets + protected paths. .env.example/.sample/.template
|
|
68
|
+
// and all lockfiles are explicitly allowed (docs / package-manager output).
|
|
69
|
+
const allowed = [
|
|
70
|
+
/\/\.env\.(example|sample|template|dist)$/i,
|
|
71
|
+
/\/(package-lock|yarn|pnpm-lock|bun)\.lock\w*$/i,
|
|
72
|
+
/\.(lock|lockb)$/i,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const blocked = [
|
|
76
|
+
/\/\.envrc$/,
|
|
77
|
+
/\/\.env(\.[a-z0-9_-]+)?$/i,
|
|
78
|
+
/\/secrets?\//,
|
|
79
|
+
/\/credentials?\//,
|
|
80
|
+
/\/node_modules\//,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const isAllowed = filePath && allowed.some((re) => re.test(lowerPath));
|
|
84
|
+
let blockReason = '';
|
|
85
|
+
if (filePath && !isAllowed && blocked.some((re) => re.test(lowerPath))) {
|
|
86
|
+
blockReason = filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (blockReason) {
|
|
90
|
+
const out = {
|
|
91
|
+
hookSpecificOutput: {
|
|
92
|
+
hookEventName: 'PreToolUse',
|
|
93
|
+
permissionDecision: 'deny',
|
|
94
|
+
permissionDecisionReason:
|
|
95
|
+
`Bizar PreToolUse: refusing to write to protected path '${blockReason}' (Bizar harness policy).`,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Always add a small context line for the AI's next decision.
|
|
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');
|
|
117
|
+
});
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sessionend-recall.mjs — Claude Code SessionEnd hook.
|
|
4
|
+
*
|
|
5
|
+
* Captures what actually happened in the session so the next session
|
|
6
|
+
* (and the user, in `.bizar/sessions/`) can pick up where we left off.
|
|
7
|
+
*
|
|
8
|
+
* Reads `transcript_path` (JSONL), extracts:
|
|
9
|
+
* - last user prompt (what was being worked on)
|
|
10
|
+
* - files written/edited (from tool_use blocks)
|
|
11
|
+
* - bash commands run
|
|
12
|
+
* - error patterns (ENOENT / EACCES / permission / TypeError / unhandledrejection)
|
|
13
|
+
* - tool-call counts
|
|
14
|
+
* - wall-clock span
|
|
15
|
+
*
|
|
16
|
+
* Writes:
|
|
17
|
+
* 1. .bizar/sessions/<date>-<id>.md — markdown note with structured frontmatter
|
|
18
|
+
* 2. .bizar/session-state.json — ≤ 1KB handoff for the next SessionStart
|
|
19
|
+
*
|
|
20
|
+
* Claude Code SessionEnd input:
|
|
21
|
+
* { session_id, transcript_path, cwd, hook_event_name, reason }
|
|
22
|
+
*
|
|
23
|
+
* Claude Code SessionEnd output:
|
|
24
|
+
* {} (silence is fine — the artifact is the file system)
|
|
25
|
+
*
|
|
26
|
+
* Always exits 0.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
|
|
35
|
+
const SESSIONS_DIR = '.bizar/sessions';
|
|
36
|
+
const SESSION_STATE = '.bizar/session-state.json';
|
|
37
|
+
const HOOK_LOG_DIR = '.config/bizar/hook-logs';
|
|
38
|
+
const MAX_TRANSCRIPT_LINES = 200; // ~50KB cap
|
|
39
|
+
const MAX_FILES_TRACKED = 20;
|
|
40
|
+
const MAX_BLOCKERS = 5;
|
|
41
|
+
const ERROR_PATTERNS = [
|
|
42
|
+
/Error: ENOENT/i,
|
|
43
|
+
/Error: EACCES/i,
|
|
44
|
+
/Error: EPERM/i,
|
|
45
|
+
/permission denied/i,
|
|
46
|
+
/command not found/i,
|
|
47
|
+
/TypeError:/i,
|
|
48
|
+
/ReferenceError:/i,
|
|
49
|
+
/SyntaxError:/i,
|
|
50
|
+
/unhandledrejection/i,
|
|
51
|
+
/Cannot find module/i,
|
|
52
|
+
/MODULE_NOT_FOUND/i,
|
|
53
|
+
/fatal: /i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── Transcript parsing ─────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function readTranscript(transcriptPath) {
|
|
59
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return [];
|
|
60
|
+
let text = '';
|
|
61
|
+
try {
|
|
62
|
+
text = readFileSync(transcriptPath, 'utf8');
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const all = text.split('\n').filter(Boolean);
|
|
67
|
+
const tail = all.slice(-MAX_TRANSCRIPT_LINES);
|
|
68
|
+
|
|
69
|
+
const events = [];
|
|
70
|
+
for (const line of tail) {
|
|
71
|
+
try {
|
|
72
|
+
const obj = JSON.parse(line);
|
|
73
|
+
if (obj && typeof obj === 'object') events.push(obj);
|
|
74
|
+
} catch {
|
|
75
|
+
/* skip malformed lines */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return events;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const FILLER_PROMPTS = /^\s*(continue|go on|keep going|yes|ok|okay|sure|do it|proceed|yeah|yep)\s*[.!]?\s*$/i;
|
|
82
|
+
|
|
83
|
+
function isFiller(s) {
|
|
84
|
+
return typeof s === 'string' && FILLER_PROMPTS.test(s);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function scanForErrors(text) {
|
|
88
|
+
if (!text || typeof text !== 'string') return [];
|
|
89
|
+
const hits = [];
|
|
90
|
+
for (const pat of ERROR_PATTERNS) {
|
|
91
|
+
const m = text.match(pat);
|
|
92
|
+
if (m) hits.push(m[0]);
|
|
93
|
+
if (hits.length >= MAX_BLOCKERS) break;
|
|
94
|
+
}
|
|
95
|
+
return hits;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extract(events) {
|
|
99
|
+
const filesTouched = new Set();
|
|
100
|
+
const bashCommands = [];
|
|
101
|
+
const errors = [];
|
|
102
|
+
const toolsUsed = {};
|
|
103
|
+
const userPrompts = [];
|
|
104
|
+
let firstTs = null;
|
|
105
|
+
let lastTs = null;
|
|
106
|
+
|
|
107
|
+
function bumpTool(name) {
|
|
108
|
+
if (!name) return;
|
|
109
|
+
toolsUsed[name] = (toolsUsed[name] || 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function recordError(text) {
|
|
113
|
+
if (!text) return;
|
|
114
|
+
for (const e of scanForErrors(text)) {
|
|
115
|
+
if (!errors.includes(e)) errors.push(e);
|
|
116
|
+
if (errors.length >= MAX_BLOCKERS) break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const ev of events) {
|
|
121
|
+
const ts = ev.timestamp || ev.ts;
|
|
122
|
+
if (typeof ts === 'string' || typeof ts === 'number') {
|
|
123
|
+
if (!firstTs) firstTs = ts;
|
|
124
|
+
lastTs = ts;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const msg = ev.message || ev;
|
|
128
|
+
const role = msg && msg.role;
|
|
129
|
+
const content = msg && msg.content;
|
|
130
|
+
|
|
131
|
+
if (role === 'user' && typeof content === 'string') {
|
|
132
|
+
userPrompts.push(content);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(content)) {
|
|
136
|
+
for (const block of content) {
|
|
137
|
+
if (!block || typeof block !== 'object') continue;
|
|
138
|
+
const type = block.type;
|
|
139
|
+
if (type === 'tool_use' || type === 'toolCall') {
|
|
140
|
+
const name = block.name || block.tool_name || 'unknown';
|
|
141
|
+
bumpTool(name);
|
|
142
|
+
const input = block.input || block.tool_input || {};
|
|
143
|
+
if (input && typeof input === 'object') {
|
|
144
|
+
const fp = input.file_path || input.path;
|
|
145
|
+
if (fp && typeof fp === 'string') filesTouched.add(fp);
|
|
146
|
+
const cmd = input.command;
|
|
147
|
+
if (cmd && typeof cmd === 'string') {
|
|
148
|
+
bashCommands.push(cmd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (type === 'tool_result' || type === 'toolResult') {
|
|
153
|
+
const out = block.content || block.output;
|
|
154
|
+
if (typeof out === 'string') recordError(out);
|
|
155
|
+
else if (Array.isArray(out)) {
|
|
156
|
+
for (const sub of out) {
|
|
157
|
+
if (typeof sub === 'string') recordError(sub);
|
|
158
|
+
else if (sub && typeof sub === 'object' && typeof sub.text === 'string') {
|
|
159
|
+
recordError(sub.text);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (type === 'text' && typeof block.text === 'string') {
|
|
165
|
+
recordError(block.text);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pick the most substantive user prompt: longest non-filler, else last.
|
|
172
|
+
let best = '';
|
|
173
|
+
for (const p of userPrompts) {
|
|
174
|
+
if (isFiller(p)) continue;
|
|
175
|
+
if (p.length > best.length) best = p;
|
|
176
|
+
}
|
|
177
|
+
if (!best && userPrompts.length > 0) best = userPrompts[userPrompts.length - 1];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
lastUserPrompt: clip(best, 240),
|
|
181
|
+
filesTouched: [...filesTouched].slice(0, MAX_FILES_TRACKED),
|
|
182
|
+
bashCommands: bashCommands.slice(-10),
|
|
183
|
+
errors: errors.slice(0, MAX_BLOCKERS),
|
|
184
|
+
toolsUsed,
|
|
185
|
+
firstTs,
|
|
186
|
+
lastTs,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function clip(s, n) {
|
|
191
|
+
if (!s) return '';
|
|
192
|
+
const oneLine = String(s).replace(/\s+/g, ' ').trim();
|
|
193
|
+
return oneLine.length <= n ? oneLine : oneLine.slice(0, n - 1) + '…';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function detectActiveFeature(cwd) {
|
|
197
|
+
const path = join(cwd, 'feature_list.json');
|
|
198
|
+
if (!existsSync(path)) return null;
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
201
|
+
const active = (data.features || []).filter((f) => f && f.state === 'active');
|
|
202
|
+
if (active.length === 1) return active[0].id;
|
|
203
|
+
if (active.length > 1) return active.map((a) => a.id).join(',');
|
|
204
|
+
return null;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function inferNextStep(summary, activeFeature) {
|
|
211
|
+
if (summary.errors.length > 0) return `Resolve ${summary.errors[0]} from the prior session.`;
|
|
212
|
+
if (!activeFeature && !summary.lastUserPrompt) {
|
|
213
|
+
return 'Pick next feature from feature_list.json not_started.';
|
|
214
|
+
}
|
|
215
|
+
if (summary.lastUserPrompt) return `Resume: ${clip(summary.lastUserPrompt, 100)}`;
|
|
216
|
+
return `Continue with ${activeFeature}.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── File writers ───────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function writeSessionNote(cwd, sessionId, reason, summary) {
|
|
222
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
223
|
+
const projectName = cwd.split('/').pop() || 'unknown';
|
|
224
|
+
const dir = join(cwd, SESSIONS_DIR);
|
|
225
|
+
try {
|
|
226
|
+
mkdirSync(dir, { recursive: true });
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const fileName = `${today}-${sessionId.slice(0, 8)}.md`;
|
|
231
|
+
const notePath = join(dir, fileName);
|
|
232
|
+
|
|
233
|
+
const activeFeature = detectActiveFeature(cwd);
|
|
234
|
+
const frontmatter = [
|
|
235
|
+
'---',
|
|
236
|
+
`title: Session ${today} ${sessionId.slice(0, 8)}`,
|
|
237
|
+
`createdAt: ${new Date().toISOString()}`,
|
|
238
|
+
`sessionId: ${sessionId}`,
|
|
239
|
+
`reason: ${reason}`,
|
|
240
|
+
`cwd: ${cwd}`,
|
|
241
|
+
activeFeature ? `activeFeature: ${activeFeature}` : 'activeFeature: null',
|
|
242
|
+
`toolsUsed: ${JSON.stringify(summary.toolsUsed)}`,
|
|
243
|
+
summary.filesTouched.length > 0
|
|
244
|
+
? `filesTouched: [${summary.filesTouched.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(', ')}]`
|
|
245
|
+
: 'filesTouched: []',
|
|
246
|
+
summary.errors.length > 0
|
|
247
|
+
? `blockers: [${summary.errors.map((e) => `"${e.replace(/"/g, '\\"')}"`).join(', ')}]`
|
|
248
|
+
: 'blockers: []',
|
|
249
|
+
`tags: [session, auto-generated]`,
|
|
250
|
+
'---',
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
const body = [
|
|
254
|
+
'',
|
|
255
|
+
`# Session ${today} (${sessionId.slice(0, 8)})`,
|
|
256
|
+
'',
|
|
257
|
+
`Session ended with reason: \`${reason}\`.`,
|
|
258
|
+
'',
|
|
259
|
+
`Working directory: \`${cwd}\``,
|
|
260
|
+
'',
|
|
261
|
+
'## Last user prompt',
|
|
262
|
+
'',
|
|
263
|
+
summary.lastUserPrompt
|
|
264
|
+
? `> ${summary.lastUserPrompt.replace(/\n/g, ' ')}`
|
|
265
|
+
: '_No user prompts captured in transcript._',
|
|
266
|
+
'',
|
|
267
|
+
'## Files touched',
|
|
268
|
+
'',
|
|
269
|
+
summary.filesTouched.length > 0
|
|
270
|
+
? summary.filesTouched.map((f) => `- \`${f}\``).join('\n')
|
|
271
|
+
: '_No file writes captured._',
|
|
272
|
+
'',
|
|
273
|
+
'## Tools used',
|
|
274
|
+
'',
|
|
275
|
+
Object.entries(summary.toolsUsed)
|
|
276
|
+
.sort((a, b) => b[1] - a[1])
|
|
277
|
+
.map(([t, n]) => `- \`${t}\` × ${n}`)
|
|
278
|
+
.join('\n') || '_none_',
|
|
279
|
+
'',
|
|
280
|
+
'## Errors / blockers',
|
|
281
|
+
'',
|
|
282
|
+
summary.errors.length > 0
|
|
283
|
+
? summary.errors.map((e) => `- \`${e}\``).join('\n')
|
|
284
|
+
: '_None captured._',
|
|
285
|
+
'',
|
|
286
|
+
].join('\n');
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
writeFileSync(notePath, frontmatter + body + '\n', 'utf8');
|
|
290
|
+
return notePath;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function writeSessionState(cwd, sessionId, reason, summary, nextStep) {
|
|
297
|
+
const path = join(cwd, SESSION_STATE);
|
|
298
|
+
try {
|
|
299
|
+
mkdirSync(join(cwd, '.bizar'), { recursive: true });
|
|
300
|
+
} catch {
|
|
301
|
+
/* best-effort */
|
|
302
|
+
}
|
|
303
|
+
const state = {
|
|
304
|
+
lastSessionId: sessionId,
|
|
305
|
+
lastSessionEnd: new Date().toISOString(),
|
|
306
|
+
reason,
|
|
307
|
+
activeFeature: detectActiveFeature(cwd),
|
|
308
|
+
nextStep,
|
|
309
|
+
filesTouched: summary.filesTouched,
|
|
310
|
+
blockers: summary.errors,
|
|
311
|
+
toolsUsed: summary.toolsUsed,
|
|
312
|
+
};
|
|
313
|
+
try {
|
|
314
|
+
writeFileSync(path, JSON.stringify(state, null, 2), 'utf8');
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function logLifecycle(sessionId, reason, cwd) {
|
|
322
|
+
try {
|
|
323
|
+
const dir = join(cwd || process.cwd(), HOOK_LOG_DIR);
|
|
324
|
+
mkdirSync(dir, { recursive: true });
|
|
325
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
326
|
+
const logFile = join(dir, `session-end-${today}.jsonl`);
|
|
327
|
+
appendFileSync(
|
|
328
|
+
logFile,
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
ts: new Date().toISOString(),
|
|
331
|
+
sessionId,
|
|
332
|
+
reason,
|
|
333
|
+
cwd,
|
|
334
|
+
}) + '\n',
|
|
335
|
+
);
|
|
336
|
+
} catch {
|
|
337
|
+
/* best-effort */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
let errors = [];
|
|
344
|
+
|
|
345
|
+
let raw = '';
|
|
346
|
+
process.stdin.setEncoding('utf8');
|
|
347
|
+
process.stdin.on('data', (chunk) => {
|
|
348
|
+
raw += chunk;
|
|
349
|
+
});
|
|
350
|
+
process.stdin.on('end', () => {
|
|
351
|
+
let input = {};
|
|
352
|
+
try {
|
|
353
|
+
input = JSON.parse(raw || '{}');
|
|
354
|
+
} catch {
|
|
355
|
+
input = {};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const sessionId = String(input.session_id || 'unknown');
|
|
359
|
+
const reason = String(input.reason || 'unknown');
|
|
360
|
+
const cwd = String(input.cwd || process.cwd());
|
|
361
|
+
const transcript = input.transcript_path;
|
|
362
|
+
|
|
363
|
+
logLifecycle(sessionId, reason, cwd);
|
|
364
|
+
|
|
365
|
+
const events = readTranscript(transcript);
|
|
366
|
+
const summary = extract(events);
|
|
367
|
+
errors = summary.errors;
|
|
368
|
+
|
|
369
|
+
const nextStep = inferNextStep(summary, detectActiveFeature(cwd));
|
|
370
|
+
const notePath = writeSessionNote(cwd, sessionId, reason, summary);
|
|
371
|
+
const stateOk = writeSessionState(cwd, sessionId, reason, summary, nextStep);
|
|
372
|
+
|
|
373
|
+
// Silent success — the artifact is the file system.
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
JSON.stringify({
|
|
376
|
+
continue: true,
|
|
377
|
+
noteWritten: notePath || null,
|
|
378
|
+
stateWritten: stateOk,
|
|
379
|
+
toolsUsed: summary.toolsUsed,
|
|
380
|
+
filesTouched: summary.filesTouched.length,
|
|
381
|
+
blockers: summary.errors.length,
|
|
382
|
+
}) + '\n',
|
|
383
|
+
);
|
|
384
|
+
});
|