@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,279 @@
1
+ /**
2
+ * cli/commands/validate.test.mjs
3
+ *
4
+ * v6.2.0 — Unit tests for the new `bizar validate` subcommand.
5
+ *
6
+ * We exercise `runValidate()` directly. CLINE_DIR is mocked via
7
+ * `process.env.CLINE_DIR`; a minimal-but-complete Bizar install is
8
+ * staged inside a tmpdir. We then progressively delete files to
9
+ * verify each check fails with a sensible message.
10
+ */
11
+ import { test, describe, beforeEach, afterEach } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { Writable } from 'node:stream';
17
+
18
+ const { runValidate } = await import('./validate.mjs');
19
+
20
+ const ORIG_CLINE_DIR = process.env.CLINE_DIR;
21
+ const ORIG_HOME = process.env.HOME;
22
+
23
+ let workDir;
24
+
25
+ function freshWorkDir() {
26
+ const root = mkdtempSync(join(tmpdir(), 'bizar-validate-'));
27
+ process.env.CLINE_DIR = root;
28
+ return root;
29
+ }
30
+
31
+ function makeFakeClineInstall(root) {
32
+ writeFileSync(
33
+ join(root, 'cline.json'),
34
+ JSON.stringify({
35
+ $schema: 'https://docs.cline.bot/config.json',
36
+ plugin: [[
37
+ './plugins/bizar',
38
+ { loopThresholdWarn: 5, loopThresholdEscalate: 8, loopThresholdBlock: 12, loopWindowSize: 10 },
39
+ ]],
40
+ default_agent: 'odin',
41
+ permission: 'allow',
42
+ snapshot: false,
43
+ instructions: ['.cline/instructions/bizar-tools.md'],
44
+ provider: {
45
+ '9router': {
46
+ baseUrl: 'http://localhost:20128/v1',
47
+ apiKey: '${env:NINEROUTER_KEY}',
48
+ models: { 'minimax/MiniMax-M3': { reasoning: true } },
49
+ },
50
+ minimax: { models: {} },
51
+ },
52
+ }, null, 2),
53
+ );
54
+ const agentsDir = join(root, 'agents');
55
+ mkdirSync(agentsDir, { recursive: true });
56
+ for (const f of [
57
+ 'odin.md', 'vor.md', 'frigg.md', 'quick.md',
58
+ 'mimir.md', 'heimdall.md', 'hermod.md', 'thor.md', 'baldr.md',
59
+ 'tyr.md', 'vidarr.md', 'forseti.md',
60
+ 'semble-search.md', 'agent-browser.md',
61
+ ]) {
62
+ writeFileSync(join(agentsDir, f), '# fake agent');
63
+ }
64
+ writeFileSync(join(agentsDir, 'odin.yaml'), 'name: odin\ndescription: odin\n');
65
+ const cmdsDir = join(root, 'commands');
66
+ mkdirSync(cmdsDir, { recursive: true });
67
+ for (const f of [
68
+ 'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
69
+ 'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
70
+ 'visual-plan.md',
71
+ 'team.md', 'test.md', 'validate.md',
72
+ ]) {
73
+ writeFileSync(join(cmdsDir, f), '# fake command');
74
+ }
75
+ const skillsDir = join(root, 'skills');
76
+ mkdirSync(skillsDir, { recursive: true });
77
+ for (const skill of ['9router', 'bizar', 'obsidian']) {
78
+ mkdirSync(join(skillsDir, skill), { recursive: true });
79
+ writeFileSync(join(skillsDir, skill, 'SKILL.md'), '# fake skill');
80
+ }
81
+ const rulesDir = join(root, 'rules');
82
+ mkdirSync(rulesDir, { recursive: true });
83
+ for (const r of ['general.md', 'git.md', 'javascript.md', 'python.md', 'testing.md', 'thinking.md', 'uncertainty.md']) {
84
+ writeFileSync(join(rulesDir, r), '# fake rule');
85
+ }
86
+ const hooksDir = join(root, 'hooks');
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');
90
+ }
91
+ const pluginDir = join(root, 'plugins', 'bizar');
92
+ mkdirSync(pluginDir, { recursive: true });
93
+ writeFileSync(join(pluginDir, 'index.ts'), '// fake plugin entry — >100 bytes to pass size check\n'.repeat(10));
94
+ mkdirSync(join(pluginDir, 'src'), { recursive: true });
95
+ writeFileSync(
96
+ join(pluginDir, 'src', 'clineruntime.ts'),
97
+ 'export class ClineRuntime { config = { enableAgentTeams: true }; }\n',
98
+ );
99
+ // node_modules with the required runtime deps
100
+ const nmDir = join(pluginDir, 'node_modules');
101
+ mkdirSync(nmDir, { recursive: true });
102
+ mkdirSync(join(nmDir, 'zod'), { recursive: true });
103
+ writeFileSync(join(nmDir, 'zod', 'package.json'), '{}');
104
+ mkdirSync(join(nmDir, '@cline'), { recursive: true });
105
+ for (const dep of ['@cline/sdk', '@cline/core', '@cline/shared']) {
106
+ mkdirSync(join(nmDir, dep), { recursive: true });
107
+ writeFileSync(join(nmDir, dep, 'package.json'), '{}');
108
+ }
109
+ }
110
+
111
+ beforeEach(() => {
112
+ workDir = freshWorkDir();
113
+ makeFakeClineInstall(workDir);
114
+ });
115
+
116
+ afterEach(() => {
117
+ if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
118
+ if (ORIG_CLINE_DIR === undefined) delete process.env.CLINE_DIR;
119
+ else process.env.CLINE_DIR = ORIG_CLINE_DIR;
120
+ if (ORIG_HOME === undefined) delete process.env.HOME;
121
+ else process.env.HOME = ORIG_HOME;
122
+ });
123
+
124
+ /**
125
+ * Capture stdout + exit code for a single runValidate() call.
126
+ * The test runner writes its own progress to stdout, so we swap
127
+ * process.stdout for a sink Writable during the call.
128
+ */
129
+ async function captureRun(opts) {
130
+ const captured = [];
131
+ const sink = new Writable({
132
+ write(chunk, _enc, cb) {
133
+ captured.push(Buffer.from(chunk));
134
+ cb();
135
+ },
136
+ });
137
+ const origStdoutDescriptor = Object.getOwnPropertyDescriptor(process, 'stdout');
138
+ const origExit = process.exit;
139
+ let exitCode = null;
140
+ Object.defineProperty(process, 'stdout', { value: sink, configurable: true, writable: true });
141
+ process.exit = (code) => { exitCode = code; };
142
+ try {
143
+ await runValidate(opts);
144
+ } finally {
145
+ if (origStdoutDescriptor) Object.defineProperty(process, 'stdout', origStdoutDescriptor);
146
+ process.exit = origExit;
147
+ }
148
+ return { stdout: Buffer.concat(captured).toString('utf8'), exitCode };
149
+ }
150
+
151
+ describe('runValidate() — JSON output', () => {
152
+ test('returns structured JSON with passed/failed/results', async () => {
153
+ const { stdout, exitCode } = await captureRun({ json: true });
154
+ assert.equal(exitCode, 0, 'should exit 0 on success');
155
+ const parsed = JSON.parse(stdout);
156
+ assert.equal(typeof parsed.passed, 'number');
157
+ assert.equal(typeof parsed.failed, 'number');
158
+ assert.ok(Array.isArray(parsed.results));
159
+ assert.ok(parsed.results.length >= 18, 'expected at least 18 checks');
160
+ for (const r of parsed.results) {
161
+ assert.ok(typeof r.name === 'string');
162
+ assert.ok(typeof r.ok === 'boolean');
163
+ assert.ok(typeof r.message === 'string');
164
+ }
165
+ });
166
+
167
+ test('fails on missing team command', async () => {
168
+ rmSync(join(workDir, 'commands', 'team.md'));
169
+ const { exitCode } = await captureRun({ json: true });
170
+ assert.equal(exitCode, 1, 'should exit 1 on team-command-present failure');
171
+ });
172
+
173
+ test('fails on missing test command', async () => {
174
+ rmSync(join(workDir, 'commands', 'test.md'));
175
+ const { exitCode } = await captureRun({ json: true });
176
+ assert.equal(exitCode, 1);
177
+ });
178
+
179
+ test('fails on missing validate command', async () => {
180
+ rmSync(join(workDir, 'commands', 'validate.md'));
181
+ const { exitCode } = await captureRun({ json: true });
182
+ assert.equal(exitCode, 1);
183
+ });
184
+
185
+ test('fails on missing agent file', async () => {
186
+ rmSync(join(workDir, 'agents', 'mimir.md'));
187
+ const { exitCode } = await captureRun({ json: true });
188
+ assert.equal(exitCode, 1);
189
+ });
190
+
191
+ test('fails when cline.json missing', async () => {
192
+ rmSync(join(workDir, 'cline.json'));
193
+ const { exitCode } = await captureRun({ json: true });
194
+ assert.equal(exitCode, 1);
195
+ });
196
+
197
+ test('fails when enableAgentTeams is false in clineruntime.ts', async () => {
198
+ writeFileSync(
199
+ join(workDir, 'plugins', 'bizar', 'src', 'clineruntime.ts'),
200
+ 'export class ClineRuntime { config = { enableAgentTeams: false }; }\n',
201
+ );
202
+ const { exitCode } = await captureRun({ json: true });
203
+ assert.equal(exitCode, 1, 'should fail when enableAgentTeams is false');
204
+ });
205
+
206
+ test('fails when provider config is missing', async () => {
207
+ const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
208
+ delete cfg.provider;
209
+ writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
210
+ const { exitCode } = await captureRun({ json: true });
211
+ assert.equal(exitCode, 1);
212
+ });
213
+
214
+ test('passes when only 9router is configured (no minimax)', async () => {
215
+ const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
216
+ delete cfg.provider.minimax;
217
+ writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
218
+ const { exitCode } = await captureRun({ json: true });
219
+ assert.equal(exitCode, 0);
220
+ });
221
+
222
+ test('passes when only minimax is configured (no 9router)', async () => {
223
+ const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
224
+ delete cfg.provider['9router'];
225
+ writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
226
+ const { exitCode } = await captureRun({ json: true });
227
+ assert.equal(exitCode, 0);
228
+ });
229
+
230
+ test('default mode is lenient on 9router-unreachable', async () => {
231
+ const { stdout, exitCode } = await captureRun({ json: true });
232
+ const parsed = JSON.parse(stdout);
233
+ const nineR = parsed.results.find((r) => r.name === '9router-reachable');
234
+ assert.ok(nineR, '9router-reachable check should be present');
235
+ if (!nineR.ok) {
236
+ // 9router is not running. Default mode must be lenient.
237
+ assert.equal(exitCode, 0, 'default mode should be lenient when 9router is down');
238
+ } else {
239
+ // 9router is running (some test envs have it). Just confirm the
240
+ // check is in the list and exit code is sane.
241
+ assert.equal(exitCode, 0, 'expected exit 0 when all critical checks pass');
242
+ }
243
+ });
244
+
245
+ test('--strict fails when 9router is unreachable', async () => {
246
+ // We can't easily kill 9router in a test, but we can prove that
247
+ // --strict mode would fail if 9router were down: set NINEROUTER_URL
248
+ // to a port nothing is listening on.
249
+ const origUrl = process.env.NINEROUTER_URL;
250
+ process.env.NINEROUTER_URL = 'http://127.0.0.1:1'; // nothing here
251
+ try {
252
+ const { exitCode } = await captureRun({ json: true, strict: true });
253
+ assert.equal(exitCode, 1, 'strict mode should fail when 9router is unreachable');
254
+ } finally {
255
+ if (origUrl === undefined) delete process.env.NINEROUTER_URL;
256
+ else process.env.NINEROUTER_URL = origUrl;
257
+ }
258
+ });
259
+
260
+ test('--only filters to a single check', async () => {
261
+ const { stdout, exitCode } = await captureRun({ json: true, only: 'team-command-present' });
262
+ assert.equal(exitCode, 0);
263
+ const parsed = JSON.parse(stdout);
264
+ assert.equal(parsed.results.length, 1);
265
+ assert.equal(parsed.results[0].name, 'team-command-present');
266
+ assert.equal(parsed.results[0].ok, true);
267
+ });
268
+
269
+ test('--only on a missing file fails with exit 1', async () => {
270
+ rmSync(join(workDir, 'commands', 'team.md'));
271
+ const { exitCode } = await captureRun({ json: true, only: 'team-command-present' });
272
+ assert.equal(exitCode, 1);
273
+ });
274
+
275
+ test('--only with an unknown name exits 2', async () => {
276
+ const { exitCode } = await captureRun({ json: true, only: 'this-does-not-exist' });
277
+ assert.equal(exitCode, 2);
278
+ });
279
+ });
@@ -6,8 +6,10 @@
6
6
  * Verifies the installation is functional by checking:
