@polderlabs/bizar 6.1.0 → 6.2.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.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-push hook — block push of commits that touch per-machine state.
3
+ #
4
+ # Defense-in-depth: even if someone bypasses the pre-commit hook with
5
+ # `git commit --no-verify`, this hook catches .bizar/ or config/cline.json
6
+ # changes before they reach the remote.
7
+ #
8
+ # Bypass: git push --no-verify (NOT recommended).
9
+
10
+ set -euo pipefail
11
+
12
+ REMOTE="$1"
13
+ URL="$2"
14
+
15
+ # Read ref updates from stdin (format: <local-ref> <local-sha> <remote-ref> <remote-sha>)
16
+ # For a force-push or new branch, the "local-sha" may be the new tip.
17
+ # We scan the names of changed files in all commits being pushed.
18
+ COMMITS=""
19
+ while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
20
+ if [ -z "$LOCAL_SHA" ] || [ "$LOCAL_SHA" = "0000000000000000000000000000000000000000" ]; then
21
+ continue
22
+ fi
23
+ if [ -z "$REMOTE_SHA" ] || [ "$REMOTE_SHA" = "0000000000000000000000000000000000000000" ]; then
24
+ # New branch — scan all commits on the local side
25
+ RANGE="$LOCAL_SHA"
26
+ else
27
+ RANGE="$REMOTE_SHA..$LOCAL_SHA"
28
+ fi
29
+ if [ -z "$COMMITS" ]; then
30
+ COMMITS="$RANGE"
31
+ else
32
+ COMMITS="$COMMITS $RANGE"
33
+ fi
34
+ done
35
+
36
+ if [ -z "$COMMITS" ]; then
37
+ exit 0
38
+ fi
39
+
40
+ BAD=$(git log --name-only --no-merges --pretty=format: $COMMITS 2>/dev/null \
41
+ | grep -E "(^|/)\.bizar/|config/cline\.json$" \
42
+ | sort -u || true)
43
+
44
+ if [ -n "$BAD" ]; then
45
+ echo "" >&2
46
+ echo "❌ Refusing to push to $REMOTE — these files in the commit range are per-machine and must not be pushed:" >&2
47
+ echo "$BAD" | sed 's/^/ /' >&2
48
+ echo "" >&2
49
+ echo " .bizar/ contains per-project state (memory.json, graph/, plans, scripts)." >&2
50
+ echo " config/cline.json contains per-user cline config (model API tokens)." >&2
51
+ echo "" >&2
52
+ echo " To override (NOT recommended): git push --no-verify" >&2
53
+ exit 1
54
+ fi
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Install BizarHarness git hooks
3
+ # Run this once per clone: ./scripts/install-hooks.sh
4
+
5
+ set -e
6
+
7
+ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/git-hooks"
8
+ GIT_HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
9
+
10
+ if [ ! -d "$GIT_HOOKS_DIR" ]; then
11
+ echo "Error: not in a git repo"
12
+ exit 1
13
+ fi
14
+
15
+ cp "$HOOKS_DIR/pre-commit" "$GIT_HOOKS_DIR/pre-commit"
16
+ chmod +x "$GIT_HOOKS_DIR/pre-commit"
17
+
18
+ cp "$HOOKS_DIR/pre-push" "$GIT_HOOKS_DIR/pre-push"
19
+ chmod +x "$GIT_HOOKS_DIR/pre-push"
20
+
21
+ echo "✓ Installed pre-commit hook"
22
+ echo " The hook scans staged changes for likely secrets (Bearer tokens, API keys, etc.)"
23
+ echo " It also blocks commits touching .bizar/ or config/cline.json (per-machine state)"
24
+ echo " To bypass in an emergency: git commit --no-verify"
25
+ echo ""
26
+ echo "✓ Installed pre-push hook"
27
+ echo " Blocks pushes whose commit range includes .bizar/ or config/cline.json changes"
28
+ echo " Defense-in-depth: catches bypasses of pre-commit via --no-verify"
29
+ echo " To bypass in an emergency: git push --no-verify"
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/install-service.mjs
4
+ *
5
+ * v3.22.0 — Post-install hook that registers the Bizar background service
6
+ * via Stream A's `cli/service-controller.mjs`.
7
+ *
8
+ * This script is a thin wrapper that handles the case where Stream A has not
9
+ * yet landed (service-controller.mjs missing) gracefully.
10
+ *
11
+ * Design:
12
+ * - Imports service-controller.mjs dynamically (runtime dependency).
13
+ * - Calls `installService({ force })`.
14
+ * - If already installed with matching content, prints a message and exits 0.
15
+ * - On Linux without systemd, falls back to running the daemon in a tmux
16
+ * session and prints a note.
17
+ * - On failure (or missing service-controller.mjs from sibling stream not
18
+ * yet landed), prints a friendly error with the manual command and exits 0
19
+ * (non-error — install is deliberately split across streams).
20
+ */
21
+
22
+ import { existsSync } from 'node:fs';
23
+ import { join, resolve, dirname } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { spawnSync } from 'node:child_process';
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+ const REPO_ROOT = resolve(__dirname, '..');
30
+
31
+ function runCmd(cmd, args, opts = {}) {
32
+ const r = spawnSync(cmd, args, {
33
+ encoding: 'utf8',
34
+ timeout: 15_000,
35
+ shell: false,
36
+ windowsHide: true,
37
+ ...opts,
38
+ });
39
+ return {
40
+ status: r.status,
41
+ stdout: typeof r.stdout === 'string' ? r.stdout : '',
42
+ stderr: typeof r.stderr === 'string' ? r.stderr : '',
43
+ error: r.error || null,
44
+ };
45
+ }
46
+
47
+ function hasSystemd() {
48
+ try {
49
+ const r = runCmd('systemctl', ['--version']);
50
+ return r.status === 0;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function findTmux() {
57
+ try {
58
+ const r = runCmd('which', ['tmux']);
59
+ return r.status === 0;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Run the daemon in a tmux session as a fallback for non-systemd Linux.
67
+ */
68
+ function installViaTmux(projectRoot) {
69
+ const cliEntry = join(projectRoot, 'cli', 'bin.mjs');
70
+ if (!existsSync(cliEntry)) {
71
+ // Fallback to node path
72
+ return {
73
+ ok: false,
74
+ error: `cli entry not found at ${cliEntry}`,
75
+ note: 'install the BizarHarness repo first, then re-run this script',
76
+ };
77
+ }
78
+
79
+ if (!findTmux()) {
80
+ return {
81
+ ok: false,
82
+ error: 'tmux not found — cannot start background daemon',
83
+ note: 'install tmux or run manually: node cli/bin.mjs dash start --bg',
84
+ };
85
+ }
86
+
87
+ const sessionName = 'bizar-service';
88
+ // Kill existing session if present (idempotent)
89
+ runCmd('tmux', ['kill-session', '-t', sessionName]);
90
+ const r = runCmd('tmux', [
91
+ 'new-session', '-d', '-s', sessionName,
92
+ 'node', cliEntry, 'dash', 'start', '--bg',
93
+ ], { shell: false, timeout: 10_000 });
94
+
95
+ if (r.error || (r.status !== 0 && r.status !== null)) {
96
+ return {
97
+ ok: false,
98
+ error: `tmux new-session failed: ${r.stderr || r.error?.message || `exit ${r.status}`}`,
99
+ note: `run manually: node "${cliEntry}" dash start --bg`,
100
+ };
101
+ }
102
+
103
+ return {
104
+ ok: true,
105
+ unitPath: null,
106
+ note: `service started in tmux session "${sessionName}" (non-systemd fallback)`,
107
+ };
108
+ }
109
+
110
+ async function installService(opts = {}) {
111
+ const force = !!opts.force;
112
+ const projectRoot = resolve(REPO_ROOT);
113
+
114
+ // Path to service-controller.mjs (Stream A's file)
115
+ const controllerPath = join(projectRoot, 'cli', 'service-controller.mjs');
116
+
117
+ if (!existsSync(controllerPath)) {
118
+ // Stream A hasn't landed yet — this is NOT an error.
119
+ // Print a message and exit 0 so the installation doesn't fail.
120
+ console.log(JSON.stringify({
121
+ ok: true,
122
+ skipped: true,
123
+ note: 'service-controller.mjs not found (Stream A not yet landed). Install split across streams — service registration will be completed when cli/service-controller.mjs is available.',
124
+ }));
125
+ return;
126
+ }
127
+
128
+ // Dynamic import — we want a runtime error only if the module is present
129
+ // but fails to load.
130
+ let controller;
131
+ try {
132
+ controller = await import(controllerPath);
133
+ } catch (err) {
134
+ console.log(JSON.stringify({
135
+ ok: false,
136
+ error: `failed to load service-controller.mjs: ${err.message}`,
137
+ note: `run manually: node "${join(projectRoot, 'cli', 'service-controller.mjs')}" install`,
138
+ }));
139
+ return;
140
+ }
141
+
142
+ if (typeof controller.installService !== 'function') {
143
+ console.log(JSON.stringify({
144
+ ok: false,
145
+ error: 'service-controller.mjs does not export installService',
146
+ }));
147
+ return;
148
+ }
149
+
150
+ // Platform-specific fallback: on Linux without systemd, use tmux.
151
+ if (process.platform === 'linux' && !hasSystemd()) {
152
+ const result = installViaTmux(projectRoot);
153
+ console.log(JSON.stringify(result));
154
+ return;
155
+ }
156
+
157
+ try {
158
+ const result = controller.installService({ force, projectRoot });
159
+ console.log(JSON.stringify(result));
160
+ } catch (err) {
161
+ console.log(JSON.stringify({
162
+ ok: false,
163
+ error: `installService threw: ${err.message}`,
164
+ }));
165
+ }
166
+ }
167
+
168
+ // ── CLI entry ──────────────────────────────────────────────────────────────────
169
+
170
+ const isMain = process.argv[1] && (
171
+ process.argv[1] === import.meta.filename ||
172
+ process.argv[1].endsWith('/install-service.mjs')
173
+ );
174
+
175
+ if (isMain) {
176
+ const flags = new Set(process.argv.slice(2));
177
+ const force = flags.has('--force') || flags.has('-f');
178
+ const opts = { force };
179
+
180
+ installService(opts).catch(err => {
181
+ console.log(JSON.stringify({
182
+ ok: false,
183
+ error: err.message,
184
+ note: 'unexpected error during service installation',
185
+ }));
186
+ process.exit(0); // still exit 0 so the overall install script doesn't fail
187
+ });
188
+ }
189
+
190
+ export { installService };
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # session-trace.sh — Record structured session events to JSONL.
3
+ #
4
+ # Usage:
5
+ # ./scripts/session-trace.sh start # record session start
6
+ # ./scripts/session-trace.sh end # record session end
7
+ # ./scripts/session-trace.sh event <type> <data> # record arbitrary event
8
+ #
9
+ # Output: .harness/traces/sessions.jsonl (gitignored)
10
+ #
11
+ # Per session, records:
12
+ # - start time, branch, commit
13
+ # - features touched (read from feature_list.json state transitions)
14
+ # - end time, files changed, tests run
15
+
16
+ set -uo pipefail
17
+
18
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19
+ TRACE_DIR="$ROOT/.harness/traces"
20
+ TRACE_FILE="$TRACE_DIR/sessions.jsonl"
21
+
22
+ mkdir -p "$TRACE_DIR"
23
+
24
+ ACTION="${1:-}"
25
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
26
+ BRANCH=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-branch")
27
+ COMMIT=$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo "no-commit")
28
+
29
+ write_event() {
30
+ echo "$1" >> "$TRACE_FILE"
31
+ }
32
+
33
+ case "$ACTION" in
34
+ start)
35
+ write_event "{\"event\":\"session.start\",\"timestamp\":\"$NOW\",\"branch\":\"$BRANCH\",\"commit\":\"$COMMIT\"}"
36
+ echo "✓ Session started — recorded to $TRACE_FILE"
37
+ ;;
38
+ end)
39
+ write_event "{\"event\":\"session.end\",\"timestamp\":\"$NOW\",\"branch\":\"$BRANCH\",\"commit\":\"$COMMIT\"}"
40
+ echo "✓ Session ended — recorded to $TRACE_FILE"
41
+ ;;
42
+ event)
43
+ TYPE="${2:-unknown}"
44
+ DATA="${3:-}"
45
+ write_event "{\"event\":\"$TYPE\",\"timestamp\":\"$NOW\",\"branch\":\"$BRANCH\",\"commit\":\"$COMMIT\",\"data\":\"$DATA\"}"
46
+ echo "✓ Event '$TYPE' recorded"
47
+ ;;
48
+ *)
49
+ echo "Usage: $0 {start|end|event <type> <data>}"
50
+ exit 1
51
+ ;;
52
+ esac
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # verify-feature.sh — Verify a feature from feature_list.json by ID.
3
+ #
4
+ # Usage: ./scripts/verify-feature.sh F-001
5
+ #
6
+ # Multi-layer validation in strict order (Layer 1 → Layer 2 → Layer 3).
7
+ # On layer failure, prints the `repair` instruction from feature_list.json
8
+ # so the agent knows exactly what to fix.
9
+ #
10
+ # On success, marks the feature `passing` and recomputes VCR.
11
+
12
+ set -euo pipefail
13
+
14
+ FEATURE_ID="${1:-}"
15
+ if [[ -z "$FEATURE_ID" ]]; then
16
+ echo "Usage: $0 <feature-id>"
17
+ exit 1
18
+ fi
19
+
20
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
21
+ FL="$ROOT/feature_list.json"
22
+
23
+ if [[ ! -f "$FL" ]]; then
24
+ echo "FAIL: feature_list.json not found at $FL"
25
+ exit 1
26
+ fi
27
+
28
+ echo "▶ Verifying feature $FEATURE_ID..."
29
+ echo "──────────────────────────────────────"
30
+
31
+ # Run layers in order. Read the layers array from feature_list.json.
32
+ # Stop on first failure; print repair instructions.
33
+
34
+ # Layer 1: Compile (always runs)
35
+ echo "Layer 1: TypeScript compile"
36
+ if ! (cd "$ROOT" && bunx tsc --noEmit 2>&1 | tail -3); then
37
+ echo "FAIL: Layer 1 (compile) failed"
38
+ echo "REPAIR: Run 'make check' and fix TypeScript errors. Common issues:"
39
+ echo " - Missing type annotation"
40
+ echo " - Wrong zod shape passed to createTool"
41
+ echo " - Missing import from @cline/sdk or @cline/core"
42
+ exit 1
43
+ fi
44
+ echo " PASS"
45
+
46
+ # Layers 2-N: Read from feature_list.json
47
+ LAYERS=$(bun -e "
48
+ const f = JSON.parse(await Bun.file('$FL').text());
49
+ const x = f.features.find(y => y.id === '$FEATURE_ID');
50
+ if (x && x.layers) {
51
+ for (const l of x.layers.slice(1)) { // skip compile (already run)
52
+ console.log(JSON.stringify(l));
53
+ }
54
+ }
55
+ " 2>/dev/null || echo "")
56
+
57
+ while IFS= read -r LAYER_JSON; do
58
+ [[ -z "$LAYER_JSON" ]] && continue
59
+ LABEL=$(echo "$LAYER_JSON" | bun -e "const d = JSON.parse(await Bun.stdin.text()); console.log(d.label);" 2>/dev/null)
60
+ CMD=$(echo "$LAYER_JSON" | bun -e "const d = JSON.parse(await Bun.stdin.text()); console.log(d.cmd);" 2>/dev/null)
61
+ REPAIR=$(echo "$LAYER_JSON" | bun -e "const d = JSON.parse(await Bun.stdin.text()); console.log(d.repair);" 2>/dev/null)
62
+
63
+ echo "Layer ($LABEL): $CMD"
64
+ set +e
65
+ OUTPUT=$(cd "$ROOT" && eval "$CMD" 2>&1)
66
+ RC=$?
67
+ set -e
68
+ if [[ $RC -eq 0 ]]; then
69
+ echo " PASS"
70
+ else
71
+ echo "FAIL: Layer ($LABEL) failed (exit $RC)"
72
+ echo "REPAIR: $REPAIR"
73
+ echo "OUTPUT: $(echo "$OUTPUT" | tail -5)"
74
+ echo ""
75
+ echo "Do NOT proceed to the next layer. Fix this one first."
76
+ exit 1
77
+ fi
78
+ done <<< "$LAYERS"
79
+
80
+ # All layers passed — mark the feature `passing`
81
+ COMMIT=$(cd "$ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "no-commit")
82
+ NOW=$(date -u +%Y-%m-%d)
83
+
84
+ echo "▶ Marking $FEATURE_ID as passing..."
85
+ bun -e "
86
+ const f = JSON.parse(await Bun.file('$FL').text());
87
+ const x = f.features.find(y => y.id === '$FEATURE_ID');
88
+ if (x) {
89
+ x.state = 'passing';
90
+ x.passed = '$NOW';
91
+ x.commit = '$COMMIT';
92
+ x.evidence = 'make verify-feature $FEATURE_ID — all layers green ($NOW)';
93
+ const activated = f.features.filter(y => y.state !== 'not_started').length;
94
+ const passing = f.features.filter(y => y.state === 'passing').length;
95
+ f.vcr = { passing, activated, ratio: activated === 0 ? 1.0 : +(passing / activated).toFixed(3) };
96
+ await Bun.write('$FL', JSON.stringify(f, null, 2));
97
+ console.log('VCR:', passing + '/' + activated, '=', f.vcr.ratio);
98
+ }
99
+ "
100
+
101
+ echo "──────────────────────────────────────"
102
+ echo "✓ Feature $FEATURE_ID verified"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file