@ordewell/web 0.7.1 → 0.8.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.
@@ -103,7 +103,7 @@ var WebConfig = class extends core.BaseConfig {
103
103
  if (process.env.ORDEWELL_OPENCODE_ENABLED === "true" || process.env.ORDEWELL_OPENCODE_ENABLED === "1") {
104
104
  out.push("opencode");
105
105
  }
106
- return out.length > 0 ? out : ["claude-code"];
106
+ return out.length > 0 ? out : [...core.DEFAULT_RUNNERS];
107
107
  }
108
108
  };
109
109
  function scanWorkspacesImpl(deps) {
@@ -336,20 +336,22 @@ ${err}` : out;
336
336
  return { success: true, output: combined || "(no output)", truncated: combined.length > 2e4 };
337
337
  }
338
338
  };
339
+
340
+ // server/adapters/PoolAwareRunner.ts
339
341
  var PoolAwareRunner = class {
340
342
  constructor(sessionId, broadcast, inner) {
341
343
  this.sessionId = sessionId;
342
344
  this.broadcast = broadcast;
343
- this.inner = inner ?? new core.HeadlessRunner();
345
+ this.inner = inner;
344
346
  }
345
347
  sessionId;
346
348
  broadcast;
347
349
  inner;
348
350
  /**
349
- * Sessions this plan spawned. The inner runner may be shared by every plan
350
- * in the pool (one tmux session per daemon), so "stop everything" and
351
- * "how many are running" must be answered from this set, never delegated —
352
- * delegating would let one plan's /stop kill another plan's tasks.
351
+ * Sessions this plan spawned. The inner runner is shared by every plan in
352
+ * the pool, so "stop everything" and "how many are running" must be
353
+ * answered from this set, never delegated — delegating would let one plan's
354
+ * /stop kill another plan's tasks.
353
355
  */
354
356
  owned = /* @__PURE__ */ new Set();
355
357
  get activeCount() {
@@ -374,69 +376,22 @@ var PoolAwareRunner = class {
374
376
  this.owned.clear();
375
377
  }
376
378
  };
377
- var TMUX_MISSING_ADVICE = "tmux is not installed, so terminal-transport tasks run without a terminal window you can open or type into. Install tmux, or keep the structured transport (/transport structured).";
378
- function startTerminalHost(port2, deps = {}) {
379
- const hasTmuxImpl = deps.hasTmuxImpl ?? core.hasTmux;
380
- if (!hasTmuxImpl()) return { advice: TMUX_MISSING_ADVICE };
381
- const runner = (deps.createTmuxRunner ?? ((p) => new core.TmuxRunner({ port: p })))(port2);
382
- runner.ensureSession().catch((err) => {
383
- console.error(`[web] tmux session setup failed (will retry on next task spawn): ${err?.message ?? err}`);
384
- });
385
- return { runner };
386
- }
387
- var AdvisingRunner = class {
388
- constructor(inner, advice, advise) {
389
- this.inner = inner;
390
- this.advice = advice;
391
- this.advise = advise;
392
- }
393
- inner;
394
- advice;
395
- advise;
396
- advised = false;
397
- get activeCount() {
398
- return this.inner.activeCount;
399
- }
400
- spawn(opts) {
401
- if (!this.advised) {
402
- this.advised = true;
403
- this.advise(this.advice);
404
- }
405
- return this.inner.spawn(opts);
406
- }
407
- stop(sessionId) {
408
- this.inner.stop(sessionId);
409
- }
410
- stopAll() {
411
- this.inner.stopAll();
412
- }
413
- };
414
379
 
415
380
  // server/pool/orchestratorPool.ts
416
381
  var OrchestratorPool = class {
417
382
  sessions = /* @__PURE__ */ new Map();
418
383
  clients = /* @__PURE__ */ new Map();
419
- /** The in-flight planning turn's abort controller, one per session — see `cancelPlanning`. */
420
- planningAborts = /* @__PURE__ */ new Map();
421
- /** Sessions whose planning slot a compaction holds; a reply must not take it over. */
422
- compacting = /* @__PURE__ */ new Set();
423
- registry = (() => {
424
- const r = new core.RunnerRegistry();
425
- r.loadUserPlugins();
426
- return r;
427
- })();
384
+ registry = new core.RunnerRegistry();
428
385
  modelResolver;
429
386
  runnerInstallation = new core.RunnerInstallation(this.registry);
430
387
  cachedProviderLists;
431
388
  settingsService = new core.SettingsService();
432
389
  plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
433
- sharedRunner;
434
- structuredRunner;
435
- terminalAdvice;
390
+ runner;
436
391
  constructor(deps = {}) {
437
- this.sharedRunner = deps.runner;
438
- this.structuredRunner = deps.structuredRunner ?? new core.StructuredRunner();
439
- this.terminalAdvice = deps.terminalAdvice;
392
+ const pluginNotice = core.removedPluginNotice();
393
+ if (pluginNotice) console.warn(pluginNotice);
394
+ this.runner = deps.runner ?? new core.StructuredRunner();
440
395
  this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
441
396
  }
442
397
  subscribe(sessionId, ws) {
@@ -502,12 +457,7 @@ var OrchestratorPool = class {
502
457
  });
503
458
  const fsAdapter = new PoolFileSystem(workspace);
504
459
  const broadcast = (msg) => this.broadcast(sessionId, msg);
505
- const terminal = this.sharedRunner ?? new core.HeadlessRunner();
506
- const router = new core.TransportRouter({
507
- terminal: this.terminalAdvice ? new AdvisingRunner(terminal, this.terminalAdvice, (message) => this.broadcast(sessionId, { type: "notice", level: "warn", message })) : terminal,
508
- structured: this.structuredRunner
509
- });
510
- const runner = new PoolAwareRunner(sessionId, broadcast, router);
460
+ const runner = new PoolAwareRunner(sessionId, broadcast, this.runner);
511
461
  return core.createSession({
512
462
  config,
513
463
  notifications: { info() {
@@ -561,14 +511,10 @@ var OrchestratorPool = class {
561
511
  // for a planner that is running at "high".
562
512
  plannerThinkingEffort: config.plannerThinkingEffort ?? "",
563
513
  maxParallel: config.maxParallelSessions,
564
- tdd: userSettings.tdd,
565
- verification: userSettings.verification,
566
514
  modelAllowlist: userSettings.modelAllowlist,
567
515
  // The model remembered per planner backend (this task), so a surface can
568
516
  // render what's remembered without a second round-trip.
569
- plannerModels: userSettings.plannerModels,
570
- // ADR-0018; a run copies it when it starts.
571
- runnerTransport: userSettings.runnerTransport
517
+ plannerModels: userSettings.plannerModels
572
518
  };
573
519
  }
574
520
  /**
@@ -596,22 +542,18 @@ var OrchestratorPool = class {
596
542
  const envChanges = admission2 ? admission2.accepted : void 0;
597
543
  const incomingAiProvider = typeof envChanges?.AI_PROVIDER === "string" ? envChanges.AI_PROVIDER : void 0;
598
544
  const switchedToProvider = incomingAiProvider && incomingAiProvider !== providerBefore ? incomingAiProvider : void 0;
599
- const switchRecall = switchedToProvider ? this.plannerModelMemory.recall(switchedToProvider, this.plannerCatalogFor(switchedToProvider)) : void 0;
545
+ const requestedModel = envChanges?.ORCHESTRATOR_MODEL ?? changes.orchestratorModel;
546
+ const explicitModel = typeof requestedModel === "string" && requestedModel.trim().length > 0;
547
+ const switchRecall = switchedToProvider && !explicitModel ? this.plannerModelMemory.recall(switchedToProvider, this.plannerCatalogFor(switchedToProvider)) : void 0;
548
+ if (switchedToProvider && explicitModel && typeof changes.plannerThinkingEffort !== "string" && envChanges?.ORDEWELL_PLANNER_EFFORT === void 0) {
549
+ process.env.ORDEWELL_PLANNER_EFFORT = "";
550
+ }
600
551
  if (typeof changes.orchestratorModel === "string") {
601
552
  process.env.ORCHESTRATOR_MODEL = changes.orchestratorModel;
602
553
  }
603
554
  if (typeof changes.plannerThinkingEffort === "string") {
604
555
  process.env.ORDEWELL_PLANNER_EFFORT = changes.plannerThinkingEffort;
605
556
  }
606
- if (changes.tdd && typeof changes.tdd.enabled === "boolean") {
607
- this.settingsService.setTdd(changes.tdd.enabled);
608
- }
609
- if (changes.verification && typeof changes.verification.enabled === "boolean") {
610
- this.settingsService.setVerification(changes.verification.enabled);
611
- }
612
- if (core.isRunnerTransport(changes.runnerTransport)) {
613
- this.settingsService.setRunnerTransport(changes.runnerTransport);
614
- }
615
557
  if (envChanges) {
616
558
  let touched = false;
617
559
  for (const [key, value] of Object.entries(envChanges)) {
@@ -619,7 +561,11 @@ var OrchestratorPool = class {
619
561
  if (core.PROVIDER_CREDENTIAL_ENV.has(key)) touched = true;
620
562
  }
621
563
  if (envChanges.ORDEWELL_MAX_PARALLEL !== void 0) {
622
- for (const session of this.sessions.values()) void session.reschedule();
564
+ for (const [id, session] of this.sessions) {
565
+ session.reschedule().catch((err) => {
566
+ console.error(`[web] reschedule failed for session ${id}: ${err instanceof Error ? err.message : err}`);
567
+ });
568
+ }
623
569
  }
624
570
  if (touched) {
625
571
  this.modelResolver.invalidate();
@@ -640,7 +586,7 @@ var OrchestratorPool = class {
640
586
  }
641
587
  }
642
588
  }
643
- const envCarriesModelOrEffort = envChanges?.AI_PROVIDER === void 0 && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
589
+ const envCarriesModelOrEffort = (envChanges?.AI_PROVIDER === void 0 || explicitModel) && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
644
590
  if (typeof changes.orchestratorModel === "string" || typeof changes.plannerThinkingEffort === "string" || envCarriesModelOrEffort) {
645
591
  const rememberProvider = switchedToProvider ?? new WebConfig({
646
592
  enabledRunners: this.enabledRunnerOverride(),
@@ -655,6 +601,7 @@ var OrchestratorPool = class {
655
601
  const rejectedEnvKeys = admission2?.rejected ?? [];
656
602
  const settings = this.getSettings();
657
603
  if (rejectedEnvKeys.length > 0) settings.rejectedEnvKeys = rejectedEnvKeys;
604
+ if (switchRecall) settings.switchRecall = switchRecall;
658
605
  return settings;
659
606
  }
660
607
  setOrchestratorModel(model) {
@@ -683,18 +630,7 @@ var OrchestratorPool = class {
683
630
  }
684
631
  }
685
632
  }
686
- const providers = [];
687
- for (const provider of core.PROVIDER_PRIORITY) {
688
- if (provider === "openai_compatible") {
689
- if (config.openaiCompatibleBaseUrl) providers.push(provider);
690
- } else if (provider === "openrouter") {
691
- if (config.openrouterKey) providers.push(provider);
692
- } else if (provider === "google") {
693
- if (config.geminiKey) providers.push(provider);
694
- } else if (config.getProviderApiKey(provider)) {
695
- providers.push(provider);
696
- }
697
- }
633
+ const providers = core.configuredProviders(config);
698
634
  return {
699
635
  models: allModels,
700
636
  modelsByRunner,
@@ -717,11 +653,11 @@ var OrchestratorPool = class {
717
653
  async getInstalledRunners() {
718
654
  const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
719
655
  const enabled = new Set(config.enabledRunners);
720
- const plugins = this.registry.list();
656
+ const runners = this.registry.list();
721
657
  const installedIds = new Set(
722
- await this.runnerInstallation.filterInstalled(plugins.map((p) => p.manifest.name))
658
+ await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
723
659
  );
724
- return plugins.filter((p) => installedIds.has(p.manifest.name)).map((p) => ({
660
+ return runners.filter((p) => installedIds.has(p.manifest.name)).map((p) => ({
725
661
  id: p.manifest.name,
726
662
  name: p.manifest.displayName,
727
663
  enabled: enabled.has(p.manifest.name)
@@ -729,15 +665,9 @@ var OrchestratorPool = class {
729
665
  }
730
666
  async generatePlan(sessionId, goal, runners, workspace, modelOverride, options) {
731
667
  const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
668
+ this.destroy(sessionId);
732
669
  this.sessions.set(sessionId, session);
733
- const controller = new AbortController();
734
- this.planningAborts.set(sessionId, controller);
735
- try {
736
- const plan = await session.generatePlan(goal, runners, { signal: controller.signal });
737
- return core.migratePlanState(plan);
738
- } finally {
739
- this.clearPlanningAbort(sessionId, controller);
740
- }
670
+ return core.migratePlanState(await session.generatePlan(goal, runners));
741
671
  }
742
672
  /**
743
673
  * Conversational planning (ADR-0002): start the planner dialogue for a
@@ -747,69 +677,27 @@ var OrchestratorPool = class {
747
677
  */
748
678
  async startPlanning(sessionId, goal, runners, workspace, modelOverride, options) {
749
679
  const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
680
+ this.destroy(sessionId);
750
681
  this.sessions.set(sessionId, session);
751
- const controller = new AbortController();
752
- this.planningAborts.set(sessionId, controller);
753
- try {
754
- return await session.startPlanning(goal, runners, { signal: controller.signal });
755
- } finally {
756
- this.clearPlanningAbort(sessionId, controller);
757
- }
682
+ return session.startPlanning(goal, runners);
758
683
  }
759
- /**
760
- * Continue the planner dialogue with the user's reply. Refused during a
761
- * compaction before the abort slot is touched: the session refuses the reply
762
- * too, but by then the compaction's controller would be gone and a stop could
763
- * no longer reach its summary turn.
764
- */
684
+ /** Continue the planner dialogue with the user's reply; refused while a planner turn is in flight. */
765
685
  async continuePlanning(sessionId, message) {
766
- if (this.compacting.has(sessionId)) throw new core.ConversationBusyError("send a message");
767
- const controller = new AbortController();
768
- this.planningAborts.set(sessionId, controller);
769
- try {
770
- return await this.session(sessionId).continueConversation(message, { signal: controller.signal });
771
- } finally {
772
- this.clearPlanningAbort(sessionId, controller);
773
- }
686
+ return this.session(sessionId).continueConversation(message);
774
687
  }
775
- /**
776
- * Condense a session's conversation. The summary turn is a planning turn as
777
- * far as a surface is concerned, so it registers the same abort controller
778
- * and `cancelPlanning` stops it — but only when no turn holds that slot
779
- * already: taking it over would leave the reply in flight beyond stopping.
780
- */
688
+ /** Condense a session's conversation. The summary turn is a planner turn, so `cancelPlanning` stops it. */
781
689
  async compactConversation(sessionId) {
782
690
  const session = this.session(sessionId);
783
- if (this.planningAborts.has(sessionId)) throw new core.ConversationBusyError("condense the conversation");
784
- const controller = new AbortController();
785
- this.planningAborts.set(sessionId, controller);
786
- this.compacting.add(sessionId);
787
- try {
788
- const compaction = await session.compactConversation(controller.signal);
789
- return { ...compaction, plan: session.planState };
790
- } finally {
791
- this.compacting.delete(sessionId);
792
- this.clearPlanningAbort(sessionId, controller);
793
- }
691
+ const compaction = await session.compactConversation();
692
+ return { ...compaction, plan: session.planState };
794
693
  }
795
694
  /**
796
- * Abort the planning turn in flight for a session, if any. Answers whether
695
+ * Abort the planner turn in flight for a session, if any. Answers whether
797
696
  * there was one to cancel — the route reports that rather than 404ing a
798
697
  * session that simply is not planning right now.
799
698
  */
800
699
  cancelPlanning(sessionId) {
801
- const controller = this.planningAborts.get(sessionId);
802
- if (!controller) return false;
803
- controller.abort();
804
- return true;
805
- }
806
- /**
807
- * Only clears the map entry if it still holds the controller this turn
808
- * created — a later turn may already have installed its own by the time
809
- * this one's `finally` runs (a fresh request racing a slow abort).
810
- */
811
- clearPlanningAbort(sessionId, controller) {
812
- if (this.planningAborts.get(sessionId) === controller) this.planningAborts.delete(sessionId);
700
+ return this.sessions.get(sessionId)?.abortPlannerTurn() ?? false;
813
701
  }
814
702
  /**
815
703
  * Register a session persisted on disk so its plan becomes executable and
@@ -824,9 +712,10 @@ var OrchestratorPool = class {
824
712
  const live = this.sessions.get(sessionId);
825
713
  if (live?.planState) return live.planState;
826
714
  const saved = core.loadSession(sessionId, workspace);
827
- if (!saved) throw new Error("Session not found");
715
+ if (!saved) throw new core.SessionNotFoundError();
828
716
  const session = this.createSessionFor(sessionId, workspace);
829
717
  session.loadPlan(saved.plan, saved.meta.goal, workspace, { sessionId: saved.meta.id });
718
+ this.destroy(sessionId);
830
719
  this.sessions.set(sessionId, session);
831
720
  return session.planState ?? saved.plan;
832
721
  }
@@ -853,14 +742,10 @@ var OrchestratorPool = class {
853
742
  */
854
743
  session(sessionId) {
855
744
  const session = this.sessions.get(sessionId);
856
- if (!session) throw new Error("Session not found");
745
+ if (!session) throw new core.SessionNotFoundError();
857
746
  return session;
858
747
  }
859
- // The Session drops its planner context, but the model call in flight is
860
- // the pool's to abort: it holds the only signal that reaches it.
861
748
  destroy(sessionId) {
862
- this.planningAborts.get(sessionId)?.abort();
863
- this.planningAborts.delete(sessionId);
864
749
  this.sessions.get(sessionId)?.destroy();
865
750
  this.sessions.delete(sessionId);
866
751
  }
@@ -875,6 +760,37 @@ function getSessionList(workspace) {
875
760
  function removeSession(sessionId, workspace) {
876
761
  return core.deleteSession(sessionId, workspace);
877
762
  }
763
+ function classify(err) {
764
+ if (err instanceof core.SessionNotFoundError) return { status: 404, code: "session_not_found" };
765
+ if (err instanceof core.NoPlanError) return { status: 400, code: "no_plan" };
766
+ if (err instanceof core.AlreadyExecutingError) return { status: 409, code: "already_executing" };
767
+ if (err instanceof core.WorkspaceNotAProjectError) return { status: 400, code: "workspace_not_a_project" };
768
+ if (err instanceof core.WorkspaceNotFoundError) return { status: 400, code: "workspace_not_found" };
769
+ if (err instanceof core.PlannerTurnStoppedError) return { status: 409, code: "planner_turn_stopped" };
770
+ if (err instanceof core.PlannerTurnDiscardedError) return { status: 409, code: "planner_turn_discarded" };
771
+ if (err instanceof core.ConversationBusyError) return { status: 409, code: "conversation_busy" };
772
+ if (err instanceof core.ConversationEditError || err instanceof core.PlanEditError || err instanceof core.TaskControlError) {
773
+ return { status: 400, code: "refused" };
774
+ }
775
+ return void 0;
776
+ }
777
+ function refuse(c, status, message, code) {
778
+ const body = code ? { error: message, code } : { error: message };
779
+ return c.json(body, status);
780
+ }
781
+ function failure(c, err, label, { fallback = 500, message = "Internal error" } = {}) {
782
+ const classified = classify(err);
783
+ const status = classified?.status ?? fallback;
784
+ if (status >= 500) console.error(`[web] ${label} failed:`, err);
785
+ const body = {
786
+ error: err instanceof Error && err.message || message,
787
+ ...classified ? { code: classified.code } : {},
788
+ ...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {}
789
+ };
790
+ return c.json(body, status);
791
+ }
792
+
793
+ // server/routes/sessions.ts
878
794
  function sessionsRoute(pool2) {
879
795
  const router = new hono.Hono();
880
796
  router.get("/", (c) => {
@@ -886,20 +802,17 @@ function sessionsRoute(pool2) {
886
802
  const ws = c.req.query("workspace") || process.cwd();
887
803
  const id = c.req.param("id");
888
804
  const saved = core.loadSessionPlanState(id, ws);
889
- if (!saved) return c.json({ error: "Session not found" }, 404);
890
- return c.json({ meta: saved.meta, plan: pool2.getPlanState(id) ?? saved.plan });
805
+ if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
806
+ return c.json({ meta: saved.meta, plan: core.surfacePlanState(pool2.getPlanState(id) ?? saved.plan) });
891
807
  });
892
808
  router.post("/:id/load", (c) => {
893
809
  const ws = c.req.query("workspace") || process.cwd();
894
810
  try {
895
811
  const id = c.req.param("id");
896
812
  const plan = pool2.adoptSavedSession(id, ws);
897
- return c.json({ ok: true, plan, goal: pool2.getGoal(id) });
813
+ return c.json({ ok: true, plan: core.surfacePlan(plan), goal: pool2.getGoal(id) });
898
814
  } catch (err) {
899
- if (err instanceof core.WorkspaceNotFoundError) return c.json({ error: err.message }, 400);
900
- const message = err instanceof Error ? err.message : void 0;
901
- const status = message === "Session not found" ? 404 : 500;
902
- return c.json({ error: message ?? "Failed to load session" }, status);
815
+ return failure(c, err, "load session", { message: "Failed to load session" });
903
816
  }
904
817
  });
905
818
  router.get("/:id/tasks/:taskId/log", (c) => {
@@ -909,8 +822,8 @@ function sessionsRoute(pool2) {
909
822
  router.get("/:id/tasks/:taskId/log/:attempt", (c) => {
910
823
  const ws = c.req.query("workspace") || process.cwd();
911
824
  const attempt = Number(c.req.param("attempt"));
912
- if (!Number.isInteger(attempt) || attempt < 1) return c.json({ error: "attempt must be a positive integer" }, 400);
913
- return c.json({ attempt, events: core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt) });
825
+ if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
826
+ return c.json({ attempt, events: core.taskLogForSurface(core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt)) });
914
827
  });
915
828
  router.delete("/:id", (c) => {
916
829
  const ws = c.req.query("workspace") || process.cwd();
@@ -923,22 +836,7 @@ function sessionsRoute(pool2) {
923
836
  });
924
837
  return router;
925
838
  }
926
- function editFailure(c, err) {
927
- const e = err;
928
- if (e.message === "Session not found") return c.json({ error: e.message }, 404);
929
- if (e instanceof core.PlanEditError || e instanceof core.TaskControlError) return c.json({ error: e.message }, 400);
930
- console.error("[plans] task edit failed:", err);
931
- return c.json({ error: e.message || "Internal error" }, 500);
932
- }
933
839
  var NO_CHECKPOINT = "The task is not waiting at a checkpoint \u2014 it was answered already, withdrawn, or has no runner left to hear it.";
934
- function conversationFailure(c, err) {
935
- const e = err;
936
- if (e.message === "Session not found") return c.json({ error: e.message }, 404);
937
- if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
938
- if (e instanceof core.ConversationEditError) return c.json({ error: e.message }, 400);
939
- console.error("[plans] conversation edit failed:", err);
940
- return c.json({ error: e.message || "Internal error" }, 500);
941
- }
942
840
  function plansRoute(pool2) {
943
841
  const router = new hono.Hono();
944
842
  router.post("/:sessionId/generate", async (c) => {
@@ -950,14 +848,10 @@ function plansRoute(pool2) {
950
848
  const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) : pool2.getRunnerState().enabledRunners;
951
849
  const plan = await pool2.generatePlan(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
952
850
  const { models, modelsByRunner } = await pool2.getProviderModels();
953
- return c.json({ plan, models, modelsByRunner });
851
+ const notes = (pool2.session(c.req.param("sessionId")).planState?.conversationHistory ?? []).filter((e) => e.kind === "system").map((e) => e.content);
852
+ return c.json({ plan: core.surfacePlanState(plan), models, modelsByRunner, ...notes.length > 0 ? { notes } : {} });
954
853
  } catch (err) {
955
- if (err instanceof core.WorkspaceNotFoundError) return c.json({ error: err.message }, 400);
956
- if (err instanceof core.WorkspaceNotAProjectError) {
957
- return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
958
- }
959
- console.error("[plans] generate failed:", err);
960
- return c.json({ error: err instanceof Error ? err.message : "Plan generation failed" }, 500);
854
+ return failure(c, err, "generate", { message: "Plan generation failed" });
961
855
  }
962
856
  });
963
857
  router.post("/:sessionId/execute", async (c) => {
@@ -965,9 +859,7 @@ function plansRoute(pool2) {
965
859
  await pool2.session(c.req.param("sessionId")).executePlan();
966
860
  return c.json({ status: "running" });
967
861
  } catch (err) {
968
- const e = err;
969
- const status = e.message === "Session not found" ? 404 : e.message === "No plan to execute" ? 400 : e.message === "Session already executing" ? 409 : 500;
970
- return c.json({ error: e.message }, status);
862
+ return failure(c, err, "execute");
971
863
  }
972
864
  });
973
865
  router.post("/:sessionId/stop", (c) => {
@@ -983,7 +875,7 @@ function plansRoute(pool2) {
983
875
  await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
984
876
  return c.json({ ok: true });
985
877
  } catch (err) {
986
- return c.json({ error: err instanceof Error ? err.message : "Not found" }, 404);
878
+ return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
987
879
  }
988
880
  });
989
881
  router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
@@ -991,7 +883,7 @@ function plansRoute(pool2) {
991
883
  await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
992
884
  return c.json({ ok: true });
993
885
  } catch (err) {
994
- return c.json({ error: err instanceof Error ? err.message : "Not found" }, 404);
886
+ return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
995
887
  }
996
888
  });
997
889
  for (const [segment, run2] of [
@@ -1005,39 +897,29 @@ function plansRoute(pool2) {
1005
897
  await run2(pool2.session(c.req.param("sessionId")), c.req.param("taskId"));
1006
898
  return c.json({ ok: true });
1007
899
  } catch (err) {
1008
- const status = err.message === "Session not found" ? 404 : 500;
1009
- return c.json({ error: err.message }, status);
900
+ return failure(c, err, segment);
1010
901
  }
1011
902
  });
1012
903
  }
1013
- router.post("/:sessionId/tasks/:taskId/messages", async (c) => {
1014
- try {
1015
- const body = await c.req.json().catch(() => ({}));
1016
- const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
1017
- if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
1018
- const id = pool2.session(c.req.param("sessionId")).sendTaskMessage(c.req.param("taskId"), text);
1019
- return c.json({ id });
1020
- } catch (err) {
1021
- return editFailure(c, err);
1022
- }
1023
- });
1024
- router.post("/:sessionId/tasks/:taskId/messages/now", async (c) => {
904
+ const messageHandler = (send) => async (c) => {
1025
905
  try {
1026
906
  const body = await c.req.json().catch(() => ({}));
1027
907
  const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
1028
908
  if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
1029
- const id = pool2.session(c.req.param("sessionId")).forceSendTaskMessage(c.req.param("taskId"), text);
909
+ const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
1030
910
  return c.json({ id });
1031
911
  } catch (err) {
1032
- return editFailure(c, err);
912
+ return failure(c, err, "task edit");
1033
913
  }
1034
- });
914
+ };
915
+ router.post("/:sessionId/tasks/:taskId/messages", messageHandler((session, taskId, text) => session.sendTaskMessage(taskId, text)));
916
+ router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
1035
917
  router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
1036
918
  try {
1037
919
  const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1038
920
  return c.json({ sent });
1039
921
  } catch (err) {
1040
- return editFailure(c, err);
922
+ return failure(c, err, "task edit");
1041
923
  }
1042
924
  });
1043
925
  router.post("/:sessionId/tasks/:taskId/continue", async (c) => {
@@ -1048,7 +930,7 @@ function plansRoute(pool2) {
1048
930
  await pool2.session(c.req.param("sessionId")).continueTask(c.req.param("taskId"), text);
1049
931
  return c.json({ ok: true });
1050
932
  } catch (err) {
1051
- return editFailure(c, err);
933
+ return failure(c, err, "task edit");
1052
934
  }
1053
935
  });
1054
936
  router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
@@ -1056,7 +938,7 @@ function plansRoute(pool2) {
1056
938
  const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1057
939
  return c.json({ removed });
1058
940
  } catch (err) {
1059
- return editFailure(c, err);
941
+ return failure(c, err, "task edit");
1060
942
  }
1061
943
  });
1062
944
  router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
@@ -1064,18 +946,18 @@ function plansRoute(pool2) {
1064
946
  await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
1065
947
  return c.json({ ok: true });
1066
948
  } catch (err) {
1067
- return editFailure(c, err);
949
+ return failure(c, err, "task edit");
1068
950
  }
1069
951
  });
1070
952
  router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
1071
953
  try {
1072
954
  const session = pool2.session(c.req.param("sessionId"));
1073
955
  const taskId = c.req.param("taskId");
1074
- if (!session.awaitsCheckpoint(taskId)) return c.json({ error: NO_CHECKPOINT }, 409);
956
+ if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1075
957
  session.approveCheckpoint(taskId);
1076
958
  return c.json({ ok: true });
1077
959
  } catch (err) {
1078
- return editFailure(c, err);
960
+ return failure(c, err, "task edit");
1079
961
  }
1080
962
  });
1081
963
  router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
@@ -1084,11 +966,11 @@ function plansRoute(pool2) {
1084
966
  const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
1085
967
  const session = pool2.session(c.req.param("sessionId"));
1086
968
  const taskId = c.req.param("taskId");
1087
- if (!session.awaitsCheckpoint(taskId)) return c.json({ error: NO_CHECKPOINT }, 409);
969
+ if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1088
970
  session.rejectCheckpoint(taskId, reason || void 0);
1089
971
  return c.json({ ok: true });
1090
972
  } catch (err) {
1091
- return editFailure(c, err);
973
+ return failure(c, err, "task edit");
1092
974
  }
1093
975
  });
1094
976
  router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
@@ -1101,21 +983,21 @@ function plansRoute(pool2) {
1101
983
  });
1102
984
  return c.json({ mergeGate });
1103
985
  } catch (err) {
1104
- return editFailure(c, err);
986
+ return failure(c, err, "task edit");
1105
987
  }
1106
988
  });
1107
989
  router.get("/:sessionId/isolation/diff", async (c) => {
1108
990
  try {
1109
991
  return c.json({ diff: await pool2.session(c.req.param("sessionId")).reviewRunDiff() });
1110
992
  } catch (err) {
1111
- return editFailure(c, err);
993
+ return failure(c, err, "task edit");
1112
994
  }
1113
995
  });
1114
996
  router.post("/:sessionId/isolation/merge", async (c) => {
1115
997
  try {
1116
998
  return c.json(await pool2.session(c.req.param("sessionId")).mergeRun());
1117
999
  } catch (err) {
1118
- return editFailure(c, err);
1000
+ return failure(c, err, "task edit");
1119
1001
  }
1120
1002
  });
1121
1003
  for (const [segment, run2] of [
@@ -1129,16 +1011,16 @@ function plansRoute(pool2) {
1129
1011
  await run2(pool2.session(c.req.param("sessionId")));
1130
1012
  return c.json({ ok: true });
1131
1013
  } catch (err) {
1132
- return editFailure(c, err);
1014
+ return failure(c, err, "task edit");
1133
1015
  }
1134
1016
  });
1135
1017
  }
1136
1018
  router.post("/:sessionId/tasks/:taskId/resolve-conflict", async (c) => {
1137
1019
  try {
1138
1020
  const plan = await pool2.session(c.req.param("sessionId")).resolveConflictAsTask(c.req.param("taskId"));
1139
- return c.json({ plan });
1021
+ return c.json({ plan: plan && core.surfacePlan(plan) });
1140
1022
  } catch (err) {
1141
- return editFailure(c, err);
1023
+ return failure(c, err, "task edit");
1142
1024
  }
1143
1025
  });
1144
1026
  router.put("/:sessionId/tasks/:taskId", async (c) => {
@@ -1154,43 +1036,41 @@ function plansRoute(pool2) {
1154
1036
  try {
1155
1037
  result = await session.setTaskDependencies(taskId, dependencies.map(String));
1156
1038
  } catch (err) {
1157
- return c.json({ error: err.message }, 400);
1039
+ return failure(c, err, "task dependencies", { fallback: 400 });
1158
1040
  }
1159
1041
  }
1160
1042
  if (Object.keys(rest).length > 0 || !hasRunner && !hasDeps) result = await session.updateTask(taskId, rest);
1161
- if (!result) return c.json({ error: "Task not found" }, 404);
1043
+ if (!result) return refuse(c, 404, "Task not found", "task_not_found");
1162
1044
  return c.json({ ok: true });
1163
1045
  } catch (err) {
1164
- return editFailure(c, err);
1046
+ return failure(c, err, "task edit");
1165
1047
  }
1166
1048
  });
1167
1049
  router.delete("/:sessionId/tasks/:taskId", async (c) => {
1168
1050
  try {
1169
1051
  const result = await pool2.session(c.req.param("sessionId")).removeTask(c.req.param("taskId"));
1170
- if (!result) return c.json({ error: "Task not found" }, 404);
1052
+ if (!result) return refuse(c, 404, "Task not found", "task_not_found");
1171
1053
  return c.json({ ok: true });
1172
1054
  } catch (err) {
1173
- return editFailure(c, err);
1055
+ return failure(c, err, "task edit");
1174
1056
  }
1175
1057
  });
1176
1058
  router.post("/:sessionId/tasks", async (c) => {
1177
1059
  try {
1178
1060
  const body = await c.req.json();
1179
1061
  const result = await pool2.session(c.req.param("sessionId")).addTask(body);
1180
- if (!result) return c.json({ error: "Session not found" }, 404);
1062
+ if (!result) return refuse(c, 404, "Session not found", "session_not_found");
1181
1063
  return c.json({ ok: true });
1182
1064
  } catch (err) {
1183
- return editFailure(c, err);
1065
+ return failure(c, err, "task edit");
1184
1066
  }
1185
1067
  });
1186
1068
  router.post("/:sessionId/review/approve", async (c) => {
1187
1069
  try {
1188
1070
  const plan = await pool2.session(c.req.param("sessionId")).approveReview();
1189
- return c.json({ plan });
1071
+ return c.json({ plan: core.surfacePlan(plan) });
1190
1072
  } catch (err) {
1191
- const e = err;
1192
- const status = e.message === "Session not found" ? 404 : e.message === "No plan to review" ? 400 : 500;
1193
- return c.json({ error: e.message }, status);
1073
+ return failure(c, err, "review approve");
1194
1074
  }
1195
1075
  });
1196
1076
  router.post("/:sessionId/tasks/merge", async (c) => {
@@ -1200,21 +1080,17 @@ function plansRoute(pool2) {
1200
1080
  return c.json({ error: "taskIds array with at least two ids is required" }, 400);
1201
1081
  }
1202
1082
  const plan = await pool2.session(c.req.param("sessionId")).requestMerge(taskIds);
1203
- return c.json({ plan });
1083
+ return c.json({ plan: core.surfacePlan(plan) });
1204
1084
  } catch (err) {
1205
- const e = err;
1206
- const status = e.message === "Session not found" ? 404 : 400;
1207
- return c.json({ error: e.message }, status);
1085
+ return failure(c, err, "merge", { fallback: 400 });
1208
1086
  }
1209
1087
  });
1210
1088
  router.post("/:sessionId/tasks/:taskId/split", async (c) => {
1211
1089
  try {
1212
1090
  const plan = await pool2.session(c.req.param("sessionId")).requestSplit(c.req.param("taskId"));
1213
- return c.json({ plan });
1091
+ return c.json({ plan: core.surfacePlan(plan) });
1214
1092
  } catch (err) {
1215
- const e = err;
1216
- const status = e.message === "Session not found" ? 404 : 400;
1217
- return c.json({ error: e.message }, status);
1093
+ return failure(c, err, "split", { fallback: 400 });
1218
1094
  }
1219
1095
  });
1220
1096
  router.post("/:sessionId/converse/start", async (c) => {
@@ -1224,13 +1100,9 @@ function plansRoute(pool2) {
1224
1100
  const ws = workspace || c.req.query("workspace") || process.cwd();
1225
1101
  const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : pool2.getRunnerState().enabledRunners;
1226
1102
  const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
1227
- return c.json({ plan });
1103
+ return c.json({ plan: core.surfacePlan(plan) });
1228
1104
  } catch (err) {
1229
- if (err instanceof core.WorkspaceNotFoundError) return c.json({ error: err.message }, 400);
1230
- if (err instanceof core.WorkspaceNotAProjectError) {
1231
- return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
1232
- }
1233
- return c.json({ error: err instanceof Error ? err.message : "Planning failed" }, 500);
1105
+ return failure(c, err, "converse start", { message: "Planning failed" });
1234
1106
  }
1235
1107
  });
1236
1108
  router.post("/:sessionId/converse/message", async (c) => {
@@ -1238,57 +1110,47 @@ function plansRoute(pool2) {
1238
1110
  const { message } = await c.req.json();
1239
1111
  if (!message) return c.json({ error: "message is required" }, 400);
1240
1112
  const plan = await pool2.continuePlanning(c.req.param("sessionId"), message);
1241
- return c.json({ plan });
1113
+ return c.json({ plan: core.surfacePlan(plan) });
1242
1114
  } catch (err) {
1243
- const e = err;
1244
- if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
1245
- const status = e.message === "Session not found" ? 404 : 500;
1246
- return c.json({ error: e.message }, status);
1115
+ return failure(c, err, "conversation message");
1247
1116
  }
1248
1117
  });
1249
1118
  router.post("/:sessionId/conversation/fork", (c) => {
1250
1119
  try {
1251
- return c.json(pool2.forkConversation(c.req.param("sessionId")));
1120
+ const fork = pool2.forkConversation(c.req.param("sessionId"));
1121
+ return c.json({ ...fork, plan: core.surfacePlan(fork.plan) });
1252
1122
  } catch (err) {
1253
- return conversationFailure(c, err);
1123
+ return failure(c, err, "conversation edit");
1254
1124
  }
1255
1125
  });
1256
1126
  router.get("/:sessionId/conversation/rewind-targets", (c) => {
1257
1127
  try {
1258
1128
  return c.json({ targets: pool2.session(c.req.param("sessionId")).rewindTargets() });
1259
1129
  } catch (err) {
1260
- return conversationFailure(c, err);
1130
+ return failure(c, err, "conversation edit");
1261
1131
  }
1262
1132
  });
1263
1133
  router.post("/:sessionId/conversation/rewind", async (c) => {
1264
1134
  try {
1265
1135
  const { index } = await c.req.json();
1266
1136
  if (!Number.isInteger(index) || index < 0) return c.json({ error: "index must be a non-negative integer" }, 400);
1267
- return c.json(pool2.rewindConversation(c.req.param("sessionId"), index));
1137
+ const rewound = pool2.rewindConversation(c.req.param("sessionId"), index);
1138
+ return c.json({ ...rewound, plan: core.surfacePlan(rewound.plan) });
1268
1139
  } catch (err) {
1269
- return conversationFailure(c, err);
1140
+ return failure(c, err, "conversation edit");
1270
1141
  }
1271
1142
  });
1272
1143
  router.post("/:sessionId/conversation/compact", async (c) => {
1273
1144
  try {
1274
- return c.json(await pool2.compactConversation(c.req.param("sessionId")));
1275
- } catch (err) {
1276
- return conversationFailure(c, err);
1277
- }
1278
- });
1279
- router.post("/:sessionId/process-queued", async (c) => {
1280
- try {
1281
- await pool2.session(c.req.param("sessionId")).processQueuedMessages();
1282
- return c.json({ ok: true });
1145
+ const compacted = await pool2.compactConversation(c.req.param("sessionId"));
1146
+ return c.json({ ...compacted, plan: core.surfacePlan(compacted.plan) });
1283
1147
  } catch (err) {
1284
- const e = err;
1285
- const status = e.message === "Session not found" ? 404 : 500;
1286
- return c.json({ error: e.message }, status);
1148
+ return failure(c, err, "conversation edit");
1287
1149
  }
1288
1150
  });
1289
1151
  router.get("/:sessionId/prd", (c) => {
1290
1152
  const plan = pool2.getPlan(c.req.param("sessionId"));
1291
- if (!plan?.prdMarkdown) return c.json({ error: "No PRD found" }, 404);
1153
+ if (!plan?.prdMarkdown) return refuse(c, 404, "No PRD found");
1292
1154
  return c.json({ prdMarkdown: plan.prdMarkdown });
1293
1155
  });
1294
1156
  return router;
@@ -1339,7 +1201,7 @@ function approvalsRoute(pool2) {
1339
1201
  const router = new hono.Hono();
1340
1202
  router.get("/:sessionId", (c) => {
1341
1203
  const sessionId = c.req.param("sessionId");
1342
- if (!pool2.hasSession(sessionId)) return c.json({ error: `Unknown session: ${sessionId}` }, 404);
1204
+ if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
1343
1205
  return c.json({
1344
1206
  pending: pool2.outstandingApprovals(sessionId),
1345
1207
  approvedScopes: pool2.approvedScopes(sessionId)
@@ -1348,10 +1210,10 @@ function approvalsRoute(pool2) {
1348
1210
  router.post("/:sessionId/:approvalId", async (c) => {
1349
1211
  const sessionId = c.req.param("sessionId");
1350
1212
  const approvalId = c.req.param("approvalId");
1351
- if (!pool2.hasSession(sessionId)) return c.json({ error: `Unknown session: ${sessionId}` }, 404);
1213
+ if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
1352
1214
  const answer = answerOf(await c.req.json().catch(() => null));
1353
1215
  if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
1354
- return c.json({ error: "Approval is no longer outstanding \u2014 it timed out or was already answered." }, 409);
1216
+ return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
1355
1217
  }
1356
1218
  const { decision } = core.toApprovalDecision(answer);
1357
1219
  return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
@@ -1361,64 +1223,28 @@ function approvalsRoute(pool2) {
1361
1223
  function settingsRoute(pool2) {
1362
1224
  const router = new hono.Hono();
1363
1225
  router.get("/", (c) => {
1364
- return c.json(pool2.getSettings());
1226
+ const settings = pool2.getSettings();
1227
+ return c.json(settings);
1365
1228
  });
1366
1229
  router.patch("/", async (c) => {
1367
1230
  const body = await c.req.json();
1368
1231
  if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
1369
- return c.json({ error: "At least one setting field is required" }, 400);
1232
+ return refuse(c, 400, "At least one setting field is required");
1370
1233
  }
1371
1234
  const result = pool2.updateSettings(body);
1372
1235
  return c.json(result);
1373
1236
  });
1374
1237
  return router;
1375
1238
  }
1376
- var COMMANDS = [
1377
- { name: "tdd", description: "Toggle Test-Driven Development mode (on|off|status)" },
1378
- { name: "verify", description: "Toggle verification mode \u2014 adds a final evidence-based verification task that runs the full suite (on|off|status)" },
1379
- { name: "transport", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run (terminal|structured|status)" }
1380
- ];
1381
- function commandsRoute(pool2) {
1239
+ var COMMANDS = [];
1240
+ function commandsRoute() {
1382
1241
  const router = new hono.Hono();
1383
1242
  router.get("/", (c) => {
1384
1243
  return c.json({ commands: COMMANDS });
1385
1244
  });
1386
- router.post("/:name", async (c) => {
1245
+ router.post("/:name", (c) => {
1387
1246
  const name = c.req.param("name");
1388
- const body = await c.req.json().catch(() => ({}));
1389
- const args2 = body?.args || {};
1390
- const command = COMMANDS.find((cmd) => cmd.name === name);
1391
- if (!command) {
1392
- return c.json({ error: `Unknown command: ${name}` }, 404);
1393
- }
1394
- if (name === "tdd") {
1395
- const action = args2.action || "status";
1396
- if (action === "on") {
1397
- pool2.updateSettings({ tdd: { enabled: true } });
1398
- } else if (action === "off") {
1399
- pool2.updateSettings({ tdd: { enabled: false } });
1400
- }
1401
- return c.json({ ok: true, settings: pool2.getSettings() });
1402
- }
1403
- if (name === "verify") {
1404
- const action = args2.action || "status";
1405
- if (action === "on") {
1406
- pool2.updateSettings({ verification: { enabled: true } });
1407
- } else if (action === "off") {
1408
- pool2.updateSettings({ verification: { enabled: false } });
1409
- }
1410
- return c.json({ ok: true, settings: pool2.getSettings() });
1411
- }
1412
- if (name === "transport") {
1413
- const action = args2.action || "status";
1414
- if (action === "terminal" || action === "structured") {
1415
- pool2.updateSettings({ runnerTransport: action });
1416
- } else if (action !== "status") {
1417
- return c.json({ error: `Unknown transport: ${action} (terminal|structured)` }, 400);
1418
- }
1419
- return c.json({ ok: true, settings: pool2.getSettings() });
1420
- }
1421
- return c.json({ error: `Unknown command: ${name}` }, 404);
1247
+ return refuse(c, 404, `Unknown command: ${name}`);
1422
1248
  });
1423
1249
  return router;
1424
1250
  }
@@ -1524,7 +1350,7 @@ function createApp(pool2, admission2) {
1524
1350
  app2.route("/api/runners", runnersRoute(pool2));
1525
1351
  app2.route("/api/models", modelsRoute(pool2));
1526
1352
  app2.route("/api/settings", settingsRoute(pool2));
1527
- app2.route("/api/commands", commandsRoute(pool2));
1353
+ app2.route("/api/commands", commandsRoute());
1528
1354
  app2.route("/api/workspaces", workspacesRoute());
1529
1355
  app2.route("/api/approvals", approvalsRoute(pool2));
1530
1356
  return app2;
@@ -1612,10 +1438,9 @@ if (portIdx !== -1 && args[portIdx + 1]) {
1612
1438
  }
1613
1439
  var watchParentIdx = args.indexOf("--watch-parent");
1614
1440
  var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
1615
- var { runner: tmuxRunner, advice: terminalAdvice } = startTerminalHost(port);
1616
1441
  var { token, file: tokenFile } = core.mintDaemonToken(port);
1617
1442
  var admission = { port, token, tokenFile };
1618
- var pool = new OrchestratorPool({ runner: tmuxRunner, terminalAdvice, structuredRunner: new core.StructuredRunner() });
1443
+ var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
1619
1444
  var app = createApp(pool, admission);
1620
1445
  pool.getProviderModels().catch((err) => {
1621
1446
  console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
@@ -1626,14 +1451,15 @@ attachWsHandler(server, pool, admission);
1626
1451
  server.listen(port, "127.0.0.1", () => {
1627
1452
  console.error(`[web] Ordewell API server: http://localhost:${port}`);
1628
1453
  });
1629
- async function shutdown() {
1454
+ function shutdown() {
1630
1455
  core.clearDaemonToken(port);
1631
1456
  pool.destroyAll();
1632
- if (tmuxRunner) await tmuxRunner.killSession().catch(() => {
1633
- });
1634
1457
  server.close();
1635
1458
  process.exit(0);
1636
1459
  }
1460
+ process.on("unhandledRejection", (reason) => {
1461
+ console.error(`[web] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
1462
+ });
1637
1463
  process.on("SIGINT", () => {
1638
1464
  shutdown();
1639
1465
  });