7
7
  * 1. Memory vault exists and is git-initialized
8
8
  * 2. `bizar doctor` exits 0
9
- * 3. `bizar bg list` returns (even if empty)
10
- * 4. lightrag-server installed
9
+ * 3. Dashboard HTTP responds 200
10
+ * 4. WebSocket connects to dashboard
11
+ * 5. `bizar bg list` returns (even if empty)
12
+ * 6. lightrag-server installed
11
13
  *
12
14
  * Each check has a 30s timeout. Exits 0 if all pass, 1 otherwise.
13
15
  *
@@ -27,6 +29,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;
27
29
  const HOME = homedir();
28
30
  const DEFAULT_BIZAR_HOME = join(HOME, '.config', 'bizar');
29
31
  const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
32
+ const DEFAULT_DASHBOARD_PORT = 4097;
30
33
 
31
34
  /**
32
35
  * @param {object} opts
@@ -37,6 +40,7 @@ const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
37
40
  export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
38
41
  const bh = bizarHome || DEFAULT_BIZAR_HOME;
39
42
  const mv = process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
43
+ const port = parseInt(process.env.BIZAR_DASHBOARD_PORT || String(DEFAULT_DASHBOARD_PORT), 10) || DEFAULT_DASHBOARD_PORT;
40
44
  const checks = [];
41
45
 
42
46
  function check(name, fn) {
@@ -77,7 +81,96 @@ export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TI
77
81
  }
78
82
  });
79
83
 
80
- // ── 3. `bizar bg list` returns ────────────────────────────────────────────
84
+ // ── 3. Dashboard HTTP responds 200 (with retry) ─────────────────────────────────
85
+ check('dashboard HTTP', async () => {
86
+ const maxAttempts = 5;
87
+ const delayMs = 2000;
88
+ let lastErr = null;
89
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
90
+ try {
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
93
+ try {
94
+ const res = await fetch(`http://127.0.0.1:${port}/`, {
95
+ signal: controller.signal,
96
+ method: 'GET',
97
+ });
98
+ clearTimeout(timer);
99
+ if (res.ok) {
100
+ if (attempt > 1) {
101
+ return { ok: true, message: `HTTP ${res.status} on port ${port} (tried ${attempt}x)` };
102
+ }
103
+ return { ok: true, message: `HTTP ${res.status} on port ${port}` };
104
+ }
105
+ lastErr = `HTTP ${res.status} on port ${port} (expected 200)`;
106
+ } catch (err) {
107
+ clearTimeout(timer);
108
+ lastErr = err.name === 'AbortError'
109
+ ? `dashboard not responding on port ${port} (timeout)`
110
+ : `dashboard unreachable on port ${port}: ${err.message}`;
111
+ }
112
+ } catch (err) {
113
+ lastErr = `fetch not available: ${err.message}`;
114
+ }
115
+ if (attempt < maxAttempts) {
116
+ await new Promise((r) => setTimeout(r, delayMs));
117
+ }
118
+ }
119
+ return { ok: false, message: lastErr || 'dashboard HTTP failed' };
120
+ });
121
+
122
+ // ── 4. WebSocket connects (with retry) ────────────────────────────────────────
123
+ check('dashboard WebSocket', async () => {
124
+ const maxAttempts = 5;
125
+ const delayMs = 2000;
126
+ let lastErr = null;
127
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
128
+ try {
129
+ const { WebSocket } = await import('ws' in globalThis
130
+ ? { ws: globalThis.ws, WebSocket: globalThis.WebSocket }
131
+ : await import(`ws`).then(m => ({ ws: m, WebSocket: m.WebSocket || m.default }))
132
+ );
133
+ const url = `ws://127.0.0.1:${port}/ws`;
134
+ const controller = new AbortController();
135
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
136
+ try {
137
+ const ws = new WebSocket(url);
138
+ await new Promise((resolve, reject) => {
139
+ ws.on('open', resolve);
140
+ ws.on('error', reject);
141
+ ws.on('close', () => reject(new Error('closed before open')));
142
+ controller.signal.addEventListener('abort', () => {
143
+ ws.close();
144
+ reject(new Error('timeout'));
145
+ });
146
+ });
147
+ clearTimeout(timer);
148
+ ws.close();
149
+ if (attempt > 1) {
150
+ return { ok: true, message: `WebSocket connected at ${url} (tried ${attempt}x)` };
151
+ }
152
+ return { ok: true, message: `WebSocket connected at ${url}` };
153
+ } catch (err) {
154
+ clearTimeout(timer);
155
+ lastErr = err.message === 'timeout'
156
+ ? `WebSocket timeout on ${url}`
157
+ : `WebSocket failed on ${url}: ${err.message}`;
158
+ }
159
+ } catch (err) {
160
+ // ws module not available in this environment — skip
161
+ if (err.code === 'MODULE_NOT_FOUND' || err.message?.includes('ws')) {
162
+ return { ok: true, message: 'ws module not available — skipping WebSocket check' };
163
+ }
164
+ lastErr = `WebSocket check failed: ${err.message}`;
165
+ }
166
+ if (attempt < maxAttempts) {
167
+ await new Promise((r) => setTimeout(r, delayMs));
168
+ }
169
+ }
170
+ return { ok: false, message: lastErr || 'WebSocket failed' };
171
+ });
172
+
173
+ // ── 5. `bizar bg list` returns ────────────────────────────────────────────
81
174
  check('background agents', () => {
82
175
  try {
83
176
  execSync('bizar bg list', {
@@ -5,11 +5,9 @@
5
5
  *
6
6
  * Tests:
7
7
  * 1. lightrag-server check: file existence is verified
8
- * 2. lightrag: warns when not found in PATH or ~/.local/bin/
9
- *
10
- * Note: dashboard HTTP/WS smoke checks were removed in v6.0.0-beta.2
11
- * (the dashboard is optional and the checks were failing noisily on
12
- * systems that don't run it). See PROGRESS.md.
8
+ * 2. HTTP retry: retries on failure, succeeds when server eventually responds
9
+ * 3. WS retry: retries on failure, succeeds when server eventually responds
10
+ * 4. lightrag: warns when not found in PATH or ~/.local/bin/
13
11
  */
