@wrongstack/cli 0.9.19 → 0.10.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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as path24 from 'path';
3
3
  import * as fsp3 from 'fs/promises';
4
- import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
4
+ import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
5
5
  import { createRequire } from 'module';
6
6
  import * as os3 from 'os';
7
7
  import os3__default from 'os';
@@ -13,6 +13,7 @@ import { MCPRegistry } from '@wrongstack/mcp';
13
13
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
14
14
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
15
15
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
16
+ import { fileURLToPath } from 'url';
16
17
  import * as readline from 'readline';
17
18
  import * as fs10 from 'fs';
18
19
  import { writeFileSync } from 'fs';
@@ -380,12 +381,9 @@ function buildSddCommand(opts) {
380
381
  name: "sdd",
381
382
  description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
382
383
  async run(args) {
383
- opts.context;
384
384
  if (!opts.paths) return { message: "SDD not available \u2014 paths not configured." };
385
385
  const specsDir = opts.paths.projectSpecs;
386
- const graphsDir = opts.paths.projectTaskGraphs;
387
386
  const specStore = new SpecStore({ baseDir: specsDir });
388
- new TaskGraphStore({ baseDir: graphsDir });
389
387
  const versioning = sddState.getVersioning();
390
388
  const [verb, ...rest] = args.trim().split(/\s+/);
391
389
  const restJoined = rest.join(" ").trim();
@@ -1024,8 +1022,8 @@ Start executing the tasks one by one.`
1024
1022
  return { message: lines2.join("\n") };
1025
1023
  }
1026
1024
  try {
1027
- const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1028
- const stored = await graphStore2.load(graphId);
1025
+ const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1026
+ const stored = await graphStore.load(graphId);
1029
1027
  if (stored) {
1030
1028
  return { message: renderTaskGraph(stored, { compact: false }) };
1031
1029
  }
@@ -1245,8 +1243,8 @@ ${lines.join("\n")}`
1245
1243
  return { message: "No task graph found. Generate tasks first." };
1246
1244
  }
1247
1245
  try {
1248
- const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1249
- const graph = await graphStore2.load(graphId);
1246
+ const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1247
+ const graph = await graphStore.load(graphId);
1250
1248
  if (!graph) {
1251
1249
  return { message: "Could not load task graph." };
1252
1250
  }
@@ -1659,7 +1657,7 @@ async function checkForUpdate(signal, homeFn) {
1659
1657
  outdated: isNewer(latest, current),
1660
1658
  checkFailed: false
1661
1659
  };
1662
- } catch (err) {
1660
+ } catch (_err) {
1663
1661
  if (aborted()) {
1664
1662
  return { current, latest: current, outdated: false, checkFailed: true };
1665
1663
  }
@@ -1980,7 +1978,7 @@ async function runWebUI(opts) {
1980
1978
  }
1981
1979
  }
1982
1980
  }
