@polderlabs/bizar 10.16.2 → 10.17.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.
Files changed (33) hide show
  1. package/cli/bin.mjs +16 -0
  2. package/cli/commands/evidence.mjs +356 -0
  3. package/cli/commands/models.mjs +606 -17
  4. package/config/claude/agents/office-manager.md +4 -0
  5. package/config/claude/hooks/agent-model-guard.mjs +68 -2
  6. package/config/workflows/bizar-debug.js +8 -6
  7. package/config/workflows/bizar-implement.js +11 -7
  8. package/config/workflows/bizar-research.js +15 -9
  9. package/config/workflows/lib/dispatch.js +859 -0
  10. package/config/workflows/ultracode-research.js +8 -6
  11. package/config/workflows/ultracode-review.js +15 -3
  12. package/config/workflows/ultracode.js +10 -8
  13. package/package.json +2 -2
  14. package/packages/sdk/dist/router/agent-model-registry.d.ts +210 -1
  15. package/packages/sdk/dist/router/agent-model-registry.js +292 -5
  16. package/packages/sdk/dist/router/dispatch-evidence.d.ts +176 -0
  17. package/packages/sdk/dist/router/dispatch-evidence.js +505 -0
  18. package/packages/sdk/dist/router/failover-mirror.mjs +311 -0
  19. package/packages/sdk/dist/router/failover.d.ts +188 -0
  20. package/packages/sdk/dist/router/failover.js +255 -0
  21. package/packages/sdk/dist/router/index.d.ts +84 -9
  22. package/packages/sdk/dist/router/index.js +100 -12
  23. package/packages/sdk/dist/router/model-profile.d.ts +229 -0
  24. package/packages/sdk/dist/router/model-profile.js +199 -0
  25. package/packages/sdk/dist/router/model-router.d.ts +51 -21
  26. package/packages/sdk/dist/router/model-router.js +90 -29
  27. package/packages/sdk/dist/router/outcome-learner.d.ts +180 -0
  28. package/packages/sdk/dist/router/outcome-learner.js +502 -0
  29. package/packages/sdk/dist/router/select-dispatch-model.d.ts +227 -0
  30. package/packages/sdk/dist/router/select-dispatch-model.js +532 -0
  31. package/packages/sdk/dist/version.d.ts +1 -1
  32. package/packages/sdk/dist/version.js +1 -1
  33. package/packages/sdk/package.json +1 -1
package/cli/bin.mjs CHANGED
@@ -432,6 +432,22 @@ async function main() {
432
432
  break;
433
433
  }
434
434
 