14
12
  import { test, describe, beforeEach, afterEach } from 'node:test';
15
13
  import assert from 'node:assert/strict';
package/cli/provision.mjs CHANGED
@@ -860,8 +860,46 @@ export async function patchClineJson({ dryRun, force }) {
860
860
  (p) => Array.isArray(p) && typeof p[0] === 'string' && p[0].includes('plugins/bizar'),
861
861
  );
862
862
 
863
- // Auto-add provider.minimax block if missing (v5.x — must happen
864
- // even when the plugin entry already exists, so always evaluate).
863
+ // v6.2.0 — Multi-patch installer. On every install/update, ensure the
864
+ // full Bizar config surface is present in cline.json:
865
+ //
866
+ // 1. provider.9router (preferred gateway since v6.0.1) — add if
867
+ // missing so the install is "complete" out of the box.
868
+ // 2. provider.minimax (legacy fallback) — kept for back-compat.
869
+ // 3. default_agent — set to "odin" if missing.
870
+ // 4. $schema — add if missing (helps editors validate).
871
+ // 5. instructions — point at .cline/instructions/bizar-tools.md if
872
+ // missing, so Cline loads the tool reference on every session.
873
+ // 6. plugin entry — add if missing (the critical one; without this
874
+ // the Bizar plugin never loads).
875
+ // 7. permissions — set to "allow" if missing.
876
+ // 8. snapshot — set to false if missing.
877
+ //
878
+ // All patches are additive and idempotent. We never overwrite a value
879
+ // the user has set; the `force` flag bypasses that protection for the
880
+ // plugin entry only.
881
+ let addedProvider = false;
882
+ let addedDefaultAgent = false;
883
+ let addedSchema = false;
884
+ let addedInstructions = false;
885
+ let addedPermissions = false;
886
+ let addedSnapshot = false;
887
+ let added9router = false;
888
+
889
+ if (!cfg.provider) cfg.provider = {};
890
+
891
+ // 1. provider.9router — preferred gateway.
892
+ if (!cfg.provider['9router'] && existsSync(join(REPO_ROOT, 'config', 'cline.json'))) {
893
+ try {
894
+ const tpl = JSON.parse(readFileSync(join(REPO_ROOT, 'config', 'cline.json'), 'utf8'));
895
+ if (tpl.provider && tpl.provider['9router']) {
896
+ cfg.provider['9router'] = tpl.provider['9router'];
897
+ added9router = true;
898
+ }
899
+ } catch { /* ignore template parse errors */ }
900
+ }
901
+
902
+ // 2. provider.minimax — legacy fallback.
865
903
  const DEFAULT_MINIMAX_BLOCK = {
866
904
  options: {
867
905
  baseURL: 'https://api.minimax.io/v1',
@@ -874,21 +912,59 @@ export async function patchClineJson({ dryRun, force }) {
874
912
  'MiniMax-M3-Reasoning': { name: 'MiniMax M3 Reasoning', interleaved: { field: 'reasoning_details' }, reasoning: true },
875
913
  },
876
914
  };
877
- let addedProvider = false;
878
- if (!cfg.provider) {
879
- cfg.provider = {};
880
- }
881
915
  if (!cfg.provider.minimax) {
882
916
  cfg.provider.minimax = DEFAULT_MINIMAX_BLOCK;
883
917
  addedProvider = true;
884
918
  }
885
919
 
886
- if (hasEntry && !force && !addedProvider) {
920
+ // 3. default_agent
921
+ if (!cfg.default_agent) {
922
+ cfg.default_agent = 'odin';
923
+ addedDefaultAgent = true;
924
+ }
925
+
926
+ // 4. $schema
927
+ if (!cfg.$schema) {
928
+ cfg.$schema = 'https://docs.cline.bot/config.json';
929
+ addedSchema = true;
930
+ }
931
+
932
+ // 5. instructions — point at the bundled Bizar tools reference.
933
+ if (!cfg.instructions || (Array.isArray(cfg.instructions) && cfg.instructions.length === 0)) {
934
+ cfg.instructions = ['.cline/instructions/bizar-tools.md'];
935
+ addedInstructions = true;
936
+ }
937
+
938
+ // 6. permission
939
+ if (!cfg.permission) {
940
+ cfg.permission = 'allow';
941
+ addedPermissions = true;
942
+ }
943
+
944
+ // 7. snapshot
945
+ if (typeof cfg.snapshot !== 'boolean') {
946
+ cfg.snapshot = false;
947
+ addedSnapshot = true;
948
+ }
949
+
950
+ const anyAdded = addedProvider || added9router || addedDefaultAgent || addedSchema
951
+ || addedInstructions || addedPermissions || addedSnapshot;
952
+
953
+ if (hasEntry && !force && !anyAdded) {
887
954
  return { ok: true, message: 'cline.json already has Bizar plugin entry' };
888
955
  }
889
956
 
890
957
  if (dryRun) {
891
- return { ok: true, message: `[dry-run] would patch cline.json with plugin entry${addedProvider ? ' + provider.minimax' : ''}` };
958
+ const added = [];
959
+ if (!hasEntry) added.push('plugin entry');
960
+ if (added9router) added.push('provider.9router');
961
+ if (addedProvider) added.push('provider.minimax');
962
+ if (addedDefaultAgent) added.push('default_agent');
963
+ if (addedSchema) added.push('$schema');
964
+ if (addedInstructions) added.push('instructions');
965
+ if (addedPermissions) added.push('permission');
966
+ if (addedSnapshot) added.push('snapshot');
967
+ return { ok: true, message: `[dry-run] would patch cline.json with: ${added.join(', ')}` };
892
968
  }
893
969
 
894
970
  if (!hasEntry) {
@@ -904,16 +980,26 @@ export async function patchClineJson({ dryRun, force }) {
904
980
  loopThresholdEscalate: 8,
905
981
  loopThresholdBlock: 12,
906
982
  loopWindowSize: 10,
983
+ clineruntimeMaxConsecutiveMistakes: 6,
907
984
  }]);
908
985
  cfg.plugin = plugins;
909
986
  }
