clisbot 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +392 -89
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -54623,6 +54623,12 @@ function parseCliArgs(argv) {
54623
54623
  args: args.slice(1)
54624
54624
  };
54625
54625
  }
54626
+ if (command === "runner") {
54627
+ return {
54628
+ name: "runner",
54629
+ args: args.slice(1)
54630
+ };
54631
+ }
54626
54632
  if (command === "pairing") {
54627
54633
  return {
54628
54634
  name: "pairing",
@@ -54685,6 +54691,7 @@ function renderCliHelp() {
54685
54691
  " clisbot message <subcommand>",
54686
54692
  " clisbot agents <subcommand>",
54687
54693
  " clisbot auth <subcommand>",
54694
+ " clisbot runner <subcommand>",
54688
54695
  " clisbot pairing <subcommand>",
54689
54696
  " clisbot init [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54690
54697
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
@@ -54728,6 +54735,8 @@ function renderCliHelp() {
54728
54735
  " agents Manage configured agents and top-level bindings.",
54729
54736
  " See `clisbot agents --help` for focused add/bootstrap/binding help.",
54730
54737
  " auth Manage app and agent auth roles, principals, and permissions in config. See `clisbot auth --help`.",
54738
+ " runner Validate and inspect runner-facing operator contracts such as `runner smoke`.",
54739
+ " See `clisbot runner --help` for the current smoke surface.",
54731
54740
  " pairing Run the pairing control CLI. See `clisbot pairing --help`.",
54732
54741
  ` init Seed ${configPath} and optionally create the first agent without starting clisbot.`,
54733
54742
  " See `clisbot init --help` for bootstrap-focused flags and examples.",
@@ -64255,6 +64264,8 @@ function resolveAgentTargetInternal(loadedConfig, target) {
64255
64264
  }
64256
64265
 
64257
64266
  // src/agents/job-queue.ts
64267
+ var QUEUE_PENDING_POLL_INTERVAL_MS = 25;
64268
+
64258
64269
  class ClearedQueuedTaskError extends Error {
64259
64270
  constructor() {
64260
64271
  super("Queued task was cleared before execution.");
@@ -64282,6 +64293,7 @@ class AgentJobQueue {
64282
64293
  text: options.text,
64283
64294
  createdAt: Date.now(),
64284
64295
  status: "pending",
64296
+ canStart: options.canStart,
64285
64297
  task,
64286
64298
  resolve,
64287
64299
  reject,
@@ -64349,6 +64361,10 @@ class AgentJobQueue {
64349
64361
  if (!nextEntry) {
64350
64362
  break;
64351
64363
  }
64364
+ if (nextEntry.canStart && !await nextEntry.canStart()) {
64365
+ await sleep(QUEUE_PENDING_POLL_INTERVAL_MS);
64366
+ continue;
64367
+ }
64352
64368
  nextEntry.status = "running";
64353
64369
  try {
64354
64370
  nextEntry.resolve(await nextEntry.task());
@@ -64609,12 +64625,22 @@ function isInterruptStatusLine(line) {
64609
64625
  }
64610
64626
  return CODEX_WORKING_STATUS_PATTERN.test(trimmed) || CODEX_INTERRUPT_FOOTER_PATTERN.test(trimmed);
64611
64627
  }
64628
+ function isActiveTimerStatusLine(line) {
64629
+ const trimmed = line.trim();
64630
+ if (!trimmed) {
64631
+ return false;
64632
+ }
64633
+ return isInterruptStatusLine(trimmed) || GEMINI_THINKING_STATUS_PATTERN.test(trimmed) || CLAUDE_TIMER_FOOTER_PATTERN.test(trimmed);
64634
+ }
64612
64635
  function isTimerDrivenStatusLine(line) {
64613
64636
  const trimmed = line.trim();
64614
64637
  if (!trimmed) {
64615
64638
  return false;
64616
64639
  }
64617
- return isInterruptStatusLine(trimmed) || GEMINI_THINKING_STATUS_PATTERN.test(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed) || CLAUDE_TIMER_FOOTER_PATTERN.test(trimmed);
64640
+ return isActiveTimerStatusLine(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed);
64641
+ }
64642
+ function hasActiveTimerStatus(snapshot) {
64643
+ return splitNormalizedLines(snapshot).some((line) => isActiveTimerStatusLine(line));
64618
64644
  }
64619
64645
  function shouldDropCodexChromeLine(line) {
64620
64646
  const trimmed = line.trim();
@@ -66145,8 +66171,10 @@ var FIRST_OUTPUT_POLL_INTERVAL_MS = 250;
66145
66171
  async function monitorTmuxRun(params) {
66146
66172
  let previousSnapshot = params.initialSnapshot;
66147
66173
  let previousRunningSnapshot = "";
66148
- let lastActivityAt = params.startedAt;
66174
+ let lastPaneChangeAt = params.startedAt;
66149
66175
  let sawActivity = false;
66176
+ let sawPaneChange = false;
66177
+ let sawPromptSubmission = Boolean(params.prompt);
66150
66178
  let detachedNotified = params.detachedAlready;
66151
66179
  let firstMeaningfulDeltaLogged = false;
66152
66180
  let noOutputThresholdLogged = false;
@@ -66162,6 +66190,8 @@ async function monitorTmuxRun(params) {
66162
66190
  promptSubmitDelayMs: params.promptSubmitDelayMs,
66163
66191
  timingContext: params.timingContext
66164
66192
  });
66193
+ sawPromptSubmission = true;
66194
+ lastPaneChangeAt = Date.now();
66165
66195
  await params.onPromptSubmitted?.();
66166
66196
  logLatencyDebug("tmux-submit-complete", params.timingContext, {
66167
66197
  sessionName: params.sessionName,
@@ -66173,11 +66203,16 @@ async function monitorTmuxRun(params) {
66173
66203
  await sleep(sawActivity ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
66174
66204
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
66175
66205
  const now = Date.now();
66206
+ const paneChanged = snapshot !== previousSnapshot;
66207
+ if (paneChanged) {
66208
+ lastPaneChangeAt = now;
66209
+ sawPaneChange = true;
66210
+ }
66211
+ const hasActiveTimer = hasActiveTimerStatus(snapshot);
66176
66212
  const runningSnapshot = params.initialSnapshot ? deriveRunningInteractionText(params.initialSnapshot, snapshot) : deriveRunningInteractionSnapshot(snapshot);
66177
66213
  previousSnapshot = snapshot;
66178
66214
  if (runningSnapshot && runningSnapshot !== previousRunningSnapshot) {
66179
66215
  previousRunningSnapshot = runningSnapshot;
66180
- lastActivityAt = now;
66181
66216
  sawActivity = true;
66182
66217
  if (!firstMeaningfulDeltaLogged) {
66183
66218
  firstMeaningfulDeltaLogged = true;
@@ -66200,7 +66235,7 @@ async function monitorTmuxRun(params) {
66200
66235
  initialSnapshot: params.initialSnapshot
66201
66236
  });
66202
66237
  }
66203
- if (sawActivity && now - lastActivityAt >= params.idleTimeoutMs) {
66238
+ if (!hasActiveTimer && (sawActivity || sawPaneChange || sawPromptSubmission) && now - lastPaneChangeAt >= params.idleTimeoutMs) {
66204
66239
  await params.onCompleted({
66205
66240
  snapshot: deriveInteractionText(params.initialSnapshot, previousSnapshot),
66206
66241
  fullSnapshot: previousSnapshot,
@@ -66221,9 +66256,20 @@ async function monitorTmuxRun(params) {
66221
66256
  // src/agents/session-service.ts
66222
66257
  var OBSERVER_RETRYABLE_FAILURE_LIMIT = 3;
66223
66258
  var DETACHED_OBSERVER_INTERVAL_MS = 5 * 60000;
66259
+ var TMUX_MISSING_SESSION_PATTERN3 = /(?:can't find session:|no server running on )/i;
66260
+ var TMUX_SERVER_UNAVAILABLE_PATTERN3 = /(?:No such file or directory|error connecting to|failed to connect to server)/i;
66224
66261
  function formatObserverError(error) {
66225
66262
  return error instanceof Error ? error.stack ?? error.message : String(error);
66226
66263
  }
66264
+ function isMissingTmuxSessionError2(error) {
66265
+ return error instanceof Error && TMUX_MISSING_SESSION_PATTERN3.test(error.message);
66266
+ }
66267
+ function isTmuxServerUnavailableError2(error) {
66268
+ return error instanceof Error && TMUX_SERVER_UNAVAILABLE_PATTERN3.test(error.message);
66269
+ }
66270
+ function isBootstrapSessionLostError2(error) {
66271
+ return error instanceof Error && /tmux session disappeared before startup finished|tmux server became unavailable before startup finished/i.test(error.message);
66272
+ }
66227
66273
  function listObserverErrorCodes(error) {
66228
66274
  const codes = new Set;
66229
66275
  const visit = (value) => {
@@ -66295,6 +66341,7 @@ class SessionService {
66295
66341
  runnerSessions;
66296
66342
  resolveTarget;
66297
66343
  activeRuns = new Map;
66344
+ nextRunId = 1;
66298
66345
  stopping = false;
66299
66346
  constructor(tmux, sessionState, runnerSessions, resolveTarget) {
66300
66347
  this.tmux = tmux;
@@ -66308,42 +66355,10 @@ class SessionService {
66308
66355
  if (!entry.runtime || entry.runtime.state === "idle") {
66309
66356
  continue;
66310
66357
  }
66311
- const resolved = this.resolveTarget({
66358
+ await this.reconcilePersistedActiveRun({
66312
66359
  agentId: entry.agentId,
66313
66360
  sessionKey: entry.sessionKey
66314
66361
  });
66315
- if (!await this.tmux.hasSession(resolved.sessionName)) {
66316
- await this.sessionState.setSessionRuntime(resolved, {
66317
- state: "idle"
66318
- });
66319
- continue;
66320
- }
66321
- const fullSnapshot = normalizePaneText(await this.tmux.capturePane(resolved.sessionName, resolved.stream.captureLines));
66322
- const initialResult = createDeferred();
66323
- const update = this.createRunUpdate({
66324
- resolved,
66325
- status: entry.runtime.state === "detached" ? "detached" : "running",
66326
- snapshot: deriveInteractionText("", fullSnapshot),
66327
- fullSnapshot,
66328
- initialSnapshot: "",
66329
- note: entry.runtime.state === "detached" ? this.buildDetachedNote(resolved) : undefined
66330
- });
66331
- this.activeRuns.set(resolved.sessionKey, {
66332
- resolved,
66333
- observers: new Map,
66334
- observerFailures: new Map,
66335
- initialResult,
66336
- latestUpdate: update,
66337
- steeringReady: true,
66338
- startedAt: entry.runtime.startedAt ?? Date.now()
66339
- });
66340
- this.startRunMonitor(resolved.sessionKey, {
66341
- prompt: undefined,
66342
- initialSnapshot: fullSnapshot,
66343
- startedAt: entry.runtime.startedAt ?? Date.now(),
66344
- detachedAlready: entry.runtime.state === "detached",
66345
- timingContext: undefined
66346
- });
66347
66362
  }
66348
66363
  }
66349
66364
  async executePrompt(target, prompt, observer, options = {}) {
@@ -66354,21 +66369,15 @@ class SessionService {
66354
66369
  if (existingActiveRun) {
66355
66370
  throw new ActiveRunInProgressError(existingActiveRun.latestUpdate);
66356
66371
  }
66357
- const existingEntry = await this.sessionState.getEntry(target.sessionKey);
66358
- if (existingEntry?.runtime?.state && existingEntry.runtime.state !== "idle") {
66359
- const resolvedExisting = this.resolveTarget(target);
66360
- throw new ActiveRunInProgressError(this.createRunUpdate({
66361
- resolved: resolvedExisting,
66362
- status: existingEntry.runtime.state === "detached" ? "detached" : "running",
66363
- snapshot: "",
66364
- fullSnapshot: "",
66365
- initialSnapshot: "",
66366
- note: existingEntry.runtime.state === "detached" ? this.buildDetachedNote(resolvedExisting) : "This session already has an active run. Use `/attach`, `/watch every 30s`, or `/stop` before sending a new prompt."
66367
- }));
66372
+ const reconciledRun = await this.reconcilePersistedActiveRun(target);
66373
+ if (reconciledRun) {
66374
+ throw new ActiveRunInProgressError(reconciledRun.latestUpdate);
66368
66375
  }
66369
66376
  const initialResult = createDeferred();
66370
66377
  const provisionalResolved = this.resolveTarget(target);
66378
+ const runId = this.allocateRunId();
66371
66379
  this.activeRuns.set(provisionalResolved.sessionKey, {
66380
+ runId,
66372
66381
  resolved: provisionalResolved,
66373
66382
  observers: new Map([[observer.id, { ...observer }]]),
66374
66383
  observerFailures: new Map,
@@ -66416,6 +66425,7 @@ class SessionService {
66416
66425
  startedAt
66417
66426
  });
66418
66427
  this.startRunMonitor(resolved.sessionKey, {
66428
+ runId,
66419
66429
  prompt,
66420
66430
  initialSnapshot,
66421
66431
  startedAt,
@@ -66424,12 +66434,12 @@ class SessionService {
66424
66434
  });
66425
66435
  return initialResult.promise;
66426
66436
  } catch (error) {
66427
- await this.failActiveRun(provisionalResolved.sessionKey, error);
66437
+ await this.failActiveRun(provisionalResolved.sessionKey, runId, error);
66428
66438
  throw error;
66429
66439
  }
66430
66440
  }
66431
66441
  async observeRun(target, observer) {
66432
- const existingRun = this.activeRuns.get(target.sessionKey);
66442
+ const existingRun = this.activeRuns.get(target.sessionKey) ?? await this.reconcilePersistedActiveRun(target);
66433
66443
  if (existingRun) {
66434
66444
  existingRun.observers.set(observer.id, {
66435
66445
  ...observer
@@ -66592,8 +66602,8 @@ class SessionService {
66592
66602
  }
66593
66603
  }
66594
66604
  }
66595
- async finishActiveRun(sessionKey, update) {
66596
- const run = this.activeRuns.get(sessionKey);
66605
+ async finishActiveRun(sessionKey, runId, update) {
66606
+ const run = this.getRun(sessionKey, runId);
66597
66607
  if (!run) {
66598
66608
  return;
66599
66609
  }
@@ -66604,8 +66614,8 @@ class SessionService {
66604
66614
  run.initialResult.resolve(update);
66605
66615
  this.activeRuns.delete(run.resolved.sessionKey);
66606
66616
  }
66607
- async failActiveRun(sessionKey, error) {
66608
- const run = this.activeRuns.get(sessionKey);
66617
+ async failActiveRun(sessionKey, runId, error) {
66618
+ const run = this.getRun(sessionKey, runId);
66609
66619
  if (!run) {
66610
66620
  return;
66611
66621
  }
@@ -66631,7 +66641,7 @@ class SessionService {
66631
66641
  if (!this.runnerSessions.canRecoverMidRun(error)) {
66632
66642
  return false;
66633
66643
  }
66634
- const run = this.activeRuns.get(sessionKey);
66644
+ const run = this.getRun(sessionKey, params.runId);
66635
66645
  if (!run) {
66636
66646
  return true;
66637
66647
  }
@@ -66648,7 +66658,7 @@ class SessionService {
66648
66658
  }));
66649
66659
  try {
66650
66660
  const recovered = await this.runnerSessions.reopenRunContext(target, params.timingContext);
66651
- const currentRun = this.activeRuns.get(sessionKey);
66661
+ const currentRun = this.getRun(sessionKey, params.runId);
66652
66662
  if (!currentRun) {
66653
66663
  return true;
66654
66664
  }
@@ -66664,6 +66674,7 @@ class SessionService {
66664
66674
  });
66665
66675
  await this.notifyRunObservers(currentRun, currentRun.latestUpdate);
66666
66676
  this.startRunMonitor(sessionKey, {
66677
+ runId: currentRun.runId,
66667
66678
  prompt: MID_RUN_RECOVERY_CONTINUE_PROMPT,
66668
66679
  initialSnapshot: recovered.initialSnapshot,
66669
66680
  startedAt: currentRun.startedAt,
@@ -66676,11 +66687,12 @@ class SessionService {
66676
66687
  } catch (reopenError) {
66677
66688
  if (recoveryAttempt < MID_RUN_RECOVERY_MAX_ATTEMPTS && this.runnerSessions.canRecoverMidRun(reopenError)) {
66678
66689
  return await this.recoverLostMidRun(sessionKey, {
66690
+ runId: params.runId,
66679
66691
  timingContext: params.timingContext,
66680
66692
  recoveryAttempt: recoveryAttempt + 1
66681
66693
  }, reopenError);
66682
66694
  }
66683
- const currentRun = this.activeRuns.get(sessionKey);
66695
+ const currentRun = this.getRun(sessionKey, params.runId);
66684
66696
  if (!currentRun) {
66685
66697
  return true;
66686
66698
  }
@@ -66688,15 +66700,15 @@ class SessionService {
66688
66700
  try {
66689
66701
  await this.runnerSessions.startFreshSession(target, params.timingContext);
66690
66702
  } catch (freshError) {
66691
- await this.failActiveRun(sessionKey, await this.runnerSessions.mapRunError(freshError, currentRun.resolved.sessionName, currentRun.latestUpdate.fullSnapshot));
66703
+ await this.failActiveRun(sessionKey, currentRun.runId, await this.runnerSessions.mapRunError(freshError, currentRun.resolved.sessionName, currentRun.latestUpdate.fullSnapshot));
66692
66704
  return true;
66693
66705
  }
66694
- await this.failActiveRun(sessionKey, new Error(buildRunRecoveryNote("fresh-required")));
66706
+ await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("fresh-required")));
66695
66707
  return true;
66696
66708
  }
66697
66709
  }
66698
66710
  startRunMonitor(sessionKey, params) {
66699
- const run = this.activeRuns.get(sessionKey);
66711
+ const run = this.getRun(sessionKey, params.runId);
66700
66712
  if (!run) {
66701
66713
  return;
66702
66714
  }
@@ -66720,14 +66732,14 @@ class SessionService {
66720
66732
  detachedAlready: params.detachedAlready,
66721
66733
  timingContext: params.timingContext,
66722
66734
  onPromptSubmitted: async () => {
66723
- const currentRun = this.activeRuns.get(sessionKey);
66735
+ const currentRun = this.getRun(sessionKey, params.runId);
66724
66736
  if (!currentRun) {
66725
66737
  return;
66726
66738
  }
66727
66739
  currentRun.steeringReady = true;
66728
66740
  },
66729
66741
  onRunning: async (update) => {
66730
- const currentRun = this.activeRuns.get(sessionKey);
66742
+ const currentRun = this.getRun(sessionKey, params.runId);
66731
66743
  if (!currentRun) {
66732
66744
  return;
66733
66745
  }
@@ -66743,7 +66755,7 @@ class SessionService {
66743
66755
  }));
66744
66756
  },
66745
66757
  onDetached: async (update) => {
66746
- const currentRun = this.activeRuns.get(sessionKey);
66758
+ const currentRun = this.getRun(sessionKey, params.runId);
66747
66759
  if (!currentRun) {
66748
66760
  return;
66749
66761
  }
@@ -66765,27 +66777,111 @@ class SessionService {
66765
66777
  currentRun.initialResult.resolve(detachedUpdate);
66766
66778
  },
66767
66779
  onCompleted: async (update) => {
66780
+ const currentRun = this.getRun(sessionKey, params.runId);
66781
+ if (!currentRun) {
66782
+ return;
66783
+ }
66768
66784
  const runUpdate = this.createRunUpdate({
66769
- resolved: run.resolved,
66785
+ resolved: currentRun.resolved,
66770
66786
  status: "completed",
66771
66787
  snapshot: mergeRunSnapshot(params.snapshotPrefix ?? "", update.snapshot),
66772
66788
  fullSnapshot: update.fullSnapshot,
66773
66789
  initialSnapshot: update.initialSnapshot
66774
66790
  });
66775
- await this.finishActiveRun(sessionKey, runUpdate);
66791
+ await this.finishActiveRun(sessionKey, params.runId, runUpdate);
66776
66792
  }
66777
66793
  });
66778
66794
  } catch (error) {
66779
66795
  if (await this.recoverLostMidRun(sessionKey, {
66796
+ runId: params.runId,
66780
66797
  timingContext: params.timingContext,
66781
66798
  recoveryAttempt: (params.recoveryAttempt ?? 0) + 1
66782
66799
  }, error)) {
66783
66800
  return;
66784
66801
  }
66785
- await this.failActiveRun(sessionKey, await this.runnerSessions.mapRunError(error, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
66802
+ await this.failActiveRun(sessionKey, params.runId, await this.runnerSessions.mapRunError(error, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
66786
66803
  }
66787
66804
  })();
66788
66805
  }
66806
+ allocateRunId() {
66807
+ return String(this.nextRunId++);
66808
+ }
66809
+ async reconcilePersistedActiveRun(target) {
66810
+ const activeRun = this.activeRuns.get(target.sessionKey);
66811
+ if (activeRun) {
66812
+ return activeRun;
66813
+ }
66814
+ const entry = await this.sessionState.getEntry(target.sessionKey);
66815
+ if (!entry?.runtime || entry.runtime.state === "idle") {
66816
+ return null;
66817
+ }
66818
+ const resolved = this.resolveTarget(target);
66819
+ if (!await this.tmux.hasSession(resolved.sessionName)) {
66820
+ await this.sessionState.setSessionRuntime(resolved, {
66821
+ state: "idle"
66822
+ });
66823
+ return null;
66824
+ }
66825
+ try {
66826
+ return await this.rehydratePersistedActiveRun(resolved, {
66827
+ runtimeState: entry.runtime.state,
66828
+ startedAt: entry.runtime.startedAt
66829
+ });
66830
+ } catch (error) {
66831
+ if (isMissingTmuxSessionError2(error) || isTmuxServerUnavailableError2(error) || isBootstrapSessionLostError2(error)) {
66832
+ await this.sessionState.setSessionRuntime(resolved, {
66833
+ state: "idle"
66834
+ });
66835
+ return null;
66836
+ }
66837
+ throw error;
66838
+ }
66839
+ }
66840
+ async rehydratePersistedActiveRun(resolved, params) {
66841
+ const existingRun = this.activeRuns.get(resolved.sessionKey);
66842
+ if (existingRun) {
66843
+ return existingRun;
66844
+ }
66845
+ const fullSnapshot = normalizePaneText(await this.tmux.capturePane(resolved.sessionName, resolved.stream.captureLines));
66846
+ const startedAt = params.startedAt ?? Date.now();
66847
+ const runId = this.allocateRunId();
66848
+ const initialResult = createDeferred();
66849
+ const update = this.createRunUpdate({
66850
+ resolved,
66851
+ status: params.runtimeState === "detached" ? "detached" : "running",
66852
+ snapshot: deriveInteractionText("", fullSnapshot),
66853
+ fullSnapshot,
66854
+ initialSnapshot: "",
66855
+ note: params.runtimeState === "detached" ? this.buildDetachedNote(resolved) : undefined
66856
+ });
66857
+ const run = {
66858
+ runId,
66859
+ resolved,
66860
+ observers: new Map,
66861
+ observerFailures: new Map,
66862
+ initialResult,
66863
+ latestUpdate: update,
66864
+ steeringReady: true,
66865
+ startedAt
66866
+ };
66867
+ this.activeRuns.set(resolved.sessionKey, run);
66868
+ this.startRunMonitor(resolved.sessionKey, {
66869
+ runId,
66870
+ prompt: undefined,
66871
+ initialSnapshot: fullSnapshot,
66872
+ startedAt,
66873
+ detachedAlready: params.runtimeState === "detached",
66874
+ timingContext: undefined
66875
+ });
66876
+ return run;
66877
+ }
66878
+ getRun(sessionKey, runId) {
66879
+ const run = this.activeRuns.get(sessionKey);
66880
+ if (!run || run.runId !== runId) {
66881
+ return null;
66882
+ }
66883
+ return run;
66884
+ }
66789
66885
  }
66790
66886
 
66791
66887
  // src/agents/agent-service.ts
@@ -67133,7 +67229,8 @@ class AgentService {
67133
67229
  timingContext: callbacks.timingContext,
67134
67230
  onUpdate: callbacks.onUpdate
67135
67231
  }), {
67136
- text: prompt
67232
+ text: prompt,
67233
+ canStart: async () => !this.activeRuns.hasActiveRun(target)
67137
67234
  });
67138
67235
  }
67139
67236
  getMaxMessageChars(agentId) {
@@ -69465,6 +69562,7 @@ async function clearSlackAssistantThreadStatus(client, target) {
69465
69562
  }
69466
69563
 
69467
69564
  // src/channels/processing-indicator.ts
69565
+ var ACTIVE_RUN_WAIT_POLL_INTERVAL_MS = 250;
69468
69566
  function shouldResolveIndicatorWait(update) {
69469
69567
  return isTerminalRunStatus(update.status);
69470
69568
  }
@@ -69491,6 +69589,18 @@ async function waitForProcessingIndicatorLifecycle(params) {
69491
69589
  settled = true;
69492
69590
  resolve();
69493
69591
  };
69592
+ (async () => {
69593
+ while (!settled) {
69594
+ await sleep(ACTIVE_RUN_WAIT_POLL_INTERVAL_MS);
69595
+ if (settled) {
69596
+ return;
69597
+ }
69598
+ if (!params.agentService.hasActiveRun(params.sessionTarget)) {
69599
+ resolveOnce();
69600
+ return;
69601
+ }
69602
+ }
69603
+ })();
69494
69604
  try {
69495
69605
  const observation = await params.agentService.observeRun(params.sessionTarget, {
69496
69606
  id: params.observerId,
@@ -76277,6 +76387,216 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
76277
76387
  dependencies.print(JSON.stringify(execution.result, null, 2));
76278
76388
  }
76279
76389
 
76390
+ // src/control/runtime-cli-shared.ts
76391
+ class CliCommandError extends Error {
76392
+ exitCode;
76393
+ constructor(message, exitCode = 1) {
76394
+ super(message);
76395
+ this.exitCode = exitCode;
76396
+ }
76397
+ }
76398
+ function printCommandOutcomeBanner(outcome) {
76399
+ console.log("");
76400
+ console.log("+---------+");
76401
+ console.log(outcome === "success" ? "| SUCCESS |" : "| FAILED |");
76402
+ console.log("+---------+");
76403
+ console.log("");
76404
+ }
76405
+ function printCommandOutcomeFooter(outcome) {
76406
+ printCommandOutcomeBanner(outcome);
76407
+ }
76408
+ function assertSupportedPlatform(command) {
76409
+ if (process.platform !== "win32") {
76410
+ return;
76411
+ }
76412
+ if (command.name === "help" || command.name === "version") {
76413
+ return;
76414
+ }
76415
+ throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
76416
+ }
76417
+ function getCliErrorExitCode(error) {
76418
+ return error instanceof CliCommandError ? error.exitCode : 1;
76419
+ }
76420
+
76421
+ // src/control/runner-cli.ts
76422
+ var SMOKE_BACKENDS = ["codex", "claude", "gemini", "all"];
76423
+ var SMOKE_SCENARIOS = [
76424
+ "startup_ready",
76425
+ "first_prompt_roundtrip",
76426
+ "session_id_roundtrip",
76427
+ "interrupt_during_run",
76428
+ "recover_after_runner_loss"
76429
+ ];
76430
+ var SMOKE_SUITES = ["launch-trio"];
76431
+ function parseRepeatedOption4(args, name) {
76432
+ const values = [];
76433
+ for (let index = 0;index < args.length; index += 1) {
76434
+ if (args[index] !== name) {
76435
+ continue;
76436
+ }
76437
+ const value = args[index + 1]?.trim();
76438
+ if (!value) {
76439
+ throw new CliCommandError(`Missing value for ${name}`, 2);
76440
+ }
76441
+ values.push(value);
76442
+ }
76443
+ return values;
76444
+ }
76445
+ function parseSingleOption3(args, name) {
76446
+ const values = parseRepeatedOption4(args, name);
76447
+ if (values.length === 0) {
76448
+ return;
76449
+ }
76450
+ return values[values.length - 1];
76451
+ }
76452
+ function hasFlag5(args, name) {
76453
+ return args.includes(name);
76454
+ }
76455
+ function isOneOf(value, allowed) {
76456
+ return allowed.includes(value);
76457
+ }
76458
+ function parseTimeoutMs(raw) {
76459
+ if (!raw) {
76460
+ return;
76461
+ }
76462
+ const parsed = Number.parseInt(raw, 10);
76463
+ if (!Number.isInteger(parsed) || parsed <= 0) {
76464
+ throw new CliCommandError("Invalid value for --timeout-ms", 2);
76465
+ }
76466
+ return parsed;
76467
+ }
76468
+ function renderRunnerHelp() {
76469
+ return [
76470
+ "clisbot runner",
76471
+ "",
76472
+ "Usage:",
76473
+ " clisbot runner",
76474
+ " clisbot runner --help",
76475
+ " clisbot runner smoke --backend <codex|claude|gemini> --scenario <name> [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]",
76476
+ " clisbot runner smoke --backend all --suite launch-trio [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]",
76477
+ "",
76478
+ "Smoke scenarios:",
76479
+ " - startup_ready",
76480
+ " - first_prompt_roundtrip",
76481
+ " - session_id_roundtrip",
76482
+ " - interrupt_during_run",
76483
+ " - recover_after_runner_loss",
76484
+ "",
76485
+ "Smoke suites:",
76486
+ " - launch-trio",
76487
+ "",
76488
+ "Current status:",
76489
+ " - the `runner` CLI surface now validates the smoke command contract",
76490
+ " - real smoke execution is the next implementation batch"
76491
+ ].join(`
76492
+ `);
76493
+ }
76494
+ function parseSmokeCommand(args) {
76495
+ const backend = parseSingleOption3(args, "--backend");
76496
+ if (!backend) {
76497
+ throw new CliCommandError(`Usage: clisbot runner smoke --backend <codex|claude|gemini> --scenario <name> [--json]
76498
+ clisbot runner smoke --backend all --suite launch-trio [--json]`, 2);
76499
+ }
76500
+ if (!isOneOf(backend, SMOKE_BACKENDS)) {
76501
+ throw new CliCommandError(`Unsupported --backend value: ${backend}`, 2);
76502
+ }
76503
+ const rawScenario = parseSingleOption3(args, "--scenario");
76504
+ const rawSuite = parseSingleOption3(args, "--suite");
76505
+ if (rawScenario && rawSuite) {
76506
+ throw new CliCommandError("--scenario and --suite are mutually exclusive", 2);
76507
+ }
76508
+ if (rawScenario && !isOneOf(rawScenario, SMOKE_SCENARIOS)) {
76509
+ throw new CliCommandError(`Unsupported --scenario value: ${rawScenario}`, 2);
76510
+ }
76511
+ if (rawSuite && !isOneOf(rawSuite, SMOKE_SUITES)) {
76512
+ throw new CliCommandError(`Unsupported --suite value: ${rawSuite}`, 2);
76513
+ }
76514
+ const scenario = rawScenario;
76515
+ const suite = rawSuite;
76516
+ if (backend === "all") {
76517
+ if (scenario) {
76518
+ throw new CliCommandError("--backend all is only valid with --suite", 2);
76519
+ }
76520
+ if (!suite) {
76521
+ throw new CliCommandError("--backend all requires --suite launch-trio", 2);
76522
+ }
76523
+ } else {
76524
+ if (suite) {
76525
+ throw new CliCommandError(`--suite is only valid with --backend all`, 2);
76526
+ }
76527
+ if (!scenario) {
76528
+ throw new CliCommandError(`--backend ${backend} requires --scenario`, 2);
76529
+ }
76530
+ }
76531
+ return {
76532
+ backend,
76533
+ scenario,
76534
+ suite,
76535
+ workspace: parseSingleOption3(args, "--workspace"),
76536
+ agent: parseSingleOption3(args, "--agent"),
76537
+ artifactDir: parseSingleOption3(args, "--artifact-dir"),
76538
+ timeoutMs: parseTimeoutMs(parseSingleOption3(args, "--timeout-ms")),
76539
+ keepSession: hasFlag5(args, "--keep-session"),
76540
+ json: hasFlag5(args, "--json")
76541
+ };
76542
+ }
76543
+ function renderSmokeNotImplementedResult(options) {
76544
+ return {
76545
+ kind: "runner-smoke-framework-error",
76546
+ version: "v0",
76547
+ ok: false,
76548
+ backendId: options.backend,
76549
+ scenario: options.scenario ?? null,
76550
+ suite: options.suite ?? null,
76551
+ error: {
76552
+ code: "NOT_IMPLEMENTED",
76553
+ message: "clisbot runner smoke is not implemented yet. The command surface and contract validation are ready; the real execution batch is next."
76554
+ },
76555
+ options: {
76556
+ workspace: options.workspace ?? null,
76557
+ agent: options.agent ?? null,
76558
+ artifactDir: options.artifactDir ?? null,
76559
+ timeoutMs: options.timeoutMs ?? null,
76560
+ keepSession: options.keepSession,
76561
+ json: options.json
76562
+ }
76563
+ };
76564
+ }
76565
+ async function runSmokeCli(args) {
76566
+ if (args.length === 0 || hasFlag5(args, "--help") || hasFlag5(args, "-h")) {
76567
+ console.log(renderRunnerHelp());
76568
+ return;
76569
+ }
76570
+ const options = parseSmokeCommand(args);
76571
+ const result = renderSmokeNotImplementedResult(options);
76572
+ if (options.json) {
76573
+ console.log(JSON.stringify(result, null, 2));
76574
+ } else {
76575
+ console.log([
76576
+ "clisbot runner smoke",
76577
+ "",
76578
+ `backend: ${options.backend}`,
76579
+ options.scenario ? `scenario: ${options.scenario}` : `suite: ${options.suite}`,
76580
+ "status: not implemented yet",
76581
+ "note: the smoke contract is validated, but real CLI execution is the next batch"
76582
+ ].join(`
76583
+ `));
76584
+ }
76585
+ process.exitCode = 3;
76586
+ }
76587
+ async function runRunnerCli(args) {
76588
+ const subcommand = args[0];
76589
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
76590
+ console.log(renderRunnerHelp());
76591
+ return;
76592
+ }
76593
+ if (subcommand === "smoke") {
76594
+ await runSmokeCli(args.slice(1));
76595
+ return;
76596
+ }
76597
+ throw new CliCommandError(renderRunnerHelp(), 2);
76598
+ }
76599
+
76280
76600
  // src/control/activity-store.ts
76281
76601
  import { dirname as dirname14 } from "node:path";
76282
76602
  class ActivityStore {
@@ -76812,27 +77132,6 @@ function renderOperatorErrorWithHelpLines(error) {
76812
77132
  ];
76813
77133
  }
76814
77134
 
76815
- // src/control/runtime-cli-shared.ts
76816
- function printCommandOutcomeBanner(outcome) {
76817
- console.log("");
76818
- console.log("+---------+");
76819
- console.log(outcome === "success" ? "| SUCCESS |" : "| FAILED |");
76820
- console.log("+---------+");
76821
- console.log("");
76822
- }
76823
- function printCommandOutcomeFooter(outcome) {
76824
- printCommandOutcomeBanner(outcome);
76825
- }
76826
- function assertSupportedPlatform(command) {
76827
- if (process.platform !== "win32") {
76828
- return;
76829
- }
76830
- if (command.name === "help" || command.name === "version") {
76831
- return;
76832
- }
76833
- throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
76834
- }
76835
-
76836
77135
  // src/control/runtime-bootstrap-cli.ts
76837
77136
  function hasHelpFlag(args) {
76838
77137
  return args.includes("--help") || args.includes("-h") || args.includes("help");
@@ -77922,6 +78221,10 @@ async function runControlCommand(command) {
77922
78221
  await runAuthCli(command.args);
77923
78222
  return true;
77924
78223
  }
78224
+ if (command.name === "runner") {
78225
+ await runRunnerCli(command.args);
78226
+ return true;
78227
+ }
77925
78228
  if (command.name === "pairing") {
77926
78229
  await runPairingCli(command.args);
77927
78230
  return true;
@@ -77943,5 +78246,5 @@ try {
77943
78246
  printCommandOutcomeBanner("failure");
77944
78247
  }
77945
78248
  await printCliError(error);
77946
- process.exit(1);
78249
+ process.exit(getCliErrorExitCode(error));
77947
78250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",