@wrongstack/cli 0.9.7 → 0.9.20

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, 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';
@@ -380,12 +380,9 @@ function buildSddCommand(opts) {
380
380
  name: "sdd",
381
381
  description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
382
382
  async run(args) {
383
- opts.context;
384
383
  if (!opts.paths) return { message: "SDD not available \u2014 paths not configured." };
385
384
  const specsDir = opts.paths.projectSpecs;
386
- const graphsDir = opts.paths.projectTaskGraphs;
387
385
  const specStore = new SpecStore({ baseDir: specsDir });
388
- new TaskGraphStore({ baseDir: graphsDir });
389
386
  const versioning = sddState.getVersioning();
390
387
  const [verb, ...rest] = args.trim().split(/\s+/);
391
388
  const restJoined = rest.join(" ").trim();
@@ -1024,8 +1021,8 @@ Start executing the tasks one by one.`
1024
1021
  return { message: lines2.join("\n") };
1025
1022
  }
1026
1023
  try {
1027
- const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1028
- const stored = await graphStore2.load(graphId);
1024
+ const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1025
+ const stored = await graphStore.load(graphId);
1029
1026
  if (stored) {
1030
1027
  return { message: renderTaskGraph(stored, { compact: false }) };
1031
1028
  }
@@ -1245,8 +1242,8 @@ ${lines.join("\n")}`
1245
1242
  return { message: "No task graph found. Generate tasks first." };
1246
1243
  }
1247
1244
  try {
1248
- const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1249
- const graph = await graphStore2.load(graphId);
1245
+ const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1246
+ const graph = await graphStore.load(graphId);
1250
1247
  if (!graph) {
1251
1248
  return { message: "Could not load task graph." };
1252
1249
  }
@@ -1659,7 +1656,7 @@ async function checkForUpdate(signal, homeFn) {
1659
1656
  outdated: isNewer(latest, current),
1660
1657
  checkFailed: false
1661
1658
  };
1662
- } catch (err) {
1659
+ } catch (_err) {
1663
1660
  if (aborted()) {
1664
1661
  return { current, latest: current, outdated: false, checkFailed: true };
1665
1662
  }
@@ -1980,7 +1977,7 @@ async function runWebUI(opts) {
1980
1977
  }
1981
1978
  }
1982
1979
  }