435
+ case 'evidence': {
436
+ const mod = await importCommand('evidence');
437
+ if (!mod) {
438
+ console.error(chalk.red(` ✗ Could not load evidence command module`));
439
+ process.exit(EXIT_ERROR);
440
+ return;
441
+ }
442
+ dbg('loaded command module:', 'evidence');
443
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
444
+ if (found === false) {
445
+ console.error(chalk.red(` ✗ Usage: bizar evidence <subcommand> — run 'bizar evidence --help'`));
446
+ process.exit(EXIT_USAGE);
447
+ }
448
+ break;
449
+ }
450
+
435
451
  case 'model': {
436
452
  // Deprecated alias. Routes to the original `model.mjs` so the
437
453
  // legacy JSON shape (`{ providers: { ... }, total: N }`) and table
@@ -0,0 +1,356 @@
1
+ /**
2
+ * cli/commands/evidence.mjs
3
+ *
4
+ * `bizar evidence` — F-191 / IMP-018 per-dispatch model evidence CLI.
5
+ *
6
+ * bizar evidence tail --limit N # print the last N rows
7
+ * bizar evidence show <id> # print one record (decision + outcome)
8
+ * bizar evidence verify <id> # exit 0 on integrity OK, 1 on mismatch
9
+ * bizar evidence run <runId> # print every record for a run
10
+ * bizar evidence audit # rows missing outcome or with provider mismatch
11
+ *
12
+ * The store lives at `~/.config/bizar/evidence/dispatch.jsonl` by default;
13
+ * `BIZAR_EVIDENCE_DIR` overrides the directory and `BIZAR_HOME` overrides
14
+ * the home root. The wire format is the JSONL produced by the SDK's
15
+ * `createFileEvidenceStore` and the workflow-side `appendEvidence`
16
+ * mirror in `config/workflows/lib/dispatch.js`.
17
+ */
18
+ import chalk from 'chalk';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
21
+ import { homedir } from 'node:os';
22
+
23
+ // ── Evidence dir resolution ──────────────────────────────────────────────────
24
+
25
+ export function resolveEvidenceDir({ cwd = process.cwd(), env = process.env } = {}) {
26
+ if (env.BIZAR_EVIDENCE_DIR && typeof env.BIZAR_EVIDENCE_DIR === 'string') {
27
+ return isAbsolute(env.BIZAR_EVIDENCE_DIR)
28
+ ? env.BIZAR_EVIDENCE_DIR
29
+ : resolve(cwd, env.BIZAR_EVIDENCE_DIR);
30
+ }
31
+ const home = env.BIZAR_HOME || (env.HOME ? `${env.HOME}/.config/bizar` : null)
32
+ || join(homedir(), '.config', 'bizar');
33
+ return join(home, 'evidence');
34
+ }
35
+
36
+ export function evidenceFilePath(opts = {}) {
37
+ return join(resolveEvidenceDir(opts), 'dispatch.jsonl');
38
+ }
39
+
40
+ // ── Pure helpers ─────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Read every line from the JSONL evidence file. Returns an empty array
44
+ * when the file does not exist yet.
45
+ */
46
+ export function readEvidenceRows(opts = {}) {
47
+ const path = evidenceFilePath(opts);
48
+ if (!existsSync(path)) return [];
49
+ let raw;
50
+ try { raw = readFileSync(path, 'utf8'); } catch { return []; }
51
+ const rows = [];
52
+ for (const line of raw.split('\n')) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed) continue;
55
+ try { rows.push(JSON.parse(trimmed)); } catch { /* skip malformed lines */ }
56
+ }
57
+ return rows;
58
+ }
59
+
60
+ /**
61
+ * Pure structural integrity check. Mirrors the SDK's
62
+ * `EvidenceStore.verifyIntegrity` so the CLI matches the SDK without
63
+ * importing the TS source.
64
+ *
65
+ * Returns `{ ok: true }` when the row + chain is intact, otherwise
66
+ * `{ ok: false, reason: '<machine-readable string>' }`.
67
+ */
68
+ export function verifyIntegrityForRow(row) {
69
+ if (!row || typeof row !== 'object') return { ok: false, reason: 'not-found' };
70
+ if (row.decision && typeof row.decision === 'object'
71
+ && row.decision.routingDecisionId !== row.routingDecisionId) {
72
+ return { ok: false, reason: 'decision-id-mismatch' };
73
+ }
74
+ const inputs = row.inputs && typeof row.inputs === 'object' ? row.inputs : null;
75
+ if (!inputs) return { ok: false, reason: 'inputs-missing' };
76
+ for (const field of ['selectedProfilesHash', 'staticProfilesHash', 'budgetHash', 'healthHash']) {
77
+ if (typeof inputs[field] !== 'string' || !/^[0-9a-f]{64}$/.test(inputs[field])) {
78
+ return { ok: false, reason: `inputs-hash-mismatch:${field}` };
79
+ }
80
+ }
81
+ if (!row.schemaVersion || row.schemaVersion !== 1) {
82
+ return { ok: false, reason: 'schema-version-mismatch' };
83
+ }
84
+ if (!row.createdAt || Number.isNaN(Date.parse(row.createdAt))) {
85
+ return { ok: false, reason: 'createdAt-missing' };
86
+ }
87
+ return { ok: true };
88
+ }
89
+
90
+ export function verifyIntegrityForId(routingDecisionId, opts = {}) {
91
+ const rows = readEvidenceRows(opts).filter((r) => r.routingDecisionId === routingDecisionId);
92
+ if (rows.length === 0) return { ok: false, reason: 'not-found' };
93
+ for (const row of rows) {
94
+ const check = verifyIntegrityForRow(row);
95
+ if (!check.ok) return check;
96
+ }
97
+ return { ok: true };
98
+ }
99
+
100
+ export function tailRows({ limit = 10, dir } = {}) {
101
+ const rows = readEvidenceRows(dir ? { cwd: process.cwd(), env: { ...process.env, BIZAR_EVIDENCE_DIR: dir } } : {});
102
+ const safeLimit = typeof limit === 'number' && limit >= 0 ? limit : rows.length;
103
+ return rows.slice(Math.max(0, rows.length - safeLimit));
104
+ }
105
+
106
+ export function findByRunId(runId, opts = {}) {
107
+ if (typeof runId !== 'string' || !runId) return [];
108
+ return readEvidenceRows(opts).filter((r) => r.runId === runId);
109
+ }
110
+
111
+ export function getById(routingDecisionId, opts = {}) {
112
+ const rows = readEvidenceRows(opts).filter((r) => r.routingDecisionId === routingDecisionId);
113
+ if (rows.length === 0) return null;
114
+ return rows.reduce((latest, row) => (row.sequence ?? 0) > (latest.sequence ?? 0) ? row : latest);
115
+ }
116
+
117
+ /**
118
+ * Audit rows: missing outcome OR `actualProviderModel` disagrees with
119
+ * `decision.modelId`. The 100% agreement metric from IMPROVEMENTS.md
120
+ * line 881 lives here.
121
+ */
122
+ export function auditRows(opts = {}) {
123
+ const rows = readEvidenceRows(opts);
124
+ const primaries = rows.filter((r) => (r.sequence ?? 0) === 0);
125
+ const missingOutcome = primaries.filter((r) => !r.outcome);
126
+ const mismatched = primaries.filter((r) => r.outcome
127
+ && typeof r.outcome.actualProviderModel === 'string'
128
+ && r.decision
129
+ && typeof r.decision.modelId === 'string'
130
+ && r.outcome.actualProviderModel !== r.decision.modelId);
131
+ return { missingOutcome, mismatched, totalRows: rows.length, primaryRows: primaries.length };
132
+ }
133
+
134
+ // ── Help / argument parsing ─────────────────────────────────────────────────
135
+
136
+ function showHelp() {
137
+ console.log(`
138
+ bizar evidence — F-191 per-dispatch model evidence audit trail
139
+
140
+ Usage:
141
+ bizar evidence tail --limit N Print the last N rows (default 10)
142
+ bizar evidence show <routingDecisionId>
143
+ Print one record (decision + outcome)
144
+ bizar evidence verify <routingDecisionId>
145
+ Run integrity check; exit 0 / 1
146
+ bizar evidence run <runId> Print every record for a run
147
+ bizar evidence audit Rows missing outcome or with model mismatch
148
+
149
+ Common flags:
150
+ --dir <path> Override the evidence directory
151
+ --json Emit machine-readable JSON
152
+ `);
153
+ }
154
+
155
+ function parseLimit(args) {
156
+ const flag = args.find((a) => a.startsWith('--limit='));
157
+ if (flag) {
158
+ const value = Number(flag.slice('--limit='.length));
159
+ if (Number.isFinite(value) && value >= 0) return value;
160
+ }
161
+ const idx = args.indexOf('--limit');
162
+ if (idx !== -1 && idx + 1 < args.length) {
163
+ const value = Number(args[idx + 1]);
164
+ if (Number.isFinite(value) && value >= 0) return value;
165
+ }
166
+ return 10;
167
+ }
168
+
169
+ function parseDir(args) {
170
+ const flag = args.find((a) => a.startsWith('--dir='));
171
+ if (flag) return flag.slice('--dir='.length);
172
+ const idx = args.indexOf('--dir');
173
+ if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
174
+ return undefined;
175
+ }
176
+
177
+ // ── Subcommand handlers ──────────────────────────────────────────────────────
178
+
179
+ function printRow(row, { json = false } = {}) {
180
+ if (json) {
181
+ process.stdout.write(JSON.stringify(row, null, 2) + '\n');
182
+ return;
183
+ }
184
+ const head = `${row.routingDecisionId} seq=${row.sequence ?? 0} run=${row.runId} agent=${row.agentName ?? '-'} phase=${row.workflowPhase ?? '-'} created=${row.createdAt}`;
185
+ console.log(head);
186
+ console.log(` decision.modelId = ${row.decision?.modelId ?? '(null)'}`);
187
+ console.log(` decision.tier = ${row.decision?.tier ?? '(unknown)'}`);
188
+ console.log(` decision.reason = ${row.decision?.reason ?? '-'}`);
189
+ if (row.outcome) {
190
+ console.log(` outcome.status = ${row.outcome.status}`);
191
+ if (typeof row.outcome.durationMs === 'number') {
192
+ console.log(` outcome.duration = ${row.outcome.durationMs}ms`);
193
+ }
194
+ if (row.outcome.actualProviderModel) {
195
+ console.log(` outcome.actual = ${row.outcome.actualProviderModel}`);
196
+ }
197
+ if (row.outcome.errorMessage) {
198
+ console.log(` outcome.error = ${row.outcome.errorMessage}`);
199
+ }
200
+ } else {
201
+ console.log(chalk.yellow(' outcome = (missing)'));
202
+ }
203
+ console.log('');
204
+ }
205
+
206
+ function handleTail(args) {
207
+ const wantJson = args.includes('--json');
208
+ const limit = parseLimit(args);
209
+ const dir = parseDir(args);
210
+ const rows = tailRows({ limit, dir });
211
+ if (wantJson) {
212
+ process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
213
+ return 0;
214
+ }
215
+ if (rows.length === 0) {
216
+ console.log(chalk.yellow(' ! evidence store is empty (or file does not exist)'));
217
+ return 0;
218
+ }
219
+ for (const row of rows) printRow(row, {});
220
+ return 0;
221
+ }
222
+
223
+ function handleShow(args) {
224
+ const wantJson = args.includes('--json');
225
+ const positional = args.filter((a) => !a.startsWith('-'));
226
+ const id = positional[0];
227
+ if (!id) {
228
+ console.error(chalk.red(' ✗ bizar evidence show requires a routingDecisionId'));
229
+ return 2;
230
+ }
231
+ const dir = parseDir(args);
232
+ const row = getById(id, dir ? { cwd: process.cwd(), env: { ...process.env, BIZAR_EVIDENCE_DIR: dir } } : {});
233
+ if (!row) {
234
+ if (wantJson) {
235
+ process.stdout.write(JSON.stringify({ error: `routingDecisionId=${id} not found` }, null, 2) + '\n');
236
+ } else {
237
+ console.error(chalk.red(` ✗ routingDecisionId=${id} not found`));
238
+ }
239
+ return 2;
240
+ }
241
+ if (wantJson) {
242
+ process.stdout.write(JSON.stringify(row, null, 2) + '\n');
243
+ return 0;
244
+ }
245
+ printRow(row, {});
246
+ return 0;
247
+ }
248
+
249
+ function handleVerify(args) {
250
+ const wantJson = args.includes('--json');
251
+ const positional = args.filter((a) => !a.startsWith('-'));
252
+ const id = positional[0];
253
+ if (!id) {
254
+ console.error(chalk.red(' ✗ bizar evidence verify requires a routingDecisionId'));
255
+ return 2;
256
+ }
257
+ const dir = parseDir(args);
258
+ const check = verifyIntegrityForId(id, dir ? { cwd: process.cwd(), env: { ...process.env, BIZAR_EVIDENCE_DIR: dir } } : {});
259
+ if (wantJson) {
260
+ process.stdout.write(JSON.stringify(check, null, 2) + '\n');
261
+ } else if (!check.ok) {
262
+ console.error(chalk.red(` ✗ integrity check failed: ${check.reason}`));
263
+ } else {
264
+ console.log(chalk.green(` ✓ integrity OK for ${id}`));
265
+ }
266
+ return check.ok ? 0 : 1;
267
+ }
268
+
269
+ function handleRun(args) {
270
+ const wantJson = args.includes('--json');
271
+ const positional = args.filter((a) => !a.startsWith('-'));
272
+ const runId = positional[0];
273
+ if (!runId) {
274
+ console.error(chalk.red(' ✗ bizar evidence run requires a runId'));
275
+ return 2;
276
+ }
277
+ const dir = parseDir(args);
278
+ const rows = findByRunId(runId, dir ? { cwd: process.cwd(), env: { ...process.env, BIZAR_EVIDENCE_DIR: dir } } : {});
279
+ if (wantJson) {
280
+ process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
281
+ return 0;
282
+ }
283
+ if (rows.length === 0) {
284
+ console.log(chalk.yellow(` ! no rows for runId=${runId}`));
285
+ return 0;
286
+ }
287
+ for (const row of rows) printRow(row, {});
288
+ return 0;
289
+ }
290
+
291
+ function handleAudit(args) {
292
+ const wantJson = args.includes('--json');
293
+ const dir = parseDir(args);
294
+ const report = auditRows(dir ? { cwd: process.cwd(), env: { ...process.env, BIZAR_EVIDENCE_DIR: dir } } : {});
295
+ if (wantJson) {
296
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
297
+ return report.missingOutcome.length === 0 && report.mismatched.length === 0 ? 0 : 1;
298
+ }
299
+ console.log(chalk.green(` primary rows: ${report.primaryRows} total rows: ${report.totalRows}`));
300
+ console.log(chalk.yellow(` missing outcome: ${report.missingOutcome.length}`));
301
+ console.log(chalk.yellow(` provider-model mismatch: ${report.mismatched.length}`));
302
+ if (report.missingOutcome.length > 0) {
303
+ console.log('');
304
+ console.log(chalk.yellow(' Missing outcome:'));
305
+ for (const row of report.missingOutcome.slice(0, 20)) {
306
+ console.log(` ${row.routingDecisionId} decision.modelId=${row.decision?.modelId ?? '(null)'} run=${row.runId}`);
307
+ }
308
+ }
309
+ if (report.mismatched.length > 0) {
310
+ console.log('');
311
+ console.log(chalk.yellow(' Provider-model mismatch:'));
312
+ for (const row of report.mismatched.slice(0, 20)) {
313
+ console.log(` ${row.routingDecisionId} decision.modelId=${row.decision?.modelId} actualProviderModel=${row.outcome?.actualProviderModel}`);
314
+ }
315
+ }
316
+ return report.missingOutcome.length === 0 && report.mismatched.length === 0 ? 0 : 1;
317
+ }
318
+
319
+ // ── run() entrypoint ─────────────────────────────────────────────────────────
320
+
321
+ export async function run(name, args, isHelpRequest) {
322
+ if (name !== 'evidence') return false;
323
+ if (isHelpRequest || args.includes('--help') || args.includes('-h')) {
324
+ showHelp();
325
+ return true;
326
+ }
327
+ const sub = args[0];
328
+ if (!sub) {
329
+ showHelp();
330
+ return true;
331
+ }
332
+ // Strip the subcommand from args before delegating.
333
+ const rest = args.slice(1);
334
+ switch (sub) {
335
+ case 'tail':
336
+ process.exit(handleTail(rest));
337
+ return true;
338
+ case 'show':
339
+ process.exit(handleShow(rest));
340
+ return true;
341
+ case 'verify':
342
+ process.exit(handleVerify(rest));
343
+ return true;
344
+ case 'run':
345
+ process.exit(handleRun(rest));
346
+ return true;
347
+ case 'audit':
348
+ process.exit(handleAudit(rest));
349
+ return true;
350
+ default:
351
+ console.error(chalk.red(` ✗ unknown evidence subcommand: ${sub}`));
352
+ showHelp();
353
+ process.exit(2);
354
+ return true;
355
+ }
356
+ }