@polderlabs/bizar 10.16.1 → 10.17.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.
Files changed (36) hide show
  1. package/cli/bin.mjs +16 -0
  2. package/cli/commands/evidence.mjs +356 -0
  3. package/cli/commands/install.mjs +34 -12
  4. package/cli/commands/models.mjs +606 -17
  5. package/cli/install/index.mjs +69 -4
  6. package/cli/provision.mjs +212 -6
  7. package/config/claude/agents/office-manager.md +4 -0
  8. package/config/claude/hooks/agent-model-guard.mjs +68 -2
  9. package/config/workflows/bizar-debug.js +8 -6
  10. package/config/workflows/bizar-implement.js +11 -7
  11. package/config/workflows/bizar-research.js +15 -9
  12. package/config/workflows/lib/dispatch.js +859 -0
  13. package/config/workflows/ultracode-research.js +8 -6
  14. package/config/workflows/ultracode-review.js +15 -3
  15. package/config/workflows/ultracode.js +10 -8
  16. package/package.json +1 -1
  17. package/packages/sdk/dist/router/agent-model-registry.d.ts +210 -1
  18. package/packages/sdk/dist/router/agent-model-registry.js +292 -5
  19. package/packages/sdk/dist/router/dispatch-evidence.d.ts +176 -0
  20. package/packages/sdk/dist/router/dispatch-evidence.js +505 -0
  21. package/packages/sdk/dist/router/failover.d.ts +188 -0
  22. package/packages/sdk/dist/router/failover.js +255 -0
  23. package/packages/sdk/dist/router/index.d.ts +84 -9
  24. package/packages/sdk/dist/router/index.js +100 -12
  25. package/packages/sdk/dist/router/model-profile.d.ts +229 -0
  26. package/packages/sdk/dist/router/model-profile.js +199 -0
  27. package/packages/sdk/dist/router/model-router.d.ts +51 -21
  28. package/packages/sdk/dist/router/model-router.js +90 -29
  29. package/packages/sdk/dist/router/outcome-learner.d.ts +180 -0
  30. package/packages/sdk/dist/router/outcome-learner.js +502 -0
  31. package/packages/sdk/dist/router/select-dispatch-model.d.ts +227 -0
  32. package/packages/sdk/dist/router/select-dispatch-model.js +532 -0
  33. package/packages/sdk/dist/version.d.ts +1 -1
  34. package/packages/sdk/dist/version.js +1 -1
  35. package/packages/sdk/package.json +1 -1
  36. package/scripts/git-hooks/pre-push +6 -0
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
+ }
@@ -19,9 +19,11 @@ export function showInstallHelp() {
19
19
  Usage:
20
20
  bizar install Install (or refresh) every component
21
21
  bizar install --dry-run Print what would happen, change nothing
22
- bizar install --force Overwrite existing files AND prune stale
23
- entries in ~/.claude/{agents,skills,
24
- commands,rules,hooks}
22
+ bizar install --force Full clean install: wipe Bizar-managed dirs,
23
+ back up settings env vars, re-sync everything
24
+ from the repo. Preserves ~/.config/bizar/
25
+ login state. Combine with --yes to skip prompts.
26
+ bizar install --deep Alias for --force (clean-install semantics)
25
27
  bizar install --yes Assume yes for any non-destructive prompt
26
28
  bizar install --help Show this help
27
29
 
@@ -30,6 +32,21 @@ export function showInstallHelp() {
30
32
  difference is just mode=install vs mode=update. Every step is
31
33
  idempotent — running this twice is safe.
32
34
 
35
+ F-183 (v10.16.2+) — --force promotes the install from
36
+ overwrite+prune-stale to a fully clean install. It wipes:
37
+ - ~/.claude/{agents,skills,commands,hooks,rules,workflows,plugins}/
38
+ - ~/.agents/ (shared skill-registry + lock)
39
+ - ~/.claude/settings.json (env vars backed up to BIZAR_SAVED_ENV)
40
+ and preserves:
41
+ - ~/.config/bizar/ (login state, telemetry, model picks, worktree-queue)
42
+ - ~/.claude/.credentials.json, statsig/, .playwright-mcp/
43
+ - any user-created subdirs under ~/.claude/ outside the managed set
44
+
45
+ The freshly-emitted settings.json inherits the F-181 wildcard
46
+ permissions.allow expansion and the F-176 empty deny/ask lists
47
+ while the prior ANTHROPIC_* and BIZAR_* env vars are merged back
48
+ in from the stash.
49
+
33
50
  1. Installs @polderlabs/bizar via npm (skipped if already current).
34
51
  2. Shells to ./install.sh for platform-specific system dependencies.
35
52
  3. Syncs agent files, slash commands, and bundled skills into
@@ -38,7 +55,6 @@ export function showInstallHelp() {
38
55
  5. Wires Claude Code lifecycle hooks (SessionStart / PreToolUse /
39
56
  PostToolUse / UserPromptSubmit) under ~/.claude/hooks/.
40
57
  6. Runs 'bizar doctor' as a post-install health check.
41
-
42
58
  No API key collection, no interactive prompts.
43
59
  `);
44
60
  }
@@ -48,7 +64,6 @@ export function showUpdateHelp() {
48
64
  bizar update — Update @anthropic-ai/claude-code + @polderlabs/bizar
49
65
  (which bundles the CLI, SDK, agents, skills, hooks, and commands). Detects what's
50
66
  installed and only touches what's missing or out of date.
51
-
52
67
  Usage:
53
68
  bizar update Update all installed Bizar components
54
69
  bizar update --check Only print current vs. latest; do not update
@@ -57,11 +72,9 @@ export function showUpdateHelp() {
57
72
  bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
58
73
  bizar update --yes Same as --force, but named for one-line scripts
59
74
  bizar update --help Show this help
60
-
61
75
  Components updated:
62
76
  @anthropic-ai/claude-code the Claude Code CLI itself
63
77
  @polderlabs/bizar CLI + SDK + agents + skills + hooks
64
-
65
78
  Behavior (v4.4.7+):
66
79
  • Single unified provisioner. 'bizar install' and 'bizar update' are
67
80
  the same code path with different mode flags. Every step is
@@ -72,13 +85,11 @@ export function showUpdateHelp() {
72
85
  regressions before claude tries to start.
73
86
  • With --check: prints the version matrix and release-notes excerpt
74
87
  between current and latest, exits non-zero if an update is available.
75
-
76
88
  Examples:
77
89
  bizar update Full auto-update (recommended)
78
90
  bizar update --check Show version matrix + notes, do nothing
79
91
  bizar update --channel=beta Upgrade to latest beta build
80
92
  bizar update --dry-run Preview what would change
81
-
82
93
  Errors:
83
94
  Network failures (registry offline / DNS) and npm permission issues
84
95
  are surfaced with the raw npm output. The provisioner never silently
@@ -87,7 +98,6 @@ export function showUpdateHelp() {
87
98
  }
88
99
 
89
100
  // ── Command runners ────────────────────────────────────────────────────────────
90
-
91
101
  export async function install(args, isHelpRequest) {
92
102
  if (isHelpRequest) {
93
103
  showInstallHelp();
@@ -95,9 +105,21 @@ export async function install(args, isHelpRequest) {
95
105
  }
96
106
  // parseFlags lives in cli/provision.mjs and is the canonical argv
97
107
  // parser for the installer family. Reusing it keeps install and
98
- // update in lockstep on flag semantics.
108
+ // update in lockstep on flag semantics. --deep is parsed as an
109
+ // alias for --force (clean-install semantics, F-183).
99
110
  const { mode, dryRun, force, yes } = parseFlags(args);
100
- await runInstaller({ mode, dryRun, force, yes });
111
+ const result = await runInstaller({ mode, dryRun, force, yes });
112
+ // F-183 — print a one-line summary of the wipe scope so operators
113
+ // can see at a glance what changed without re-reading the verbose
114
+ // step list.
115
+ if (force && result?.clean) {
116
+ console.log('');
117
+ console.log(chalk.cyan(` Summary (F-183): wiped ${result.clean.wiped.length} path(s); preserved ${result.clean.preserved.length} path(s).`));
118
+ if (result.doctor) {
119
+ const ok = result.doctor.failed === 0;
120
+ console.log(chalk[ok ? 'green' : 'yellow'](` Doctor: ${result.doctor.passed} passed, ${result.doctor.failed} failed.`));
121
+ }
122
+ }
101
123
  // v4.4.3 — After install, repair any stale bin symlinks so the
102
124
  // user picks up the new code.
103
125
  try {