1983
- async function handleUserMessage(ws, client, content) {
1980
+ async function handleUserMessage(ws, _client, content) {
1984
1981
  if (abortController) {
1985
1982
  send(ws, {
1986
1983
  type: "error",
@@ -3553,8 +3550,85 @@ function buildLoadCommand(opts) {
3553
3550
  return {
3554
3551
  name: "resume",
3555
3552
  aliases: ["load", "sessions"],
3556
- description: "List recent sessions.",
3557
- async run() {
3553
+ description: "List recent sessions, show incomplete ones (--incomplete), or plan a recovery (--recover <id>).",
3554
+ async run(args) {
3555
+ const parts = args.split(/\s+/).filter(Boolean);
3556
+ const showIncomplete = parts.includes("--incomplete") || parts.includes("-i");
3557
+ const recoverIdx = parts.findIndex((p) => p === "--recover");
3558
+ const recoverTarget = recoverIdx >= 0 ? parts[recoverIdx + 1] : void 0;
3559
+ if (recoverTarget) {
3560
+ if (!opts.paths) {
3561
+ return { message: color.yellow("No paths configured \u2014 cannot build a recovery plan.") };
3562
+ }
3563
+ const recovery = new SessionRecovery(opts.paths.projectSessions);
3564
+ const plan = await recovery.recover(recoverTarget);
3565
+ if (!plan) {
3566
+ return {
3567
+ message: color.yellow(
3568
+ `No session log found for ${recoverTarget} (or it is empty).`
3569
+ )
3570
+ };
3571
+ }
3572
+ const lines2 = [
3573
+ color.bold(`Recovery plan for ${plan.sessionId}`),
3574
+ ` Stale: ${plan.stale ? color.yellow("yes") : color.green("no")}`
3575
+ ];
3576
+ if (plan.context) {
3577
+ lines2.push(` Last in-flight context: ${color.cyan(plan.context)}`);
3578
+ }
3579
+ if (plan.lastCheckpoint && plan.lastCheckpoint.type === "checkpoint") {
3580
+ const cp = plan.lastCheckpoint;
3581
+ lines2.push(
3582
+ ` Last checkpoint: promptIndex=${cp.promptIndex} preview=${color.dim(`"${cp.promptPreview}"`)} at ${color.dim(cp.ts)}`
3583
+ );
3584
+ } else {
3585
+ lines2.push(` Last checkpoint: ${color.dim("(none \u2014 full re-execution)")}`);
3586
+ }
3587
+ lines2.push(
3588
+ ` Pending events: ${plan.pendingEvents.length} (the work that would re-run on resume)`
3589
+ );
3590
+ if (plan.pendingEvents.length > 0) {
3591
+ const summary = summarizePending(plan.pendingEvents);
3592
+ lines2.push(...summary);
3593
+ }
3594
+ lines2.push("");
3595
+ lines2.push(
3596
+ color.dim(
3597
+ 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."
3598
+ )
3599
+ );
3600
+ return { message: lines2.join("\n") };
3601
+ }
3602
+ if (showIncomplete) {
3603
+ if (!opts.paths) {
3604
+ return { message: color.yellow("No paths configured \u2014 cannot scan for incomplete sessions.") };
3605
+ }
3606
+ const recovery = new SessionRecovery(opts.paths.projectSessions);
3607
+ const resumable = await recovery.listResumable();
3608
+ if (resumable.length === 0) {
3609
+ return {
3610
+ message: color.dim("No incomplete sessions. (Every recorded run ended cleanly.)")
3611
+ };
3612
+ }
3613
+ const lines2 = [
3614
+ color.bold(`${resumable.length} incomplete session(s)`),
3615
+ color.dim(" (process died mid-iteration; the last event was in_flight_start)"),
3616
+ ""
3617
+ ];
3618
+ for (const s of resumable) {
3619
+ const t = color.dim(s.lastEventTs.slice(0, 19).replace("T", " "));
3620
+ lines2.push(
3621
+ ` ${color.cyan(s.sessionId)} ${t} ${color.dim(`${s.eventCount} events`)} ${s.context}`
3622
+ );
3623
+ }
3624
+ lines2.push("");
3625
+ lines2.push(
3626
+ color.dim(
3627
+ " 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."
3628
+ )
3629
+ );
3630
+ return { message: lines2.join("\n") };
3631
+ }
3558
3632
  if (!opts.sessionStore) return { message: "No session store configured." };
3559
3633
  const list = await opts.sessionStore.list(10);
3560
3634
  if (list.length === 0) return { message: "No saved sessions." };
@@ -3565,7 +3639,8 @@ function buildLoadCommand(opts) {
3565
3639
  ${lines.join("\n")}
3566
3640
 
3567
3641
  ${color.dim(`Resume one with: wstack resume ${list[0]?.id ?? "<id>"}
3568
- `)}`;
3642
+ `)}
3643
+ ${color.dim("Tip: /resume --incomplete \u2014 list sessions that crashed mid-iteration")}`;
3569
3644
  opts.renderer.write(msg);
3570
3645
  return { message: msg };
3571
3646
  }
@@ -3589,6 +3664,47 @@ function buildExitCommand(opts) {
3589
3664
  }
3590
3665
  };
3591
3666
  }
3667
+ function summarizePending(events) {
3668
+ const lines = [];
3669
+ const cap = 12;
3670
+ for (const ev of events.slice(-cap)) {
3671
+ const t = "ts" in ev ? color.dim(String(ev.ts).slice(11, 19)) : color.dim("--:--:--");
3672
+ const kind = color.cyan(String(ev.type).padEnd(18));
3673
+ lines.push(` ${t} ${kind} ${summariseEvent(ev)}`);
3674
+ }
3675
+ if (events.length > cap) {
3676
+ lines.push(color.dim(` \u2026 and ${events.length - cap} more`));
3677
+ }
3678
+ return lines;
3679
+ }
3680
+ function summariseEvent(ev) {
3681
+ switch (ev.type) {
3682
+ case "user_input": {
3683
+ const text = "text" in ev && typeof ev.text === "string" ? ev.text : Array.isArray(ev.content) ? "\u2026" : "";
3684
+ return color.dim(text.length > 60 ? text.slice(0, 59) + "\u2026" : text);
3685
+ }
3686
+ case "llm_response":
3687
+ return color.dim("(model reply)");
3688
+ case "tool_use":
3689
+ return color.dim(`name=${ev.name ?? "?"}`);
3690
+ case "tool_result":
3691
+ return color.dim(`name=${ev.name ?? "?"}`);
3692
+ case "in_flight_start":
3693
+ return color.dim(`context="${ev.context ?? ""}"`);
3694
+ case "in_flight_end":
3695
+ return color.dim(`reason=${ev.reason ?? "?"}`);
3696
+ case "checkpoint":
3697
+ return color.dim(
3698
+ `promptIndex=${ev.promptIndex ?? "?"}`
3699
+ );
3700
+ case "compaction":
3701
+ return color.dim("(compaction)");
3702
+ case "error":
3703
+ return color.red(String(ev.message ?? ""));
3704
+ default:
3705
+ return color.dim("\u2026");
3706
+ }
3707
+ }
3592
3708
 
3593
3709
  // src/slash-commands/spawn-agents.ts
3594
3710
  function buildSpawnCommand(opts) {
@@ -4173,8 +4289,48 @@ ${lines.join("\n")}`;
4173
4289
  }
4174
4290
  };
4175
4291
  }
4176
-
4177
- // src/slash-commands/mode.ts
4292
+ async function runModePicker(modeStore, reader) {
4293
+ const modes = await modeStore.listModes();
4294
+ const active = await modeStore.getActiveMode();
4295
+ const activeIdx = modes.findIndex((m) => m.id === active?.id);
4296
+ let cursor = activeIdx >= 0 ? activeIdx : 0;
4297
+ const render = (currentCursor) => {
4298
+ const lines = [];
4299
+ lines.push(`
4300
+ ${color.bold(color.amber("WrongStack") + color.dim(" \u2014 Mode Selection"))}
4301
+ `);
4302
+ lines.push(color.dim(" \u2191\u2193 navigate Enter select q quit\n"));
4303
+ lines.push("");
4304
+ for (let i = 0; i < modes.length; i++) {
4305
+ const m = modes[i];
4306
+ const mark = m.id === active?.id ? color.green(" [active]") : "";
4307
+ const prefix = i === currentCursor ? color.bold("\u276F ") : " ";
4308
+ const name = i === currentCursor ? color.bold(m.name) : m.name;
4309
+ lines.push(` ${prefix}${name} ${color.dim(m.description)}${mark}`);
4310
+ }
4311
+ lines.push("");
4312
+ return lines.join("\n");
4313
+ };
4314
+ process.stdout.write(render(cursor));
4315
+ const options = [
4316
+ { key: "\x1B[A", label: "\u2191", value: "up" },
4317
+ { key: "\x1B[B", label: "\u2193", value: "down" },
4318
+ { key: "\r", label: "Enter", value: "enter" },
4319
+ { key: "q", label: "q", value: "quit" }
4320
+ ];
4321
+ while (true) {
4322
+ const answer = await reader.readKey("", options);
4323
+ if (answer === "quit") return void 0;
4324
+ if (answer === "enter") return modes[cursor];
4325
+ if (answer === "up") {
4326
+ cursor = cursor > 0 ? cursor - 1 : modes.length - 1;
4327
+ } else if (answer === "down") {
4328
+ cursor = cursor < modes.length - 1 ? cursor + 1 : 0;
4329
+ }
4330
+ process.stdout.write("\x1B[J");
4331
+ process.stdout.write(render(cursor));
4332
+ }
4333
+ }
4178
4334
  function buildModeCommand(opts) {
4179
4335
  return {
4180
4336
  name: "mode",
@@ -4194,7 +4350,7 @@ function buildModeCommand(opts) {
4194
4350
  " /mode brief Switch to brief mode",
4195
4351
  " /mode teach Switch to teach mode"
4196
4352
  ].join("\n"),
4197
- async run(args) {
4353
+ async run(args, _ctx) {
4198
4354
  const modeStore = opts.modeStore;
4199
4355
  if (!modeStore) {
4200
4356
  return { message: "Mode store not available in this context." };
@@ -4202,6 +4358,17 @@ function buildModeCommand(opts) {
4202
4358
  const modes = await modeStore.listModes();
4203
4359
  const active = await modeStore.getActiveMode();
4204
4360
  if (!args.trim()) {
4361
+ if (opts.inputReader) {
4362
+ const selected = await runModePicker(modeStore, opts.inputReader);
4363
+ if (!selected) {
4364
+ return { message: "Mode selection cancelled." };
4365
+ }
4366
+ await modeStore.setActiveMode(selected.id);
4367
+ return {
4368
+ message: `Switched to "${selected.name}" mode.
4369
+ ${selected.description}`
4370
+ };
4371
+ }
4205
4372
  const lines = [`Current mode: ${active?.name ?? "none"}`, "", "Available modes:"];
4206
4373
  for (const m of modes) {
4207
4374
  const mark = m.id === active?.id ? " [active]" : "";
@@ -5306,6 +5473,195 @@ function buildAutoPhaseCommand(opts) {
5306
5473
  }
5307
5474
  };
5308
5475
  }
5476
+ function buildCollabCommand(opts) {
5477
+ return {
5478
+ name: "collab",
5479
+ description: "Live collaboration helpers (status / invite / history).",
5480
+ async run(args, ctx) {
5481
+ const parts = args.split(/\s+/).filter(Boolean);
5482
+ const sub = (parts[0] ?? "status").toLowerCase();
5483
+ switch (sub) {
5484
+ case "status":
5485
+ return statusCommand(ctx.session?.id);
5486
+ case "invite":
5487
+ return inviteCommand(ctx.session?.id);
5488
+ case "history":
5489
+ return historyCommand(opts, ctx.session?.id, parts.slice(1));
5490
+ case "annotations":
5491
+ case "notes":
5492
+ return annotationsCommand(opts, ctx.session?.id);
5493
+ case "help":
5494
+ case "--help":
5495
+ case "-h":
5496
+ return helpCommand();
5497
+ default:
5498
+ return {
5499
+ message: color.yellow(
5500
+ `Unknown subcommand: ${sub}. Try: /collab status | invite | history | annotations`
5501
+ )
5502
+ };
5503
+ }
5504
+ }
5505
+ };
5506
+ }
5507
+ function statusCommand(sessionId) {
5508
+ const lines = [];
5509
+ lines.push(color.bold("Live collaboration"));
5510
+ lines.push(` Session: ${color.cyan(sessionId ?? "(none)")}`);
5511
+ lines.push(` Observers: ${color.dim("unknown (check webui)")}`);
5512
+ lines.push("");
5513
+ lines.push(
5514
+ color.dim(
5515
+ " Tip: /collab invite \u2014 print a webui join link for a teammate"
5516
+ )
5517
+ );
5518
+ lines.push(
5519
+ color.dim(" /collab history 20 \u2014 show the last 20 events of this session")
5520
+ );
5521
+ return { message: lines.join("\n") };
5522
+ }
5523
+ function inviteCommand(sessionId) {
5524
+ if (!sessionId) {
5525
+ return { message: color.yellow("No active session to invite to.") };
5526
+ }
5527
+ const url = `http://127.0.0.1:3457/?session=${encodeURIComponent(sessionId)}`;
5528
+ const lines = [
5529
+ color.bold("Live-collaboration invite"),
5530
+ ` Session ID: ${color.cyan(sessionId)}`,
5531
+ ` Share this URL with a teammate (they must be able to reach your machine):`,
5532
+ ` ${color.cyan(url)}`,
5533
+ "",
5534
+ color.dim(
5535
+ ' The teammate opens the URL in their browser, then clicks "Join as observer"'
5536
+ ),
5537
+ color.dim(" on the panel that appears above the chat. They will see a live mirror"),
5538
+ color.dim(" of tool calls, iterations, and subagent spawns \u2014 read-only."),
5539
+ "",
5540
+ color.dim(" Reminder: expose the port with: pnpm --filter @wrongstack/webui dev")
5541
+ ];
5542
+ return { message: lines.join("\n") };
5543
+ }
5544
+ async function historyCommand(opts, sessionId, args) {
5545
+ const limit = Math.max(1, Math.min(200, Number.parseInt(args[0] ?? "20", 10) || 20));
5546
+ if (!sessionId) return { message: color.yellow("No active session.") };
5547
+ if (!opts.sessionStore) {
5548
+ return { message: color.yellow("No session store configured.") };
5549
+ }
5550
+ const { DefaultSessionReader: DefaultSessionReader2 } = await import('@wrongstack/core');
5551
+ const reader = new DefaultSessionReader2({ store: opts.sessionStore });
5552
+ const events = [];
5553
+ try {
5554
+ for await (const ev of reader.replay(sessionId)) {
5555
+ events.push(ev);
5556
+ }
5557
+ } catch (err) {
5558
+ return {
5559
+ message: color.yellow(
5560
+ `Failed to read session: ${err instanceof Error ? err.message : String(err)}`
5561
+ )
5562
+ };
5563
+ }
5564
+ const tail = events.slice(-limit);
5565
+ if (tail.length === 0) {
5566
+ return { message: color.dim("No events recorded yet for this session.") };
5567
+ }
5568
+ const lines = [
5569
+ color.bold(`Last ${tail.length} events of ${sessionId}`),
5570
+ ""
5571
+ ];
5572
+ for (const raw of tail) {
5573
+ const ev = raw;
5574
+ const t = ev.ts ? color.dim(ev.ts.slice(11, 19)) : color.dim("--:--:--");
5575
+ const kind = color.cyan((ev.type ?? "unknown").padEnd(16));
5576
+ const summary = summarise(ev);
5577
+ lines.push(` ${t} ${kind} ${summary}`);
5578
+ }
5579
+ return { message: lines.join("\n") };
5580
+ }
5581
+ async function annotationsCommand(opts, sessionId) {
5582
+ if (!sessionId) return { message: color.yellow("No active session.") };
5583
+ if (!opts.sessionStore) {
5584
+ return { message: color.yellow("No session store configured.") };
5585
+ }
5586
+ const { DefaultSessionReader: DefaultSessionReader2, AnnotationsStore } = await import('@wrongstack/core');
5587
+ new DefaultSessionReader2({ store: opts.sessionStore });
5588
+ const storeDir = opts.sessionStore.dir;
5589
+ if (!storeDir) {
5590
+ return {
5591
+ message: color.yellow(
5592
+ "Annotations view needs the session store dir; not exposed in this build."
5593
+ )
5594
+ };
5595
+ }
5596
+ const annotations = new AnnotationsStore({ dir: storeDir });
5597
+ const open = await annotations.listOpen(sessionId);
5598
+ if (open.length === 0) {
5599
+ return {
5600
+ message: color.dim("No open annotations for this session.")
5601
+ };
5602
+ }
5603
+ const lines = [
5604
+ color.bold(`${open.length} open annotation(s) for ${sessionId}`),
5605
+ ""
5606
+ ];
5607
+ for (const a of open) {
5608
+ const t = color.dim(a.createdAt.slice(11, 19));
5609
+ const author = color.cyan(a.authorId.slice(0, 8));
5610
+ const idx = color.dim(`#${a.atEventIndex}`);
5611
+ lines.push(` ${t} ${idx} ${author} ${a.text}`);
5612
+ }
5613
+ return { message: lines.join("\n") };
5614
+ }
5615
+ function summarise(ev) {
5616
+ switch (ev["type"]) {
5617
+ case "user_input": {
5618
+ const text = typeof ev["text"] === "string" ? ev["text"] : "";
5619
+ return color.dim(truncate(text, 80));
5620
+ }
5621
+ case "llm_response": {
5622
+ const text = typeof ev["text"] === "string" ? ev["text"] : "";
5623
+ return color.dim(truncate(text, 80));
5624
+ }
5625
+ case "tool_result": {
5626
+ const name = String(ev["name"] ?? "?");
5627
+ const ok = ev["ok"] === false ? color.red("(failed)") : color.green("(ok)");
5628
+ return `${name} ${ok}`;
5629
+ }
5630
+ case "compaction": {
5631
+ const before = ev["before"];
5632
+ const after = ev["after"];
5633
+ return `tokens ${before}\u2192${after}`;
5634
+ }
5635
+ case "error": {
5636
+ return color.red(String(ev["message"] ?? "(no message)"));
5637
+ }
5638
+ default: {
5639
+ return color.dim(JSON.stringify(ev).slice(0, 80));
5640
+ }
5641
+ }
5642
+ }
5643
+ function truncate(s, n) {
5644
+ if (s.length <= n) return s;
5645
+ return `${s.slice(0, n - 1)}\u2026`;
5646
+ }
5647
+ function helpCommand() {
5648
+ const lines = [
5649
+ color.bold("/collab \u2014 live collaboration (read-only observers)"),
5650
+ "",
5651
+ "Subcommands:",
5652
+ ` ${color.cyan("status")} Show current session's collaboration status`,
5653
+ ` ${color.cyan("invite")} Print a webui join link for a teammate`,
5654
+ ` ${color.cyan("history [N]")} Show the last N events of this session (default 20, max 200)`,
5655
+ ` ${color.cyan("annotations")} List open (unresolved) annotations from webui annotators`,
5656
+ ` ${color.cyan("help")} Show this help`,
5657
+ "",
5658
+ color.dim(
5659
+ " The observer flow itself lives in the webui: open the URL printed"
5660
+ ),
5661
+ color.dim(' by `invite` and click "Join as observer" on the panel above the chat.')
5662
+ ];
5663
+ return { message: lines.join("\n") };
5664
+ }
5309
5665
 
5310
5666
  // src/slash-commands/worktree.ts
5311
5667
  function buildWorktreeCommand(opts) {
@@ -5524,6 +5880,7 @@ function buildBuiltinSlashCommands(opts) {
5524
5880
  buildAutoPhaseCommand(opts),
5525
5881
  buildWorktreeCommand(opts),
5526
5882
  buildSettingsCommand(opts),
5883
+ buildCollabCommand(opts),
5527
5884
  buildStatuslineCommand({
5528
5885
  cwd: opts.cwd,
5529
5886
  hiddenItems: opts.statuslineHiddenItems ?? [],
@@ -5753,7 +6110,11 @@ async function bootConfig(flags) {
5753
6110
  }
5754
6111
  }
5755
6112
  const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
5756
- const config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
6113
+ let config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
6114
+ const syncConfig = await configLoader.loadSyncConfig();
6115
+ if (syncConfig) {
6116
+ config = Object.freeze({ ...config, sync: syncConfig });
6117
+ }
5757
6118
  return {
5758
6119
  paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
5759
6120
  config,
@@ -5906,7 +6267,7 @@ var ReadlineInputReader = class {
5906
6267
  * user gets visual confirmation that bytes are arriving (especially on
5907
6268
  * paste, which previously felt like nothing happened). Pasted chunks
5908
6269
  * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
5909
- * Ctrl+U / Ctrl+W are honored. Non-TTY input is read normally — there's
6270
+ * Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
5910
6271
  * nothing to hide when piped, and echoing bullets to a file is noise.
5911
6272
  *
5912
6273
  * Returns the raw entered string (no trim — caller decides).
@@ -7844,9 +8205,9 @@ var updateCmd = async (args, deps) => {
7844
8205
  cwd,
7845
8206
  stdio: "pipe"
7846
8207
  });
7847
- let stderr = "";
8208
+ let _stderr = "";
7848
8209
  child.stderr?.on("data", (d) => {
7849
- stderr += d;
8210
+ _stderr += d;
7850
8211
  });
7851
8212
  child.on("error", reject);
7852
8213
  child.on("close", (code) => resolve4({ code: code ?? 0 }));
@@ -9080,6 +9441,167 @@ ${result.errors.length} error(s):
9080
9441
  return 1;
9081
9442
  }
9082
9443
  };
9444
+ var replayCmd = async (args, deps) => {
9445
+ const wpaths = resolveWstackPaths({
9446
+ projectRoot: deps.projectRoot,
9447
+ userHome: deps.userHome
9448
+ });
9449
+ const store = new ReplayLogStore({ dir: wpaths.projectSessions });
9450
+ if (args[0] === "--list" || args[0] === "-l") {
9451
+ const all = await store.list();
9452
+ if (all.length === 0) {
9453
+ deps.renderer.write(
9454
+ color.dim("No replay logs recorded yet. Run with --record to start one.") + "\n"
9455
+ );
9456
+ return 0;
9457
+ }
9458
+ const lines2 = [
9459
+ color.bold(`${all.length} replay log(s)`),
9460
+ color.dim(" Sorted by session id. Each entry has at least one recorded request/response."),
9461
+ ""
9462
+ ];
9463
+ for (const r of all) {
9464
+ lines2.push(
9465
+ ` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${color.dim(r.path)}`
9466
+ );
9467
+ }
9468
+ lines2.push("");
9469
+ lines2.push(
9470
+ color.dim(
9471
+ " Inspect: wstack replay <sessionId>"
9472
+ )
9473
+ );
9474
+ lines2.push(
9475
+ color.dim(
9476
+ " Replay: wstack --replay <sessionId>"
9477
+ )
9478
+ );
9479
+ deps.renderer.write(lines2.join("\n") + "\n");
9480
+ return 0;
9481
+ }
9482
+ const sessionId = args[0];
9483
+ if (!sessionId) {
9484
+ deps.renderer.writeError(
9485
+ "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"
9486
+ );
9487
+ return 1;
9488
+ }
9489
+ const entries = await store.load(sessionId);
9490
+ if (entries.length === 0) {
9491
+ deps.renderer.write(
9492
+ color.yellow(
9493
+ `No replay entries recorded for session ${sessionId}.
9494
+ Run a session with --record first, then --replay to re-execute it.`
9495
+ ) + "\n"
9496
+ );
9497
+ return 0;
9498
+ }
9499
+ const lines = [
9500
+ color.bold(`Replay log for ${sessionId}`),
9501
+ ` ${entries.length} recorded request/response pair(s)`,
9502
+ ` Log file: ${color.dim(`${sessionId}.replay.jsonl`)}`,
9503
+ "",
9504
+ "To replay this session deterministically:",
9505
+ ` ${color.cyan(`wstack --replay ${sessionId}`)}`,
9506
+ "",
9507
+ "Recorded timestamps (oldest first):"
9508
+ ];
9509
+ for (const e of entries.slice(-10)) {
9510
+ lines.push(
9511
+ ` ${color.dim(e.ts.slice(11, 19))} ${e.hash.slice(0, 16)}\u2026 ${e.request.model}`
9512
+ );
9513
+ }
9514
+ if (entries.length > 10) {
9515
+ lines.push(color.dim(` \u2026 and ${entries.length - 10} more`));
9516
+ }
9517
+ deps.renderer.write(lines.join("\n") + "\n");
9518
+ return 0;
9519
+ };
9520
+ var auditCmd = async (args, deps) => {
9521
+ const wpaths = resolveWstackPaths({
9522
+ projectRoot: deps.projectRoot,
9523
+ userHome: deps.userHome
9524
+ });
9525
+ const log = new ToolAuditLog({ dir: wpaths.projectSessions });
9526
+ if (args[0] === "--list" || args[0] === "-l") {
9527
+ return listAudits(log, wpaths.projectSessions, deps);
9528
+ }
9529
+ const sessionId = args[0];
9530
+ if (!sessionId) {
9531
+ deps.renderer.writeError(
9532
+ "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"
9533
+ );
9534
+ return 1;
9535
+ }
9536
+ const entries = await log.load(sessionId);
9537
+ if (entries.length === 0) {
9538
+ deps.renderer.write(
9539
+ color.yellow(
9540
+ `No audit entries for session ${sessionId}. (The audit log is written when tools are recorded \u2014 check that the session ran with audit enabled.)`
9541
+ ) + "\n"
9542
+ );
9543
+ return 0;
9544
+ }
9545
+ const verify = await log.verify(sessionId);
9546
+ const lines = [
9547
+ color.bold(`Audit log for ${sessionId}`),
9548
+ verify.ok ? ` ${color.green("\u2713 Verified")} ${entries.length} entries, chain intact` : ` ${color.red("\u2717 BROKEN")} at entry ${verify.brokenAt}: ${verify.reason}`,
9549
+ "",
9550
+ " Entries (oldest first):"
9551
+ ];
9552
+ const cap = 12;
9553
+ for (const e of entries.slice(0, cap)) {
9554
+ const t = color.dim(e.ts.slice(11, 19));
9555
+ const idx = color.dim(`#${e.index}`.padStart(4));
9556
+ const tool = e.toolName;
9557
+ const err = e.isError ? color.red("(error)") : "";
9558
+ lines.push(
9559
+ ` ${t} ${idx} ${color.cyan(tool.padEnd(14))} ${err} ${color.dim(e.hash.slice(0, 12) + "\u2026")}`
9560
+ );
9561
+ }
9562
+ if (entries.length > cap) {
9563
+ lines.push(color.dim(` \u2026 and ${entries.length - cap} more`));
9564
+ }
9565
+ deps.renderer.write(lines.join("\n") + "\n");
9566
+ return verify.ok ? 0 : 1;
9567
+ };
9568
+ async function listAudits(log, dir, deps) {
9569
+ const fs22 = await import('fs/promises');
9570
+ let entries;
9571
+ try {
9572
+ entries = await fs22.readdir(dir);
9573
+ } catch {
9574
+ deps.renderer.write(
9575
+ color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
9576
+ );
9577
+ return 0;
9578
+ }
9579
+ const out = [];
9580
+ for (const name of entries) {
9581
+ if (!name.endsWith(".audit.jsonl")) continue;
9582
+ const sessionId = name.slice(0, -".audit.jsonl".length);
9583
+ const all = await log.load(sessionId);
9584
+ const verify = await log.verify(sessionId);
9585
+ out.push({ sessionId, entryCount: all.length, ok: verify.ok });
9586
+ }
9587
+ out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
9588
+ if (out.length === 0) {
9589
+ deps.renderer.write(color.dim("No audit logs recorded yet.") + "\n");
9590
+ return 0;
9591
+ }
9592
+ const lines = [
9593
+ color.bold(`${out.length} audit log(s)`),
9594
+ ""
9595
+ ];
9596
+ for (const r of out) {
9597
+ const status = r.ok ? color.green("\u2713 intact") : color.red("\u2717 broken");
9598
+ lines.push(
9599
+ ` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${status}`
9600
+ );
9601
+ }
9602
+ deps.renderer.write(lines.join("\n") + "\n");
9603
+ return 0;
9604
+ }
9083
9605
  var toolsCmd = async (_args, deps) => {
9084
9606
  const reg = deps.toolRegistry;
9085
9607
  if (!reg) return 0;
@@ -9146,6 +9668,8 @@ var subcommands = {
9146
9668
  sessions: sessionsCmd,
9147
9669
  config: configCmd,
9148
9670
  rewind: rewindCmd,
9671
+ replay: replayCmd,
9672
+ audit: auditCmd,
9149
9673
  tools: toolsCmd,
9150
9674
  skills: skillsCmd,
9151
9675
  providers: providersCmd,
@@ -9182,22 +9706,22 @@ function fmtDuration(ms) {
9182
9706
  const remMin = m - h * 60;
9183
9707
  return `${h}h${remMin}m`;
9184
9708
  }
9185
- function fmtTaskResultLine(r, color37) {
9709
+ function fmtTaskResultLine(r, color41) {
9186
9710
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
9187
9711
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
9188
9712
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
9189
9713
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
9190
- const errKindChip = errKind ? color37.dim(` [${errKind}]`) : "";
9191
- const errSnip = errMsg || errKind ? `${errKindChip}${color37.dim(errTail)}` : "";
9714
+ const errKindChip = errKind ? color41.dim(` [${errKind}]`) : "";
9715
+ const errSnip = errMsg || errKind ? `${errKindChip}${color41.dim(errTail)}` : "";
9192
9716
  switch (r.status) {
9193
9717
  case "success":
9194
- return { mark: color37.green("\u2713"), stats, tail: "" };
9718
+ return { mark: color41.green("\u2713"), stats, tail: "" };
9195
9719
  case "timeout":
9196
- return { mark: color37.yellow("\u23F1"), stats: `${color37.yellow("timeout")} ${stats}`, tail: errSnip };
9720
+ return { mark: color41.yellow("\u23F1"), stats: `${color41.yellow("timeout")} ${stats}`, tail: errSnip };
9197
9721
  case "stopped":
9198
- return { mark: color37.dim("\u2298"), stats: `${color37.dim("stopped")} ${stats}`, tail: errSnip };
9722
+ return { mark: color41.dim("\u2298"), stats: `${color41.dim("stopped")} ${stats}`, tail: errSnip };
9199
9723
  case "failed":
9200
- return { mark: color37.red("\u2717"), stats: `${color37.red("failed")} ${stats}`, tail: errSnip };
9724
+ return { mark: color41.red("\u2717"), stats: `${color41.red("failed")} ${stats}`, tail: errSnip };
9201
9725
  }
9202
9726
  }
9203
9727
 
@@ -9687,7 +10211,7 @@ async function runRepl(opts) {
9687
10211
  );
9688
10212
  interrupts = 0;
9689
10213
  try {
9690
- const ok = await engine.runOneIteration();
10214
+ await engine.runOneIteration();
9691
10215
  const afterGoal = await loadGoalSafe(opts);
9692
10216
  const last = afterGoal?.journal[afterGoal.journal.length - 1];
9693
10217
  if (coord) {
@@ -9801,7 +10325,7 @@ ${color.dim(taskList2)}
9801
10325
  }
9802
10326
  }
9803
10327
  }
9804
- } catch (runErr) {
10328
+ } catch (_runErr) {
9805
10329
  opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
9806
10330
  }
9807
10331
  }
@@ -10501,7 +11025,7 @@ async function execute(deps) {
10501
11025
  fleetStatusLine?.stop();
10502
11026
  try {
10503
11027
  stats.render(renderer);
10504
- } catch (err) {
11028
+ } catch (_err) {
10505
11029
  }
10506
11030
  await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
10507
11031
  await mcpRegistry.stopAll();
@@ -10526,6 +11050,15 @@ var MultiAgentHost = class {
10526
11050
  /** Own FleetManager — created in buildDirector(), used for pending task
10527
11051
  * tracking so status() can show descriptions without host-side state. */
10528
11052
  fleetManager;
11053
+ /** Own FleetEmitTool — created in buildDirector() so subagents in director
11054
+ * mode can publish structured events (bug.found, refactor.plan,
11055
+ * critic.evaluation) onto the fleet bus without needing the tool registered
11056
+ * in the host's ToolRegistry. */
11057
+ fleetEmitTool;
11058
+ /** Own FleetStatusTool — created in buildDirector() so subagents in director
11059
+ * mode can read fleet state (subagent statuses, task progress) without the
11060
+ * tool being in the host's ToolRegistry. */
11061
+ fleetStatusTool;
10529
11062
  /** Lazily built alongside the director — produces per-subagent JSONL
10530
11063
  * writers under `<sessionsRoot>/<runId>/`. Null without sessionsRoot. */
10531
11064
  sessionFactory;
@@ -10562,7 +11095,7 @@ var MultiAgentHost = class {
10562
11095
  getDirector() {
10563
11096
  return this.director;
10564
11097
  }
10565
- async ensureCoordinator(config) {
11098
+ async ensureCoordinator(_config) {
10566
11099
  await this.buildDirector();
10567
11100
  }
10568
11101
  async buildDirector() {
@@ -10629,6 +11162,15 @@ var MultiAgentHost = class {
10629
11162
  totalExtensions: payload.totalExtensions
10630
11163
  });
10631
11164
  });
11165
+ this.director.fleet.filter("ctx.pct", (e) => {
11166
+ const payload = e.payload;
11167
+ this.deps.events.emit("subagent.ctx_pct", {
11168
+ subagentId: e.subagentId,
11169
+ load: payload.load,
11170
+ tokens: payload.tokens,
11171
+ maxContext: payload.maxContext
11172
+ });
11173
+ });
10632
11174
  this.getCoordinator().on(
10633
11175
  "task.assigned",
10634
11176
  ({
@@ -10642,9 +11184,19 @@ var MultiAgentHost = class {
10642
11184
  });
10643
11185
  }
10644
11186
  );
11187
+ this.fleetEmitTool = makeFleetEmitTool(this.director);
11188
+ this.fleetStatusTool = makeFleetStatusTool(this.director);
10645
11189
  const runner = await this.buildSubagentRunner(config);
10646
11190
  this.getCoordinator().setRunner(runner);
10647
11191
  }
11192
+ /**
11193
+ * Returns the FleetEmitTool for director-mode subagents, if the director
11194
+ * has been built. Used by makeSubagentFactory to inject the tool into
11195
+ * the filtered tool registry so collab session agents can emit fleet events.
11196
+ */
11197
+ getFleetEmitTool() {
11198
+ return this.fleetEmitTool;
11199
+ }
10648
11200
  /**
10649
11201
  * Build a per-role subagent factory: given a SubagentConfig, construct a
10650
11202
  * fresh, isolated Agent with the role's filtered tools and (when the config
@@ -10689,6 +11241,21 @@ var MultiAgentHost = class {
10689
11241
  append: (ev) => parentSession.append({ ...ev })
10690
11242
  };
10691
11243
  }
11244
+ const tools = subCfg.tools ? [...subCfg.tools] : void 0;
11245
+ let injectedFleetEmit;
11246
+ let injectedFleetStatus;
11247
+ if (tools) {
11248
+ const emitIdx = tools.indexOf("fleet_emit");
11249
+ if (emitIdx >= 0 && this.fleetEmitTool) {
11250
+ tools.splice(emitIdx, 1);
11251
+ injectedFleetEmit = this.fleetEmitTool;
11252
+ }
11253
+ const statusIdx = tools.indexOf("fleet_status");
11254
+ if (statusIdx >= 0 && this.fleetStatusTool) {
11255
+ tools.splice(statusIdx, 1);
11256
+ injectedFleetStatus = this.fleetStatusTool;
11257
+ }
11258
+ }
10692
11259
  const ctx = new Context({
10693
11260
  systemPrompt: baseSystem,
10694
11261
  provider,
@@ -10698,9 +11265,12 @@ var MultiAgentHost = class {
10698
11265
  cwd: subCwd,
10699
11266
  projectRoot: this.deps.projectRoot,
10700
11267
  model: subCfg.model ?? config.model,
10701
- tools: this.filterTools(subCfg.tools)
11268
+ tools: this.filterTools(tools)
10702
11269
  });
10703
- const toolExecutor = new ToolExecutor(this.subagentToolRegistry(subCfg.tools), {
11270
+ const baseRegistry = this.subagentToolRegistry(tools);
11271
+ if (injectedFleetEmit) baseRegistry.register(injectedFleetEmit);
11272
+ if (injectedFleetStatus) baseRegistry.register(injectedFleetStatus);
11273
+ const toolExecutor = new ToolExecutor(baseRegistry, {
10704
11274
  permissionPolicy: new AutoApprovePermissionPolicy(),
10705
11275
  secretScrubber: this.deps.secretScrubber,
10706
11276
  renderer: this.deps.renderer,
@@ -10712,7 +11282,7 @@ var MultiAgentHost = class {
10712
11282
  });
10713
11283
  const agent = new Agent({
10714
11284
  container: this.deps.container,
10715
- tools: this.subagentToolRegistry(subCfg.tools),
11285
+ tools: baseRegistry,
10716
11286
  providers: this.deps.providerRegistry,
10717
11287
  events,
10718
11288
  pipelines: createDefaultPipelines(),
@@ -11111,9 +11681,9 @@ function makeConfirmAwaiter(reader) {
11111
11681
  function stringifyInput(input) {
11112
11682
  if (!input || typeof input !== "object") return "";
11113
11683
  const obj = input;
11114
- return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
11684
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate2(JSON.stringify(v), 80)}`).join(" ");
11115
11685
  }
11116
- function truncate(s, max) {
11686
+ function truncate2(s, max) {
11117
11687
  return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
11118
11688
  }
11119
11689
  function hasDiff(input) {
@@ -11121,6 +11691,26 @@ function hasDiff(input) {
11121
11691
  input && typeof input === "object" && "diff" in input
11122
11692
  );
11123
11693
  }
11694
+ function bindReplayToContainer(opts) {
11695
+ const { container, wpaths, sessionId, mode, logger } = opts;
11696
+ if (!opts.container.has(TOKENS.ProviderRunner)) {
11697
+ container.bind(TOKENS.ProviderRunner, () => ({
11698
+ run: (o) => runProviderWithRetry(o)
11699
+ }));
11700
+ }
11701
+ const inner = container.resolve(TOKENS.ProviderRunner);
11702
+ const log = new ReplayLogStore({ dir: wpaths.projectSessions });
11703
+ const wrapped = new ReplayProviderRunner(inner, {
11704
+ log,
11705
+ sessionId,
11706
+ mode,
11707
+ logger: logger ? {
11708
+ debug: (m) => logger.debug?.(m),
11709
+ warn: (m) => logger.warn?.(m)
11710
+ } : void 0
11711
+ });
11712
+ container.bind(TOKENS.ProviderRunner, () => wrapped);
11713
+ }
11124
11714
  var SessionStats = class {
11125
11715
  tokenCounter;
11126
11716
  startedAt = Date.now();
@@ -11599,7 +12189,6 @@ async function setupCompaction(params) {
11599
12189
  function createAgent(params) {
11600
12190
  const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
11601
12191
  const renderer = params.container.has(TOKENS.Renderer) ? params.container.resolve(TOKENS.Renderer) : void 0;
11602
- params.container.resolve(TOKENS.Logger);
11603
12192
  const toolExecutor = new ToolExecutor(params.tools, {
11604
12193
  permissionPolicy: params.permissionPolicy ?? params.container.resolve(TOKENS.PermissionPolicy),
11605
12194
  secretScrubber,
@@ -11983,7 +12572,6 @@ async function main(argv) {
11983
12572
  wpaths,
11984
12573
  cwd,
11985
12574
  projectRoot,
11986
- userHome,
11987
12575
  flags,
11988
12576
  positional,
11989
12577
  modelsRegistry,
@@ -12025,6 +12613,22 @@ async function main(argv) {
12025
12613
  compactor: { preserveK: config.context.preserveK, eliseThreshold: config.context.eliseThreshold },
12026
12614
  bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0
12027
12615
  });
12616
+ const replayFlag = flags["replay"];
12617
+ const recordFlag = flags["record"];
12618
+ if (typeof replayFlag === "string" || recordFlag === true) {
12619
+ const sessionId = typeof replayFlag === "string" ? replayFlag : `record-${Date.now()}`;
12620
+ const mode = recordFlag === true ? "record" : "replay";
12621
+ bindReplayToContainer({
12622
+ container,
12623
+ wpaths,
12624
+ sessionId,
12625
+ mode,
12626
+ logger
12627
+ });
12628
+ logger.info(
12629
+ `replay: ProviderRunner bound in '${mode}' mode for session ${sessionId}`
12630
+ );
12631
+ }
12028
12632
  const configStore = container.resolve(TOKENS.ConfigStore);
12029
12633
  container.bind(TOKENS.PathResolver, () => pathResolver);
12030
12634
  container.bind(TOKENS.Renderer, () => renderer);
@@ -12092,7 +12696,7 @@ async function main(argv) {
12092
12696
  }
12093
12697
  const events = new EventBus();
12094
12698
  events.setLogger(logger);
12095
- const { metricsSink, healthRegistry, metricsServerHandle } = (() => {
12699
+ const { metricsSink, healthRegistry } = (() => {
12096
12700
  const ms = setupMetrics({ flags, wpaths, events, logger, config: { provider: config.provider, model: config.model } });
12097
12701
  return ms;
12098
12702
  })();
@@ -12171,7 +12775,6 @@ async function main(argv) {
12171
12775
  });
12172
12776
  const session = sessResult.session;
12173
12777
  sessionRef.current = session;
12174
- sessResult.restoredMessages;
12175
12778
  const context = sessResult.context;
12176
12779
  const attachments = sessResult.attachments;
12177
12780
  const recoveryLock = sessResult.recoveryLock;