1983
- async function handleUserMessage(ws, client, content) {
1981
+ async function handleUserMessage(ws, _client, content) {
1984
1982
  if (abortController) {
1985
1983
  send(ws, {
1986
1984
  type: "error",
@@ -2356,6 +2354,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2356
2354
  "recover",
2357
2355
  "no-alt-screen",
2358
2356
  "alt-screen",
2357
+ "mouse",
2359
2358
  "output-json",
2360
2359
  "prompt",
2361
2360
  "metrics",
@@ -3553,8 +3552,85 @@ function buildLoadCommand(opts) {
3553
3552
  return {
3554
3553
  name: "resume",
3555
3554
  aliases: ["load", "sessions"],
3556
- description: "List recent sessions.",
3557
- async run() {
3555
+ description: "List recent sessions, show incomplete ones (--incomplete), or plan a recovery (--recover <id>).",
3556
+ async run(args) {
3557
+ const parts = args.split(/\s+/).filter(Boolean);
3558
+ const showIncomplete = parts.includes("--incomplete") || parts.includes("-i");
3559
+ const recoverIdx = parts.findIndex((p) => p === "--recover");
3560
+ const recoverTarget = recoverIdx >= 0 ? parts[recoverIdx + 1] : void 0;
3561
+ if (recoverTarget) {
3562
+ if (!opts.paths) {
3563
+ return { message: color.yellow("No paths configured \u2014 cannot build a recovery plan.") };
3564
+ }
3565
+ const recovery = new SessionRecovery(opts.paths.projectSessions);
3566
+ const plan = await recovery.recover(recoverTarget);
3567
+ if (!plan) {
3568
+ return {
3569
+ message: color.yellow(
3570
+ `No session log found for ${recoverTarget} (or it is empty).`
3571
+ )
3572
+ };
3573
+ }
3574
+ const lines2 = [
3575
+ color.bold(`Recovery plan for ${plan.sessionId}`),
3576
+ ` Stale: ${plan.stale ? color.yellow("yes") : color.green("no")}`
3577
+ ];
3578
+ if (plan.context) {
3579
+ lines2.push(` Last in-flight context: ${color.cyan(plan.context)}`);
3580
+ }
3581
+ if (plan.lastCheckpoint && plan.lastCheckpoint.type === "checkpoint") {
3582
+ const cp = plan.lastCheckpoint;
3583
+ lines2.push(
3584
+ ` Last checkpoint: promptIndex=${cp.promptIndex} preview=${color.dim(`"${cp.promptPreview}"`)} at ${color.dim(cp.ts)}`
3585
+ );
3586
+ } else {
3587
+ lines2.push(` Last checkpoint: ${color.dim("(none \u2014 full re-execution)")}`);
3588
+ }
3589
+ lines2.push(
3590
+ ` Pending events: ${plan.pendingEvents.length} (the work that would re-run on resume)`
3591
+ );
3592
+ if (plan.pendingEvents.length > 0) {
3593
+ const summary = summarizePending(plan.pendingEvents);
3594
+ lines2.push(...summary);
3595
+ }
3596
+ lines2.push("");
3597
+ lines2.push(
3598
+ color.dim(
3599
+ plan.stale ? " This session crashed mid-iteration. The full re-execution kernel is coming in a follow-up; for now use this plan to decide whether to start fresh." : " This session ended cleanly; the plan above describes the most recent turn(s) for context."
3600
+ )
3601
+ );
3602
+ return { message: lines2.join("\n") };
3603
+ }
3604
+ if (showIncomplete) {
3605
+ if (!opts.paths) {
3606
+ return { message: color.yellow("No paths configured \u2014 cannot scan for incomplete sessions.") };
3607
+ }
3608
+ const recovery = new SessionRecovery(opts.paths.projectSessions);
3609
+ const resumable = await recovery.listResumable();
3610
+ if (resumable.length === 0) {
3611
+ return {
3612
+ message: color.dim("No incomplete sessions. (Every recorded run ended cleanly.)")
3613
+ };
3614
+ }
3615
+ const lines2 = [
3616
+ color.bold(`${resumable.length} incomplete session(s)`),
3617
+ color.dim(" (process died mid-iteration; the last event was in_flight_start)"),
3618
+ ""
3619
+ ];
3620
+ for (const s of resumable) {
3621
+ const t = color.dim(s.lastEventTs.slice(0, 19).replace("T", " "));
3622
+ lines2.push(
3623
+ ` ${color.cyan(s.sessionId)} ${t} ${color.dim(`${s.eventCount} events`)} ${s.context}`
3624
+ );
3625
+ }
3626
+ lines2.push("");
3627
+ lines2.push(
3628
+ color.dim(
3629
+ " Resuming re-executes the in-flight work from the last checkpoint. Full resume is coming in a follow-up; for now use this list to identify crashes and decide whether to restart fresh."
3630
+ )
3631
+ );
3632
+ return { message: lines2.join("\n") };
3633
+ }
3558
3634
  if (!opts.sessionStore) return { message: "No session store configured." };
3559
3635
  const list = await opts.sessionStore.list(10);
3560
3636
  if (list.length === 0) return { message: "No saved sessions." };
@@ -3565,7 +3641,8 @@ function buildLoadCommand(opts) {
3565
3641
  ${lines.join("\n")}
3566
3642
 
3567
3643
  ${color.dim(`Resume one with: wstack resume ${list[0]?.id ?? "<id>"}
3568
- `)}`;
3644
+ `)}
3645
+ ${color.dim("Tip: /resume --incomplete \u2014 list sessions that crashed mid-iteration")}`;
3569
3646
  opts.renderer.write(msg);
3570
3647
  return { message: msg };
3571
3648
  }
@@ -3589,6 +3666,47 @@ function buildExitCommand(opts) {
3589
3666
  }
3590
3667
  };
3591
3668
  }
3669
+ function summarizePending(events) {
3670
+ const lines = [];
3671
+ const cap = 12;
3672
+ for (const ev of events.slice(-cap)) {
3673
+ const t = "ts" in ev ? color.dim(String(ev.ts).slice(11, 19)) : color.dim("--:--:--");
3674
+ const kind = color.cyan(String(ev.type).padEnd(18));
3675
+ lines.push(` ${t} ${kind} ${summariseEvent(ev)}`);
3676
+ }
3677
+ if (events.length > cap) {
3678
+ lines.push(color.dim(` \u2026 and ${events.length - cap} more`));
3679
+ }
3680
+ return lines;
3681
+ }
3682
+ function summariseEvent(ev) {
3683
+ switch (ev.type) {
3684
+ case "user_input": {
3685
+ const text = "text" in ev && typeof ev.text === "string" ? ev.text : Array.isArray(ev.content) ? "\u2026" : "";
3686
+ return color.dim(text.length > 60 ? text.slice(0, 59) + "\u2026" : text);
3687
+ }
3688
+ case "llm_response":
3689
+ return color.dim("(model reply)");
3690
+ case "tool_use":
3691
+ return color.dim(`name=${ev.name ?? "?"}`);
3692
+ case "tool_result":
3693
+ return color.dim(`name=${ev.name ?? "?"}`);
3694
+ case "in_flight_start":
3695
+ return color.dim(`context="${ev.context ?? ""}"`);
3696
+ case "in_flight_end":
3697
+ return color.dim(`reason=${ev.reason ?? "?"}`);
3698
+ case "checkpoint":
3699
+ return color.dim(
3700
+ `promptIndex=${ev.promptIndex ?? "?"}`
3701
+ );
3702
+ case "compaction":
3703
+ return color.dim("(compaction)");
3704
+ case "error":
3705
+ return color.red(String(ev.message ?? ""));
3706
+ default:
3707
+ return color.dim("\u2026");
3708
+ }
3709
+ }
3592
3710
 
3593
3711
  // src/slash-commands/spawn-agents.ts
3594
3712
  function buildSpawnCommand(opts) {
@@ -4173,8 +4291,48 @@ ${lines.join("\n")}`;
4173
4291
  }
4174
4292
  };
4175
4293
  }
4176
-
4177
- // src/slash-commands/mode.ts
4294
+ async function runModePicker(modeStore, reader) {
4295
+ const modes = await modeStore.listModes();
4296
+ const active = await modeStore.getActiveMode();
4297
+ const activeIdx = modes.findIndex((m) => m.id === active?.id);
4298
+ let cursor = activeIdx >= 0 ? activeIdx : 0;
4299
+ const render = (currentCursor) => {
4300
+ const lines = [];
4301
+ lines.push(`
4302
+ ${color.bold(color.amber("WrongStack") + color.dim(" \u2014 Mode Selection"))}
4303
+ `);
4304
+ lines.push(color.dim(" \u2191\u2193 navigate Enter select q quit\n"));
4305
+ lines.push("");
4306
+ for (let i = 0; i < modes.length; i++) {
4307
+ const m = modes[i];
4308
+ const mark = m.id === active?.id ? color.green(" [active]") : "";
4309
+ const prefix = i === currentCursor ? color.bold("\u276F ") : " ";
4310
+ const name = i === currentCursor ? color.bold(m.name) : m.name;
4311
+ lines.push(` ${prefix}${name} ${color.dim(m.description)}${mark}`);
4312
+ }
4313
+ lines.push("");
4314
+ return lines.join("\n");
4315
+ };
4316
+ process.stdout.write(render(cursor));
4317
+ const options = [
4318
+ { key: "\x1B[A", label: "\u2191", value: "up" },
4319
+ { key: "\x1B[B", label: "\u2193", value: "down" },
4320
+ { key: "\r", label: "Enter", value: "enter" },
4321
+ { key: "q", label: "q", value: "quit" }
4322
+ ];
4323
+ while (true) {
4324
+ const answer = await reader.readKey("", options);
4325
+ if (answer === "quit") return void 0;
4326
+ if (answer === "enter") return modes[cursor];
4327
+ if (answer === "up") {
4328
+ cursor = cursor > 0 ? cursor - 1 : modes.length - 1;
4329
+ } else if (answer === "down") {
4330
+ cursor = cursor < modes.length - 1 ? cursor + 1 : 0;
4331
+ }
4332
+ process.stdout.write("\x1B[J");
4333
+ process.stdout.write(render(cursor));
4334
+ }
4335
+ }
4178
4336
  function buildModeCommand(opts) {
4179
4337
  return {
4180
4338
  name: "mode",
@@ -4194,7 +4352,7 @@ function buildModeCommand(opts) {
4194
4352
  " /mode brief Switch to brief mode",
4195
4353
  " /mode teach Switch to teach mode"
4196
4354
  ].join("\n"),
4197
- async run(args) {
4355
+ async run(args, _ctx) {
4198
4356
  const modeStore = opts.modeStore;
4199
4357
  if (!modeStore) {
4200
4358
  return { message: "Mode store not available in this context." };
@@ -4202,6 +4360,17 @@ function buildModeCommand(opts) {
4202
4360
  const modes = await modeStore.listModes();
4203
4361
  const active = await modeStore.getActiveMode();
4204
4362
  if (!args.trim()) {
4363
+ if (opts.inputReader) {
4364
+ const selected = await runModePicker(modeStore, opts.inputReader);
4365
+ if (!selected) {
4366
+ return { message: "Mode selection cancelled." };
4367
+ }
4368
+ await modeStore.setActiveMode(selected.id);
4369
+ return {
4370
+ message: `Switched to "${selected.name}" mode.
4371
+ ${selected.description}`
4372
+ };
4373
+ }
4205
4374
  const lines = [`Current mode: ${active?.name ?? "none"}`, "", "Available modes:"];
4206
4375
  for (const m of modes) {
4207
4376
  const mark = m.id === active?.id ? " [active]" : "";
@@ -5306,6 +5475,195 @@ function buildAutoPhaseCommand(opts) {
5306
5475
  }
5307
5476
  };
5308
5477
  }
5478
+ function buildCollabCommand(opts) {
5479
+ return {
5480
+ name: "collab",
5481
+ description: "Live collaboration helpers (status / invite / history).",
5482
+ async run(args, ctx) {
5483
+ const parts = args.split(/\s+/).filter(Boolean);
5484
+ const sub = (parts[0] ?? "status").toLowerCase();
5485
+ switch (sub) {
5486
+ case "status":
5487
+ return statusCommand(ctx.session?.id);
5488
+ case "invite":
5489
+ return inviteCommand(ctx.session?.id);
5490
+ case "history":
5491
+ return historyCommand(opts, ctx.session?.id, parts.slice(1));
5492
+ case "annotations":
5493
+ case "notes":
5494
+ return annotationsCommand(opts, ctx.session?.id);
5495
+ case "help":
5496
+ case "--help":
5497
+ case "-h":
5498
+ return helpCommand();
5499
+ default:
5500
+ return {
5501
+ message: color.yellow(
5502
+ `Unknown subcommand: ${sub}. Try: /collab status | invite | history | annotations`
5503
+ )
5504
+ };
5505
+ }
5506
+ }
5507
+ };
5508
+ }
5509
+ function statusCommand(sessionId) {
5510
+ const lines = [];
5511
+ lines.push(color.bold("Live collaboration"));
5512
+ lines.push(` Session: ${color.cyan(sessionId ?? "(none)")}`);
5513
+ lines.push(` Observers: ${color.dim("unknown (check webui)")}`);
5514
+ lines.push("");
5515
+ lines.push(
5516
+ color.dim(
5517
+ " Tip: /collab invite \u2014 print a webui join link for a teammate"
5518
+ )
5519
+ );
5520
+ lines.push(
5521
+ color.dim(" /collab history 20 \u2014 show the last 20 events of this session")
5522
+ );
5523
+ return { message: lines.join("\n") };
5524
+ }
5525
+ function inviteCommand(sessionId) {
5526
+ if (!sessionId) {
5527
+ return { message: color.yellow("No active session to invite to.") };
5528
+ }
5529
+ const url = `http://127.0.0.1:3457/?session=${encodeURIComponent(sessionId)}`;
5530
+ const lines = [
5531
+ color.bold("Live-collaboration invite"),
5532
+ ` Session ID: ${color.cyan(sessionId)}`,
5533
+ ` Share this URL with a teammate (they must be able to reach your machine):`,
5534
+ ` ${color.cyan(url)}`,
5535
+ "",
5536
+ color.dim(
5537
+ ' The teammate opens the URL in their browser, then clicks "Join as observer"'
5538
+ ),
5539
+ color.dim(" on the panel that appears above the chat. They will see a live mirror"),
5540
+ color.dim(" of tool calls, iterations, and subagent spawns \u2014 read-only."),
5541
+ "",
5542
+ color.dim(" Reminder: expose the port with: pnpm --filter @wrongstack/webui dev")
5543
+ ];
5544
+ return { message: lines.join("\n") };
5545
+ }
5546
+ async function historyCommand(opts, sessionId, args) {
5547
+ const limit = Math.max(1, Math.min(200, Number.parseInt(args[0] ?? "20", 10) || 20));
5548
+ if (!sessionId) return { message: color.yellow("No active session.") };
5549
+ if (!opts.sessionStore) {
5550
+ return { message: color.yellow("No session store configured.") };
5551
+ }
5552
+ const { DefaultSessionReader: DefaultSessionReader2 } = await import('@wrongstack/core');
5553
+ const reader = new DefaultSessionReader2({ store: opts.sessionStore });
5554
+ const events = [];
5555
+ try {
5556
+ for await (const ev of reader.replay(sessionId)) {
5557
+ events.push(ev);
5558
+ }
5559
+ } catch (err) {
5560
+ return {
5561
+ message: color.yellow(
5562
+ `Failed to read session: ${err instanceof Error ? err.message : String(err)}`
5563
+ )
5564
+ };
5565
+ }
5566
+ const tail = events.slice(-limit);
5567
+ if (tail.length === 0) {
5568
+ return { message: color.dim("No events recorded yet for this session.") };
5569
+ }
5570
+ const lines = [
5571
+ color.bold(`Last ${tail.length} events of ${sessionId}`),
5572
+ ""
5573
+ ];
5574
+ for (const raw of tail) {
5575
+ const ev = raw;
5576
+ const t = ev.ts ? color.dim(ev.ts.slice(11, 19)) : color.dim("--:--:--");
5577
+ const kind = color.cyan((ev.type ?? "unknown").padEnd(16));
5578
+ const summary = summarise(ev);
5579
+ lines.push(` ${t} ${kind} ${summary}`);
5580
+ }
5581
+ return { message: lines.join("\n") };
5582
+ }
5583
+ async function annotationsCommand(opts, sessionId) {
5584
+ if (!sessionId) return { message: color.yellow("No active session.") };
5585
+ if (!opts.sessionStore) {
5586
+ return { message: color.yellow("No session store configured.") };
5587
+ }
5588
+ const { DefaultSessionReader: DefaultSessionReader2, AnnotationsStore } = await import('@wrongstack/core');
5589
+ new DefaultSessionReader2({ store: opts.sessionStore });
5590
+ const storeDir = opts.sessionStore.dir;
5591
+ if (!storeDir) {
5592
+ return {
5593
+ message: color.yellow(
5594
+ "Annotations view needs the session store dir; not exposed in this build."
5595
+ )
5596
+ };
5597
+ }
5598
+ const annotations = new AnnotationsStore({ dir: storeDir });
5599
+ const open = await annotations.listOpen(sessionId);
5600
+ if (open.length === 0) {
5601
+ return {
5602
+ message: color.dim("No open annotations for this session.")
5603
+ };
5604
+ }
5605
+ const lines = [
5606
+ color.bold(`${open.length} open annotation(s) for ${sessionId}`),
5607
+ ""
5608
+ ];
5609
+ for (const a of open) {
5610
+ const t = color.dim(a.createdAt.slice(11, 19));
5611
+ const author = color.cyan(a.authorId.slice(0, 8));
5612
+ const idx = color.dim(`#${a.atEventIndex}`);
5613
+ lines.push(` ${t} ${idx} ${author} ${a.text}`);
5614
+ }
5615
+ return { message: lines.join("\n") };
5616
+ }
5617
+ function summarise(ev) {
5618
+ switch (ev["type"]) {
5619
+ case "user_input": {
5620
+ const text = typeof ev["text"] === "string" ? ev["text"] : "";
5621
+ return color.dim(truncate(text, 80));
5622
+ }
5623
+ case "llm_response": {
5624
+ const text = typeof ev["text"] === "string" ? ev["text"] : "";
5625
+ return color.dim(truncate(text, 80));
5626
+ }
5627
+ case "tool_result": {
5628
+ const name = String(ev["name"] ?? "?");
5629
+ const ok = ev["ok"] === false ? color.red("(failed)") : color.green("(ok)");
5630
+ return `${name} ${ok}`;
5631
+ }
5632
+ case "compaction": {
5633
+ const before = ev["before"];
5634
+ const after = ev["after"];
5635
+ return `tokens ${before}\u2192${after}`;
5636
+ }
5637
+ case "error": {
5638
+ return color.red(String(ev["message"] ?? "(no message)"));
5639
+ }
5640
+ default: {
5641
+ return color.dim(JSON.stringify(ev).slice(0, 80));
5642
+ }
5643
+ }
5644
+ }
5645
+ function truncate(s, n) {
5646
+ if (s.length <= n) return s;
5647
+ return `${s.slice(0, n - 1)}\u2026`;
5648
+ }
5649
+ function helpCommand() {
5650
+ const lines = [
5651
+ color.bold("/collab \u2014 live collaboration (read-only observers)"),
5652
+ "",
5653
+ "Subcommands:",
5654
+ ` ${color.cyan("status")} Show current session's collaboration status`,
5655
+ ` ${color.cyan("invite")} Print a webui join link for a teammate`,
5656
+ ` ${color.cyan("history [N]")} Show the last N events of this session (default 20, max 200)`,
5657
+ ` ${color.cyan("annotations")} List open (unresolved) annotations from webui annotators`,
5658
+ ` ${color.cyan("help")} Show this help`,
5659
+ "",
5660
+ color.dim(
5661
+ " The observer flow itself lives in the webui: open the URL printed"
5662
+ ),
5663
+ color.dim(' by `invite` and click "Join as observer" on the panel above the chat.')
5664
+ ];
5665
+ return { message: lines.join("\n") };
5666
+ }
5309
5667
 
5310
5668
  // src/slash-commands/worktree.ts
5311
5669
  function buildWorktreeCommand(opts) {
@@ -5524,6 +5882,7 @@ function buildBuiltinSlashCommands(opts) {
5524
5882
  buildAutoPhaseCommand(opts),
5525
5883
  buildWorktreeCommand(opts),
5526
5884
  buildSettingsCommand(opts),
5885
+ buildCollabCommand(opts),
5527
5886
  buildStatuslineCommand({
5528
5887
  cwd: opts.cwd,
5529
5888
  hiddenItems: opts.statuslineHiddenItems ?? [],
@@ -7848,9 +8207,9 @@ var updateCmd = async (args, deps) => {
7848
8207
  cwd,
7849
8208
  stdio: "pipe"
7850
8209
  });
7851
- let stderr = "";
8210
+ let _stderr = "";
7852
8211
  child.stderr?.on("data", (d) => {
7853
- stderr += d;
8212
+ _stderr += d;
7854
8213
  });
7855
8214
  child.on("error", reject);
7856
8215
  child.on("close", (code) => resolve4({ code: code ?? 0 }));
@@ -9084,6 +9443,167 @@ ${result.errors.length} error(s):
9084
9443
  return 1;
9085
9444
  }
9086
9445
  };
9446
+ var replayCmd = async (args, deps) => {
9447
+ const wpaths = resolveWstackPaths({
9448
+ projectRoot: deps.projectRoot,
9449
+ userHome: deps.userHome
9450
+ });
9451
+ const store = new ReplayLogStore({ dir: wpaths.projectSessions });
9452
+ if (args[0] === "--list" || args[0] === "-l") {
9453
+ const all = await store.list();
9454
+ if (all.length === 0) {
9455
+ deps.renderer.write(
9456
+ color.dim("No replay logs recorded yet. Run with --record to start one.") + "\n"
9457
+ );
9458
+ return 0;
9459
+ }
9460
+ const lines2 = [
9461
+ color.bold(`${all.length} replay log(s)`),
9462
+ color.dim(" Sorted by session id. Each entry has at least one recorded request/response."),
9463
+ ""
9464
+ ];
9465
+ for (const r of all) {
9466
+ lines2.push(
9467
+ ` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${color.dim(r.path)}`
9468
+ );
9469
+ }
9470
+ lines2.push("");
9471
+ lines2.push(
9472
+ color.dim(
9473
+ " Inspect: wstack replay <sessionId>"
9474
+ )
9475
+ );
9476
+ lines2.push(
9477
+ color.dim(
9478
+ " Replay: wstack --replay <sessionId>"
9479
+ )
9480
+ );
9481
+ deps.renderer.write(lines2.join("\n") + "\n");
9482
+ return 0;
9483
+ }
9484
+ const sessionId = args[0];
9485
+ if (!sessionId) {
9486
+ deps.renderer.writeError(
9487
+ "Usage: wstack replay <sessionId>\n wstack replay --list\n\nLists recorded provider responses for the session. To actually\nre-run the agent with frozen responses, use:\n wstack --replay " + color.cyan("<sessionId>") + "\n wstack --record # start a fresh recording\n"
9488
+ );
9489
+ return 1;
9490
+ }
9491
+ const entries = await store.load(sessionId);
9492
+ if (entries.length === 0) {
9493
+ deps.renderer.write(
9494
+ color.yellow(
9495
+ `No replay entries recorded for session ${sessionId}.
9496
+ Run a session with --record first, then --replay to re-execute it.`
9497
+ ) + "\n"
9498
+ );
9499
+ return 0;
9500
+ }
9501
+ const lines = [
9502
+ color.bold(`Replay log for ${sessionId}`),
9503
+ ` ${entries.length} recorded request/response pair(s)`,
9504
+ ` Log file: ${color.dim(`${sessionId}.replay.jsonl`)}`,
9505
+ "",
9506
+ "To replay this session deterministically:",
9507
+ ` ${color.cyan(`wstack --replay ${sessionId}`)}`,
9508
+ "",
9509
+ "Recorded timestamps (oldest first):"
9510
+ ];
9511
+ for (const e of entries.slice(-10)) {
9512
+ lines.push(
9513
+ ` ${color.dim(e.ts.slice(11, 19))} ${e.hash.slice(0, 16)}\u2026 ${e.request.model}`
9514
+ );
9515
+ }
9516
+ if (entries.length > 10) {
9517
+ lines.push(color.dim(` \u2026 and ${entries.length - 10} more`));
9518
+ }
9519
+ deps.renderer.write(lines.join("\n") + "\n");
9520
+ return 0;
9521
+ };
9522
+ var auditCmd = async (args, deps) => {
9523
+ const wpaths = resolveWstackPaths({
9524
+ projectRoot: deps.projectRoot,
9525
+ userHome: deps.userHome
9526
+ });
9527
+ const log = new ToolAuditLog({ dir: wpaths.projectSessions });
9528
+ if (args[0] === "--list" || args[0] === "-l") {
9529
+ return listAudits(log, wpaths.projectSessions, deps);
9530
+ }
9531
+ const sessionId = args[0];
9532
+ if (!sessionId) {
9533
+ deps.renderer.writeError(
9534
+ "Usage: wstack audit <sessionId>\n wstack audit --list\n\nInspects a session's tamper-evident tool audit log. Each entry\nis chained to the previous via SHA-256; any post-hoc modification\nbreaks the chain. Use this to verify a session was not tampered\nwith after recording.\n"
9535
+ );
9536
+ return 1;
9537
+ }
9538
+ const entries = await log.load(sessionId);
9539
+ if (entries.length === 0) {
9540
+ deps.renderer.write(
9541
+ color.yellow(
9542
+ `No audit entries for session ${sessionId}. (The audit log is written when tools are recorded \u2014 check that the session ran with audit enabled.)`
9543
+ ) + "\n"
9544
+ );
9545
+ return 0;
9546
+ }
9547
+ const verify = await log.verify(sessionId);
9548
+ const lines = [
9549
+ color.bold(`Audit log for ${sessionId}`),
9550
+ verify.ok ? ` ${color.green("\u2713 Verified")} ${entries.length} entries, chain intact` : ` ${color.red("\u2717 BROKEN")} at entry ${verify.brokenAt}: ${verify.reason}`,
9551
+ "",
9552
+ " Entries (oldest first):"
9553
+ ];
9554
+ const cap = 12;
9555
+ for (const e of entries.slice(0, cap)) {
9556
+ const t = color.dim(e.ts.slice(11, 19));
9557
+ const idx = color.dim(`#${e.index}`.padStart(4));
9558
+ const tool = e.toolName;
9559
+ const err = e.isError ? color.red("(error)") : "";
9560
+ lines.push(
9561
+ ` ${t} ${idx} ${color.cyan(tool.padEnd(14))} ${err} ${color.dim(e.hash.slice(0, 12) + "\u2026")}`
9562
+ );
9563
+ }
9564
+ if (entries.length > cap) {
9565
+ lines.push(color.dim(` \u2026 and ${entries.length - cap} more`));
9566
+ }
9567
+ deps.renderer.write(lines.join("\n") + "\n");
9568
+ return verify.ok ? 0 : 1;
9569
+ };
9570
+ async function listAudits(log, dir, deps) {
9571
+ const fs22 = await import('fs/promises');
9572
+ let entries;
9573
+ try {
9574
+ entries = await fs22.readdir(dir);
9575
+ } catch {
9576
+ deps.renderer.write(
9577
+ color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
9578
+ );
9579
+ return 0;
9580
+ }
9581
+ const out = [];
9582
+ for (const name of entries) {
9583
+ if (!name.endsWith(".audit.jsonl")) continue;
9584
+ const sessionId = name.slice(0, -".audit.jsonl".length);
9585
+ const all = await log.load(sessionId);
9586
+ const verify = await log.verify(sessionId);
9587
+ out.push({ sessionId, entryCount: all.length, ok: verify.ok });
9588
+ }
9589
+ out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
9590
+ if (out.length === 0) {
9591
+ deps.renderer.write(color.dim("No audit logs recorded yet.") + "\n");
9592
+ return 0;
9593
+ }
9594
+ const lines = [
9595
+ color.bold(`${out.length} audit log(s)`),
9596
+ ""
9597
+ ];
9598
+ for (const r of out) {
9599
+ const status = r.ok ? color.green("\u2713 intact") : color.red("\u2717 broken");
9600
+ lines.push(
9601
+ ` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${status}`
9602
+ );
9603
+ }
9604
+ deps.renderer.write(lines.join("\n") + "\n");
9605
+ return 0;
9606
+ }
9087
9607
  var toolsCmd = async (_args, deps) => {
9088
9608
  const reg = deps.toolRegistry;
9089
9609
  if (!reg) return 0;
@@ -9150,6 +9670,8 @@ var subcommands = {
9150
9670
  sessions: sessionsCmd,
9151
9671
  config: configCmd,
9152
9672
  rewind: rewindCmd,
9673
+ replay: replayCmd,
9674
+ audit: auditCmd,
9153
9675
  tools: toolsCmd,
9154
9676
  skills: skillsCmd,
9155
9677
  providers: providersCmd,
@@ -9186,27 +9708,35 @@ function fmtDuration(ms) {
9186
9708
  const remMin = m - h * 60;
9187
9709
  return `${h}h${remMin}m`;
9188
9710
  }
9189
- function fmtTaskResultLine(r, color37) {
9711
+ function fmtTaskResultLine(r, color41) {
9190
9712
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
9191
9713
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
9192
9714
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
9193
9715
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
9194
- const errKindChip = errKind ? color37.dim(` [${errKind}]`) : "";
9195
- const errSnip = errMsg || errKind ? `${errKindChip}${color37.dim(errTail)}` : "";
9716
+ const errKindChip = errKind ? color41.dim(` [${errKind}]`) : "";
9717
+ const errSnip = errMsg || errKind ? `${errKindChip}${color41.dim(errTail)}` : "";
9196
9718
  switch (r.status) {
9197
9719
  case "success":
9198
- return { mark: color37.green("\u2713"), stats, tail: "" };
9720
+ return { mark: color41.green("\u2713"), stats, tail: "" };
9199
9721
  case "timeout":
9200
- return { mark: color37.yellow("\u23F1"), stats: `${color37.yellow("timeout")} ${stats}`, tail: errSnip };
9722
+ return { mark: color41.yellow("\u23F1"), stats: `${color41.yellow("timeout")} ${stats}`, tail: errSnip };
9201
9723
  case "stopped":
9202
- return { mark: color37.dim("\u2298"), stats: `${color37.dim("stopped")} ${stats}`, tail: errSnip };
9724
+ return { mark: color41.dim("\u2298"), stats: `${color41.dim("stopped")} ${stats}`, tail: errSnip };
9203
9725
  case "failed":
9204
- return { mark: color37.red("\u2717"), stats: `${color37.red("failed")} ${stats}`, tail: errSnip };
9726
+ return { mark: color41.red("\u2717"), stats: `${color41.red("failed")} ${stats}`, tail: errSnip };
9205
9727
  }
9206
9728
  }
9207
9729
 
9208
9730
  // src/boot.ts
9209
9731
  init_update_check();
9732
+ var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
9733
+ function resolveBundledOverlayFile() {
9734
+ try {
9735
+ return fileURLToPath(new URL("../data/providers.json", import.meta.url));
9736
+ } catch {
9737
+ return void 0;
9738
+ }
9739
+ }
9210
9740
  function resolveBundledSkillsDir() {
9211
9741
  try {
9212
9742
  const req2 = createRequire(import.meta.url);
@@ -9222,6 +9752,10 @@ async function boot(argv) {
9222
9752
  flags["resume"] = positional[1];
9223
9753
  positional.splice(0, 2);
9224
9754
  }
9755
+ if (positional.length === 0) {
9756
+ if (flags["help"] === true) positional.push("help");
9757
+ else if (flags["version"] === true) positional.push("version");
9758
+ }
9225
9759
  let bootResult;
9226
9760
  try {
9227
9761
  bootResult = await bootConfig(flags);
@@ -9238,7 +9772,12 @@ async function boot(argv) {
9238
9772
  const reader = new ReadlineInputReader({ historyFile: wpaths.historyFile });
9239
9773
  const modelsRegistry = new DefaultModelsRegistry({
9240
9774
  cacheFile: wpaths.modelsCache,
9241
- ttlSeconds: 24 * 3600
9775
+ ttlSeconds: 24 * 3600,
9776
+ // Curated overlay merged on top of models.dev: fetched from GitHub raw for
9777
+ // freshness, with the bundled file as the offline floor.
9778
+ overlayUrl: GITHUB_PROVIDERS_OVERLAY_URL,
9779
+ overlayFile: resolveBundledOverlayFile(),
9780
+ overlayCacheFile: wpaths.modelsOverlayCache
9242
9781
  });
9243
9782
  let updateInfo;
9244
9783
  if (!flags["no-check"] && !process.env["WRONGSTACK_NO_CHECK"]) {
@@ -9691,7 +10230,7 @@ async function runRepl(opts) {
9691
10230
  );
9692
10231
  interrupts = 0;
9693
10232
  try {
9694
- const ok = await engine.runOneIteration();
10233
+ await engine.runOneIteration();
9695
10234
  const afterGoal = await loadGoalSafe(opts);
9696
10235
  const last = afterGoal?.journal[afterGoal.journal.length - 1];
9697
10236
  if (coord) {
@@ -9805,7 +10344,7 @@ ${color.dim(taskList2)}
9805
10344
  }
9806
10345
  }
9807
10346
  }
9808
- } catch (runErr) {
10347
+ } catch (_runErr) {
9809
10348
  opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
9810
10349
  }
9811
10350
  }
@@ -10391,9 +10930,15 @@ async function execute(deps) {
10391
10930
  // Default OFF so the terminal's native scrollback works for chat
10392
10931
  // history out of the box (mouse wheel / Shift+PgUp). Users who hit
10393
10932
  // resize/overlay-leak artifacts can opt back into alt-screen with
10394
- // `--alt-screen` or `/altscreen on`. `--no-alt-screen` still wins
10395
- // when both are passed.
10933
+ // `--alt-screen`. `--no-alt-screen` still wins when both are passed.
10396
10934
  altScreen: flags["alt-screen"] === true && flags["no-alt-screen"] !== true,
10935
+ // Full mouse mode: clickable pickers + in-app wheel scroll. Opt-in
10936
+ // via --mouse. The TUI combines altScreen || mouse to determine
10937
+ // whether to use the managed viewport (ScrollableHistory + in-app
10938
+ // scroll); --mouse alone enables that without requiring --alt-screen.
10939
+ // Mouse mode takes ownership of the terminal's mouse, suspending
10940
+ // native scroll/copy until `/mouse off`.
10941
+ mouse: flags.mouse === true,
10397
10942
  director,
10398
10943
  fleetRoster,
10399
10944
  onAfterExit: () => {
@@ -10505,7 +11050,7 @@ async function execute(deps) {
10505
11050
  fleetStatusLine?.stop();
10506
11051
  try {
10507
11052
  stats.render(renderer);
10508
- } catch (err) {
11053
+ } catch (_err) {
10509
11054
  }
10510
11055
  await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
10511
11056
  await mcpRegistry.stopAll();
@@ -10535,6 +11080,10 @@ var MultiAgentHost = class {
10535
11080
  * critic.evaluation) onto the fleet bus without needing the tool registered
10536
11081
  * in the host's ToolRegistry. */
10537
11082
  fleetEmitTool;
11083
+ /** Own FleetStatusTool — created in buildDirector() so subagents in director
11084
+ * mode can read fleet state (subagent statuses, task progress) without the
11085
+ * tool being in the host's ToolRegistry. */
11086
+ fleetStatusTool;
10538
11087
  /** Lazily built alongside the director — produces per-subagent JSONL
10539
11088
  * writers under `<sessionsRoot>/<runId>/`. Null without sessionsRoot. */
10540
11089
  sessionFactory;
@@ -10571,7 +11120,7 @@ var MultiAgentHost = class {
10571
11120
  getDirector() {
10572
11121
  return this.director;
10573
11122
  }
10574
- async ensureCoordinator(config) {
11123
+ async ensureCoordinator(_config) {
10575
11124
  await this.buildDirector();
10576
11125
  }
10577
11126
  async buildDirector() {
@@ -10638,6 +11187,15 @@ var MultiAgentHost = class {
10638
11187
  totalExtensions: payload.totalExtensions
10639
11188
  });
10640
11189
  });
11190
+ this.director.fleet.filter("ctx.pct", (e) => {
11191
+ const payload = e.payload;
11192
+ this.deps.events.emit("subagent.ctx_pct", {
11193
+ subagentId: e.subagentId,
11194
+ load: payload.load,
11195
+ tokens: payload.tokens,
11196
+ maxContext: payload.maxContext
11197
+ });
11198
+ });
10641
11199
  this.getCoordinator().on(
10642
11200
  "task.assigned",
10643
11201
  ({
@@ -10652,6 +11210,7 @@ var MultiAgentHost = class {
10652
11210
  }
10653
11211
  );
10654
11212
  this.fleetEmitTool = makeFleetEmitTool(this.director);
11213
+ this.fleetStatusTool = makeFleetStatusTool(this.director);
10655
11214
  const runner = await this.buildSubagentRunner(config);
10656
11215
  this.getCoordinator().setRunner(runner);
10657
11216
  }
@@ -10674,7 +11233,11 @@ var MultiAgentHost = class {
10674
11233
  makeSubagentFactory(config) {
10675
11234
  return async (subCfg) => {
10676
11235
  const events = new EventBus();
10677
- const provider = await this.buildSubagentProvider(config, subCfg.provider);
11236
+ const provider = await this.buildSubagentProvider(
11237
+ config,
11238
+ subCfg.provider,
11239
+ subCfg.model ?? config.model
11240
+ );
10678
11241
  const subCwd = subCfg.cwd ?? this.deps.cwd;
10679
11242
  const baseSystem = await this.deps.systemPromptBuilder.build({
10680
11243
  cwd: subCwd,
@@ -10709,11 +11272,17 @@ var MultiAgentHost = class {
10709
11272
  }
10710
11273
  const tools = subCfg.tools ? [...subCfg.tools] : void 0;
10711
11274
  let injectedFleetEmit;
10712
- if (tools?.includes("fleet_emit")) {
10713
- const fleetTool = this.fleetEmitTool;
10714
- if (fleetTool) {
10715
- tools.splice(tools.indexOf("fleet_emit"), 1);
10716
- injectedFleetEmit = fleetTool;
11275
+ let injectedFleetStatus;
11276
+ if (tools) {
11277
+ const emitIdx = tools.indexOf("fleet_emit");
11278
+ if (emitIdx >= 0 && this.fleetEmitTool) {
11279
+ tools.splice(emitIdx, 1);
11280
+ injectedFleetEmit = this.fleetEmitTool;
11281
+ }
11282
+ const statusIdx = tools.indexOf("fleet_status");
11283
+ if (statusIdx >= 0 && this.fleetStatusTool) {
11284
+ tools.splice(statusIdx, 1);
11285
+ injectedFleetStatus = this.fleetStatusTool;
10717
11286
  }
10718
11287
  }
10719
11288
  const ctx = new Context({
@@ -10729,6 +11298,7 @@ var MultiAgentHost = class {
10729
11298
  });
10730
11299
  const baseRegistry = this.subagentToolRegistry(tools);
10731
11300
  if (injectedFleetEmit) baseRegistry.register(injectedFleetEmit);
11301
+ if (injectedFleetStatus) baseRegistry.register(injectedFleetStatus);
10732
11302
  const toolExecutor = new ToolExecutor(baseRegistry, {
10733
11303
  permissionPolicy: new AutoApprovePermissionPolicy(),
10734
11304
  secretScrubber: this.deps.secretScrubber,
@@ -10805,17 +11375,28 @@ var MultiAgentHost = class {
10805
11375
  * not configured (so a typo doesn't crash the whole run — we just
10806
11376
  * use the leader and the calling code can decide to error later).
10807
11377
  */
10808
- async buildSubagentProvider(config, overrideId) {
11378
+ async buildSubagentProvider(config, overrideId, model) {
10809
11379
  const providerId = overrideId && config.providers?.[overrideId] ? overrideId : config.provider;
10810
11380
  const newCfg = config.providers?.[providerId] ?? {
10811
11381
  type: providerId,
10812
11382
  apiKey: config.apiKey,
10813
11383
  baseUrl: config.baseUrl
10814
11384
  };
10815
- return makeProviderFromConfig(providerId, {
11385
+ const provider = makeProviderFromConfig(providerId, {
10816
11386
  ...newCfg,
10817
11387
  type: providerId
10818
11388
  });
11389
+ if (this.deps.modelsRegistry) {
11390
+ const resolvedModel = model ?? config.model;
11391
+ const caps = await capabilitiesFor(
11392
+ this.deps.modelsRegistry,
11393
+ providerId,
11394
+ resolvedModel
11395
+ ).catch(() => void 0);
11396
+ const mc = caps?.maxContext ?? config.context?.effectiveMaxContext ?? provider.capabilities.maxContext;
11397
+ if (mc && mc > 0) provider.capabilities.maxContext = mc;
11398
+ }
11399
+ return provider;
10819
11400
  }
10820
11401
  async spawnACP(subagentId, task, config) {
10821
11402
  const taskId = randomUUID();
@@ -11140,9 +11721,9 @@ function makeConfirmAwaiter(reader) {
11140
11721
  function stringifyInput(input) {
11141
11722
  if (!input || typeof input !== "object") return "";
11142
11723
  const obj = input;
11143
- return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
11724
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate2(JSON.stringify(v), 80)}`).join(" ");
11144
11725
  }
11145
- function truncate(s, max) {
11726
+ function truncate2(s, max) {
11146
11727
  return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
11147
11728
  }
11148
11729
  function hasDiff(input) {
@@ -11150,6 +11731,26 @@ function hasDiff(input) {
11150
11731
  input && typeof input === "object" && "diff" in input
11151
11732
  );
11152
11733
  }
11734
+ function bindReplayToContainer(opts) {
11735
+ const { container, wpaths, sessionId, mode, logger } = opts;
11736
+ if (!opts.container.has(TOKENS.ProviderRunner)) {
11737
+ container.bind(TOKENS.ProviderRunner, () => ({
11738
+ run: (o) => runProviderWithRetry(o)
11739
+ }));
11740
+ }
11741
+ const inner = container.resolve(TOKENS.ProviderRunner);
11742
+ const log = new ReplayLogStore({ dir: wpaths.projectSessions });
11743
+ const wrapped = new ReplayProviderRunner(inner, {
11744
+ log,
11745
+ sessionId,
11746
+ mode,
11747
+ logger: logger ? {
11748
+ debug: (m) => logger.debug?.(m),
11749
+ warn: (m) => logger.warn?.(m)
11750
+ } : void 0
11751
+ });
11752
+ container.bind(TOKENS.ProviderRunner, () => wrapped);
11753
+ }
11153
11754
  var SessionStats = class {
11154
11755
  tokenCounter;
11155
11756
  startedAt = Date.now();
@@ -11577,6 +12178,13 @@ function renderProgress3(ratio, width) {
11577
12178
  const capped = Math.min(width, filled);
11578
12179
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
11579
12180
  }
12181
+ var createSessionEventBridge = (_writer, level) => ({
12182
+ append: async (_e) => {
12183
+ },
12184
+ level: level ?? "standard",
12185
+ allows: () => true
12186
+ });
12187
+ var resolveAuditLevel = (cfg) => cfg?.session?.auditLevel ?? "standard";
11580
12188
  function setupPipelines(params) {
11581
12189
  const { events, logger } = params;
11582
12190
  const pipelines = createDefaultPipelines();
@@ -11603,11 +12211,13 @@ function setupPipelines(params) {
11603
12211
  return pipelines;
11604
12212
  }
11605
12213
  async function setupCompaction(params) {
11606
- const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
12214
+ const { compactor, events, modelsRegistry, context, config, provider, pipelines, fullConfig, sessionWriter, sessionBridge: providedBridge } = params;
11607
12215
  const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
11608
12216
  const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
11609
12217
  let autoCompactor;
11610
12218
  if (config.context.autoCompact !== false) {
12219
+ const auditLevel = resolveAuditLevel(fullConfig ?? config);
12220
+ const sessionBridge = providedBridge ?? createSessionEventBridge(sessionWriter, auditLevel);
11611
12221
  autoCompactor = new AutoCompactionMiddleware(
11612
12222
  compactor,
11613
12223
  effectiveMaxContext,
@@ -11619,7 +12229,12 @@ async function setupCompaction(params) {
11619
12229
  soft: config.context.softThreshold,
11620
12230
  hard: config.context.hardThreshold
11621
12231
  },
11622
- { aggressiveOn: "soft", failureMode: "throw_on_hard", events }
12232
+ {
12233
+ aggressiveOn: "soft",
12234
+ failureMode: "throw_on_hard",
12235
+ events,
12236
+ sessionBridge
12237
+ }
11623
12238
  );
11624
12239
  pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
11625
12240
  }
@@ -11628,7 +12243,6 @@ async function setupCompaction(params) {
11628
12243
  function createAgent(params) {
11629
12244
  const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
11630
12245
  const renderer = params.container.has(TOKENS.Renderer) ? params.container.resolve(TOKENS.Renderer) : void 0;
11631
- params.container.resolve(TOKENS.Logger);
11632
12246
  const toolExecutor = new ToolExecutor(params.tools, {
11633
12247
  permissionPolicy: params.permissionPolicy ?? params.container.resolve(TOKENS.PermissionPolicy),
11634
12248
  secretScrubber,
@@ -11993,6 +12607,21 @@ async function setupSession(params) {
11993
12607
  }
11994
12608
 
11995
12609
  // src/index.ts
12610
+ var createSessionEventBridge2 = (_writer, level, _opts) => ({
12611
+ append: async (_e) => {
12612
+ },
12613
+ level: level ?? "standard",
12614
+ allows: () => true
12615
+ });
12616
+ var resolveAuditLevel2 = (cfg) => cfg?.session?.auditLevel ?? "standard";
12617
+ var resolveSessionLoggingConfig = (cfg) => ({
12618
+ auditLevel: resolveAuditLevel2(cfg),
12619
+ sampling: {
12620
+ toolProgress: {
12621
+ sampleRate: cfg?.session?.sampling?.toolProgress?.sampleRate ?? 8
12622
+ }
12623
+ }
12624
+ });
11996
12625
  function resolveBundledSkillsDir2() {
11997
12626
  try {
11998
12627
  const req2 = createRequire(import.meta.url);
@@ -12012,7 +12641,6 @@ async function main(argv) {
12012
12641
  wpaths,
12013
12642
  cwd,
12014
12643
  projectRoot,
12015
- userHome,
12016
12644
  flags,
12017
12645
  positional,
12018
12646
  modelsRegistry,
@@ -12054,6 +12682,22 @@ async function main(argv) {
12054
12682
  compactor: { preserveK: config.context.preserveK, eliseThreshold: config.context.eliseThreshold },
12055
12683
  bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0
12056
12684
  });
12685
+ const replayFlag = flags["replay"];
12686
+ const recordFlag = flags["record"];
12687
+ if (typeof replayFlag === "string" || recordFlag === true) {
12688
+ const sessionId = typeof replayFlag === "string" ? replayFlag : `record-${Date.now()}`;
12689
+ const mode = recordFlag === true ? "record" : "replay";
12690
+ bindReplayToContainer({
12691
+ container,
12692
+ wpaths,
12693
+ sessionId,
12694
+ mode,
12695
+ logger
12696
+ });
12697
+ logger.info(
12698
+ `replay: ProviderRunner bound in '${mode}' mode for session ${sessionId}`
12699
+ );
12700
+ }
12057
12701
  const configStore = container.resolve(TOKENS.ConfigStore);
12058
12702
  container.bind(TOKENS.PathResolver, () => pathResolver);
12059
12703
  container.bind(TOKENS.Renderer, () => renderer);
@@ -12121,7 +12765,7 @@ async function main(argv) {
12121
12765
  }
12122
12766
  const events = new EventBus();
12123
12767
  events.setLogger(logger);
12124
- const { metricsSink, healthRegistry, metricsServerHandle } = (() => {
12768
+ const { metricsSink, healthRegistry } = (() => {
12125
12769
  const ms = setupMetrics({ flags, wpaths, events, logger, config: { provider: config.provider, model: config.model } });
12126
12770
  return ms;
12127
12771
  })();
@@ -12200,7 +12844,6 @@ async function main(argv) {
12200
12844
  });
12201
12845
  const session = sessResult.session;
12202
12846
  sessionRef.current = session;
12203
- sessResult.restoredMessages;
12204
12847
  const context = sessResult.context;
12205
12848
  const attachments = sessResult.attachments;
12206
12849
  const recoveryLock = sessResult.recoveryLock;
@@ -12208,18 +12851,99 @@ async function main(argv) {
12208
12851
  const planPath = sessResult.planPath;
12209
12852
  const detachTodosCheckpoint = sessResult.detachTodosCheckpoint;
12210
12853
  const priorFleetState = sessResult.priorFleetState;
12854
+ const sessionConfig = resolveSessionLoggingConfig(config);
12855
+ const sessionBridge = createSessionEventBridge2(
12856
+ session,
12857
+ sessionConfig.auditLevel);
12211
12858
  const stats = new SessionStats(events, tokenCounter);
12212
12859
  const errorRing = [];
12213
12860
  events.on("error", (e) => {
12214
12861
  const err = e.err;
12215
12862
  const code = err && typeof err === "object" && "code" in err && typeof err.code === "string" ? err.code : "UNKNOWN";
12216
12863
  const message = e.err instanceof Error ? e.err.message : String(e.err);
12217
- errorRing.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), phase: e.phase, code, message });
12864
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
12865
+ errorRing.push({ ts, phase: e.phase, code, message });
12218
12866
  if (errorRing.length > 5) errorRing.shift();
12867
+ sessionBridge.append({
12868
+ type: "error",
12869
+ ts,
12870
+ message,
12871
+ phase: e.phase
12872
+ }).catch(() => {
12873
+ });
12874
+ });
12875
+ events.on("tool.started", (e) => {
12876
+ sessionBridge.append({
12877
+ type: "tool_call_start",
12878
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12879
+ name: e.name,
12880
+ id: e.id,
12881
+ input: e.input
12882
+ }).catch(() => {
12883
+ });
12884
+ });
12885
+ events.on("tool.executed", (e) => {
12886
+ sessionBridge.append({
12887
+ type: "tool_call_end",
12888
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12889
+ name: e.name,
12890
+ id: e.id ?? "",
12891
+ durationMs: e.durationMs,
12892
+ outputSize: e.outputBytes ?? 0,
12893
+ ok: e.ok,
12894
+ outputBytes: e.outputBytes,
12895
+ outputTokens: e.outputTokens,
12896
+ outputLines: e.outputLines
12897
+ }).catch(() => {
12898
+ });
12899
+ });
12900
+ events.on("tool.progress", (e) => {
12901
+ sessionBridge.append({
12902
+ type: "tool_progress",
12903
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12904
+ name: e.name,
12905
+ id: e.id,
12906
+ event: { type: e.event.type, text: e.event.text, data: e.event.data }
12907
+ }).catch(() => {
12908
+ });
12909
+ });
12910
+ events.on("provider.retry", (e) => {
12911
+ sessionBridge.append({
12912
+ type: "provider_retry",
12913
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12914
+ providerId: e.providerId,
12915
+ attempt: e.attempt,
12916
+ delayMs: e.delayMs,
12917
+ status: e.status,
12918
+ description: e.description
12919
+ }).catch(() => {
12920
+ });
12921
+ });
12922
+ events.on("provider.error", (e) => {
12923
+ sessionBridge.append({
12924
+ type: "provider_error",
12925
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12926
+ providerId: e.providerId,
12927
+ status: e.status,
12928
+ description: e.description,
12929
+ retryable: e.retryable
12930
+ }).catch(() => {
12931
+ });
12219
12932
  });
12220
12933
  const pipelines = setupPipelines({ events, logger });
12221
12934
  const compactor = container.resolve(TOKENS.Compactor);
12222
- const { effectiveMaxContext, autoCompactor } = await setupCompaction({ compactor, events, modelsRegistry, context, config, provider, pipelines });
12935
+ const { effectiveMaxContext, autoCompactor } = await setupCompaction({
12936
+ compactor,
12937
+ events,
12938
+ modelsRegistry,
12939
+ context,
12940
+ config,
12941
+ provider,
12942
+ pipelines,
12943
+ fullConfig: config,
12944
+ sessionBridge
12945
+ // share the same bridge for consistent audit logging (compaction + errors + future)
12946
+ });
12223
12947
  const refreshMaxContext = async (providerId, modelId) => {
12224
12948
  if (!autoCompactor) return;
12225
12949
  const cap = await capabilitiesFor(modelsRegistry, providerId, modelId).catch(() => void 0);
@@ -12316,6 +13040,7 @@ async function main(argv) {
12316
13040
  toolRegistry,
12317
13041
  providerRegistry,
12318
13042
  configStore,
13043
+ modelsRegistry,
12319
13044
  events,
12320
13045
  systemPromptBuilder: promptBuilder,
12321
13046
  session,