910
987
 
911
988
  writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
989
+ const added = [];
990
+ if (!hasEntry) added.push('plugin entry');
991
+ if (added9router) added.push('provider.9router');
992
+ if (addedProvider) added.push('provider.minimax');
993
+ if (addedDefaultAgent) added.push('default_agent');
994
+ if (addedSchema) added.push('$schema');
995
+ if (addedInstructions) added.push('instructions');
996
+ if (addedPermissions) added.push('permission');
997
+ if (addedSnapshot) added.push('snapshot');
912
998
  return {
913
999
  ok: true,
914
- message: addedProvider
915
- ? 'cline.json patched with provider.minimax (plugin entry was already present)'
916
- : 'cline.json patched with Bizar plugin entry + provider.minimax',
1000
+ message: added.length > 0
1001
+ ? `cline.json patched: ${added.join(', ')}`
1002
+ : 'cline.json already complete',
917
1003
  };
918
1004
  }
919
1005
 
@@ -317,6 +317,21 @@
317
317
  "description": "Bizar Plugin Menu.",
318
318
  "agent": "odin",
319
319
  "template": "commands-bizar/bizar.md\n\n$ARGUMENTS"
320
+ },
321
+ "team": {
322
+ "description": "Spawn a Cline agent team — coordinate Thor, Tyr, Mimir, Hermod, and Forseti to complete a complex mission end-to-end.",
323
+ "agent": "odin",
324
+ "template": "commands-bizar/team.md\n\n$ARGUMENTS"
325
+ },
326
+ "test": {
327
+ "description": "Run the project's test suite via the Bizar test gate (auto-detects jest/vitest/bun/pytest/cargo/go).",
328
+ "agent": "thor",
329
+ "template": "commands-bizar/test.md\n\n$ARGUMENTS"
330
+ },
331
+ "validate": {
332
+ "description": "Validate the Bizar install: cline.json, plugin entry, agents, skills, hooks, rules, commands, and provider config.",
333
+ "agent": "heimdall",
334
+ "template": "commands-bizar/validate.md\n\n$ARGUMENTS"
320
335
  }
321
336
  },
322
337
  "provider": {