clisbot 0.1.32 → 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.
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>]...",
@@ -54713,8 +54720,8 @@ function renderCliHelp() {
54713
54720
  " clear-token <slack-app|slack-bot|telegram-bot>",
54714
54721
  " See `clisbot channels --help` for route policy notes and defaults such as `requireMention`.",
54715
54722
  " accounts Manage Slack and Telegram provider accounts plus persistence state.",
54716
- " add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54717
- " add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54723
+ " add telegram --account <id> (--token|--telegram-bot-token) <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54724
+ " add slack --account <id> (--app-token|--slack-app-token) <ENV_NAME|${ENV_NAME}|literal> (--bot-token|--slack-bot-token) <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54718
54725
  " persist --channel <slack|telegram> --account <id>",
54719
54726
  " persist --all",
54720
54727
  " See `clisbot accounts --help` for env-vs-mem-vs-persist behavior.",
@@ -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.",
@@ -55261,6 +55270,8 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
55261
55270
  ],
55262
55271
  trustWorkspace: true,
55263
55272
  startupDelayMs: 3000,
55273
+ startupRetryCount: 2,
55274
+ startupRetryDelayMs: 1000,
55264
55275
  promptSubmitDelayMs: 150,
55265
55276
  sessionId: {
55266
55277
  create: {
@@ -55292,6 +55303,8 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
55292
55303
  startupOptions: ["--dangerously-skip-permissions"],
55293
55304
  trustWorkspace: true,
55294
55305
  startupDelayMs: 3000,
55306
+ startupRetryCount: 2,
55307
+ startupRetryDelayMs: 1000,
55295
55308
  promptSubmitDelayMs: 150,
55296
55309
  sessionId: {
55297
55310
  create: {
@@ -55320,6 +55333,8 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
55320
55333
  startupOptions: ["--approval-mode=yolo", "--sandbox=false"],
55321
55334
  trustWorkspace: true,
55322
55335
  startupDelayMs: 15000,
55336
+ startupRetryCount: 2,
55337
+ startupRetryDelayMs: 1000,
55323
55338
  startupReadyPattern: "Type your message or @path/to/file",
55324
55339
  startupBlockers: [
55325
55340
  {
@@ -55359,6 +55374,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55359
55374
  args: [...options, "-C", "{workspace}"],
55360
55375
  trustWorkspace: template.trustWorkspace,
55361
55376
  startupDelayMs: template.startupDelayMs,
55377
+ startupRetryCount: template.startupRetryCount,
55378
+ startupRetryDelayMs: template.startupRetryDelayMs,
55362
55379
  startupReadyPattern: template.startupReadyPattern,
55363
55380
  startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55364
55381
  promptSubmitDelayMs: template.promptSubmitDelayMs,
@@ -55383,6 +55400,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55383
55400
  args: [...options],
55384
55401
  trustWorkspace: template.trustWorkspace,
55385
55402
  startupDelayMs: template.startupDelayMs,
55403
+ startupRetryCount: template.startupRetryCount,
55404
+ startupRetryDelayMs: template.startupRetryDelayMs,
55386
55405
  startupReadyPattern: template.startupReadyPattern,
55387
55406
  startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55388
55407
  promptSubmitDelayMs: template.promptSubmitDelayMs,
@@ -60047,6 +60066,8 @@ var runnerSchema = exports_external.object({
60047
60066
  ]),
60048
60067
  trustWorkspace: exports_external.boolean().default(true),
60049
60068
  startupDelayMs: exports_external.number().int().positive().default(3000),
60069
+ startupRetryCount: exports_external.number().int().min(0).default(2),
60070
+ startupRetryDelayMs: exports_external.number().int().min(0).default(1000),
60050
60071
  startupReadyPattern: exports_external.string().min(1).optional(),
60051
60072
  startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
60052
60073
  promptSubmitDelayMs: exports_external.number().int().min(0).default(150),
@@ -60083,6 +60104,8 @@ var runnerOverrideSchema = exports_external.object({
60083
60104
  args: exports_external.array(exports_external.string()).optional(),
60084
60105
  trustWorkspace: exports_external.boolean().optional(),
60085
60106
  startupDelayMs: exports_external.number().int().positive().optional(),
60107
+ startupRetryCount: exports_external.number().int().min(0).optional(),
60108
+ startupRetryDelayMs: exports_external.number().int().min(0).optional(),
60086
60109
  startupReadyPattern: exports_external.string().min(1).optional(),
60087
60110
  startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
60088
60111
  promptSubmitDelayMs: exports_external.number().int().min(0).optional(),
@@ -60125,6 +60148,8 @@ var agentDefaultsSchema = exports_external.object({
60125
60148
  ],
60126
60149
  trustWorkspace: true,
60127
60150
  startupDelayMs: 3000,
60151
+ startupRetryCount: 2,
60152
+ startupRetryDelayMs: 1000,
60128
60153
  promptSubmitDelayMs: 150,
60129
60154
  sessionId: defaultRunnerSessionIdConfig
60130
60155
  }),
@@ -60789,6 +60814,8 @@ function renderDefaultConfigTemplate(options = {}) {
60789
60814
  ],
60790
60815
  trustWorkspace: true,
60791
60816
  startupDelayMs: 3000,
60817
+ startupRetryCount: 2,
60818
+ startupRetryDelayMs: 1000,
60792
60819
  startupReadyPattern: undefined,
60793
60820
  promptSubmitDelayMs: 150,
60794
60821
  sessionId: {
@@ -61148,6 +61175,157 @@ function resolveTopLevelBoundAgentId(config, match) {
61148
61175
  });
61149
61176
  }
61150
61177
 
61178
+ // src/control/channel-bootstrap-flags.ts
61179
+ function isLiteralToken(token) {
61180
+ return token?.kind === "mem";
61181
+ }
61182
+ function parseBotType(rawValue) {
61183
+ const value = rawValue.trim().toLowerCase();
61184
+ if (value === "personal") {
61185
+ return "personal-assistant";
61186
+ }
61187
+ if (value === "team") {
61188
+ return "team-assistant";
61189
+ }
61190
+ throw new Error(`Invalid bot type: ${rawValue}. Expected personal or team.`);
61191
+ }
61192
+ function parseOptionValue(args, name, index) {
61193
+ const value = args[index + 1]?.trim();
61194
+ if (!value) {
61195
+ throw new Error(`Missing value for ${name}`);
61196
+ }
61197
+ return value;
61198
+ }
61199
+ function getOrCreateSlackAccount(accounts, accountId) {
61200
+ let account = accounts.find((entry) => entry.accountId === accountId);
61201
+ if (!account) {
61202
+ account = { accountId };
61203
+ accounts.push(account);
61204
+ }
61205
+ return account;
61206
+ }
61207
+ function getOrCreateTelegramAccount(accounts, accountId) {
61208
+ let account = accounts.find((entry) => entry.accountId === accountId);
61209
+ if (!account) {
61210
+ account = { accountId };
61211
+ accounts.push(account);
61212
+ }
61213
+ return account;
61214
+ }
61215
+ function ensureUniqueAccount(accounts, accountId, flagName) {
61216
+ if (accounts.some((entry) => entry.accountId === accountId)) {
61217
+ throw new Error(`Duplicate ${flagName} ${accountId}`);
61218
+ }
61219
+ }
61220
+ function validateSlackAccount(account) {
61221
+ if (!account.appToken || !account.botToken) {
61222
+ throw new Error(`Slack account ${account.accountId} requires both app token and bot token`);
61223
+ }
61224
+ if (account.appToken.kind !== account.botToken.kind) {
61225
+ throw new Error(`Slack account ${account.accountId} must use one credential source kind for both app and bot tokens`);
61226
+ }
61227
+ }
61228
+ function validateTelegramAccount(account) {
61229
+ if (!account.botToken) {
61230
+ throw new Error(`Telegram account ${account.accountId} requires a bot token`);
61231
+ }
61232
+ }
61233
+ function parseBootstrapFlags(args) {
61234
+ const slackAccounts = [];
61235
+ const telegramAccounts = [];
61236
+ let currentSlackAccountId;
61237
+ let currentTelegramAccountId;
61238
+ let cliTool;
61239
+ let bootstrap;
61240
+ let persist = false;
61241
+ let sawCredentialFlags = false;
61242
+ let sawSlackFlags = false;
61243
+ let sawTelegramFlags = false;
61244
+ for (let index = 0;index < args.length; index += 1) {
61245
+ const arg = args[index];
61246
+ if (arg === "--cli") {
61247
+ cliTool = parseOptionValue(args, arg, index);
61248
+ index += 1;
61249
+ continue;
61250
+ }
61251
+ if (arg === "--bot-type") {
61252
+ bootstrap = parseBotType(parseOptionValue(args, arg, index));
61253
+ index += 1;
61254
+ continue;
61255
+ }
61256
+ if (arg === "--persist") {
61257
+ persist = true;
61258
+ continue;
61259
+ }
61260
+ if (arg === "--slack-account") {
61261
+ const accountId = parseOptionValue(args, arg, index);
61262
+ ensureUniqueAccount(slackAccounts, accountId, "--slack-account");
61263
+ currentSlackAccountId = accountId;
61264
+ getOrCreateSlackAccount(slackAccounts, accountId);
61265
+ sawSlackFlags = true;
61266
+ index += 1;
61267
+ continue;
61268
+ }
61269
+ if (arg === "--telegram-account") {
61270
+ const accountId = parseOptionValue(args, arg, index);
61271
+ ensureUniqueAccount(telegramAccounts, accountId, "--telegram-account");
61272
+ currentTelegramAccountId = accountId;
61273
+ getOrCreateTelegramAccount(telegramAccounts, accountId);
61274
+ sawTelegramFlags = true;
61275
+ index += 1;
61276
+ continue;
61277
+ }
61278
+ if (arg === "--slack-app-token") {
61279
+ const token = parseTokenInput(parseOptionValue(args, arg, index));
61280
+ const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
61281
+ account.appToken = token;
61282
+ sawCredentialFlags = true;
61283
+ sawSlackFlags = true;
61284
+ index += 1;
61285
+ continue;
61286
+ }
61287
+ if (arg === "--slack-bot-token") {
61288
+ const token = parseTokenInput(parseOptionValue(args, arg, index));
61289
+ const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
61290
+ account.botToken = token;
61291
+ sawCredentialFlags = true;
61292
+ sawSlackFlags = true;
61293
+ index += 1;
61294
+ continue;
61295
+ }
61296
+ if (arg === "--telegram-bot-token") {
61297
+ const token = parseTokenInput(parseOptionValue(args, arg, index));
61298
+ const account = getOrCreateTelegramAccount(telegramAccounts, currentTelegramAccountId ?? "default");
61299
+ account.botToken = token;
61300
+ sawCredentialFlags = true;
61301
+ sawTelegramFlags = true;
61302
+ index += 1;
61303
+ continue;
61304
+ }
61305
+ throw new Error(`Unknown option for start/init: ${arg}`);
61306
+ }
61307
+ for (const account of slackAccounts) {
61308
+ validateSlackAccount(account);
61309
+ }
61310
+ for (const account of telegramAccounts) {
61311
+ validateTelegramAccount(account);
61312
+ }
61313
+ return {
61314
+ cliTool,
61315
+ bootstrap,
61316
+ persist,
61317
+ slackAccounts,
61318
+ telegramAccounts,
61319
+ sawCredentialFlags,
61320
+ sawSlackFlags,
61321
+ sawTelegramFlags,
61322
+ literalWarnings: []
61323
+ };
61324
+ }
61325
+ function hasLiteralBootstrapCredentials(flags) {
61326
+ return flags.slackAccounts.some((account) => isLiteralToken(account.appToken) || isLiteralToken(account.botToken)) || flags.telegramAccounts.some((account) => isLiteralToken(account.botToken));
61327
+ }
61328
+
61151
61329
  // src/control/agents-cli.ts
61152
61330
  function getEditableConfigPath() {
61153
61331
  return process.env.CLISBOT_CONFIG_PATH;
@@ -61194,8 +61372,8 @@ function renderAgentsHelp() {
61194
61372
  " clisbot agents --help",
61195
61373
  " clisbot agents help",
61196
61374
  " clisbot agents list [--bindings] [--json]",
61197
- " clisbot agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...",
61198
- " clisbot agents bootstrap <id> --mode <personal-assistant|team-assistant> [--force]",
61375
+ " clisbot agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bot-type <personal|team>] [--bind <channel[:accountId]>]...",
61376
+ " clisbot agents bootstrap <id> --bot-type <personal|team> [--force]",
61199
61377
  " clisbot agents bindings [--agent <id>] [--json]",
61200
61378
  " clisbot agents bind --agent <id> --bind <channel[:accountId]>",
61201
61379
  " clisbot agents unbind --agent <id> [--bind <channel[:accountId]> | --all]",
@@ -61372,7 +61550,13 @@ async function addAgentToEditableConfig(params) {
61372
61550
  async function addAgent(args) {
61373
61551
  const agentId = args[0]?.trim();
61374
61552
  if (!agentId) {
61375
- throw new Error("Usage: agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...");
61553
+ throw new Error("Usage: agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bot-type <personal|team>] [--bind <channel[:accountId]>]...");
61554
+ }
61555
+ if (hasFlag(args, "--bootstrap")) {
61556
+ throw new Error("agents add no longer accepts --bootstrap; use --bot-type personal or --bot-type team");
61557
+ }
61558
+ if (hasFlag(args, "--mode")) {
61559
+ throw new Error("agents add does not use --mode; use --bot-type personal or --bot-type team");
61376
61560
  }
61377
61561
  const cliTool = parseSingleOption(args, "--cli");
61378
61562
  if (!cliTool || !(cliTool in DEFAULT_AGENT_TOOL_TEMPLATES)) {
@@ -61380,9 +61564,10 @@ async function addAgent(args) {
61380
61564
  }
61381
61565
  const workspace = parseSingleOption(args, "--workspace");
61382
61566
  const startupOptions = parseRepeatedOption(args, "--startup-option");
61383
- const bootstrap = parseSingleOption(args, "--bootstrap");
61384
- if (hasFlag(args, "--bootstrap") && (!bootstrap || !SUPPORTED_BOOTSTRAP_MODES.includes(bootstrap))) {
61385
- throw new Error("--bootstrap requires personal-assistant or team-assistant");
61567
+ let bootstrap;
61568
+ const botType = parseSingleOption(args, "--bot-type");
61569
+ if (botType) {
61570
+ bootstrap = parseBotType(botType);
61386
61571
  }
61387
61572
  const bindings = parseRepeatedOption(args, "--bind").map(parseBinding);
61388
61573
  const result = await addAgentToEditableConfig({
@@ -61404,12 +61589,19 @@ async function addAgent(args) {
61404
61589
  async function bootstrapAgent(args) {
61405
61590
  const agentId = args[0]?.trim();
61406
61591
  if (!agentId) {
61407
- throw new Error("Usage: agents bootstrap <id> --mode <personal-assistant|team-assistant> [--force]");
61592
+ throw new Error("Usage: agents bootstrap <id> --bot-type <personal|team> [--force]");
61593
+ }
61594
+ if (hasFlag(args, "--mode")) {
61595
+ throw new Error("agents bootstrap no longer accepts --mode; use --bot-type personal or --bot-type team");
61408
61596
  }
61409
- const mode = parseSingleOption(args, "--mode");
61410
- if (!mode || !SUPPORTED_BOOTSTRAP_MODES.includes(mode)) {
61411
- throw new Error("agents bootstrap requires --mode personal-assistant or --mode team-assistant");
61597
+ if (hasFlag(args, "--bootstrap")) {
61598
+ throw new Error("agents bootstrap does not use --bootstrap; use --bot-type personal or --bot-type team");
61412
61599
  }
61600
+ const botType = parseSingleOption(args, "--bot-type");
61601
+ if (!botType) {
61602
+ throw new Error("agents bootstrap requires --bot-type personal or --bot-type team");
61603
+ }
61604
+ const mode = parseBotType(botType);
61413
61605
  const force = hasFlag(args, "--force");
61414
61606
  const { config, configPath } = await readEditableConfig(getEditableConfigPath());
61415
61607
  const entry = ensureAgentExists(config, agentId);
@@ -64072,6 +64264,8 @@ function resolveAgentTargetInternal(loadedConfig, target) {
64072
64264
  }
64073
64265
 
64074
64266
  // src/agents/job-queue.ts
64267
+ var QUEUE_PENDING_POLL_INTERVAL_MS = 25;
64268
+
64075
64269
  class ClearedQueuedTaskError extends Error {
64076
64270
  constructor() {
64077
64271
  super("Queued task was cleared before execution.");
@@ -64099,6 +64293,7 @@ class AgentJobQueue {
64099
64293
  text: options.text,
64100
64294
  createdAt: Date.now(),
64101
64295
  status: "pending",
64296
+ canStart: options.canStart,
64102
64297
  task,
64103
64298
  resolve,
64104
64299
  reject,
@@ -64166,6 +64361,10 @@ class AgentJobQueue {
64166
64361
  if (!nextEntry) {
64167
64362
  break;
64168
64363
  }
64364
+ if (nextEntry.canStart && !await nextEntry.canStart()) {
64365
+ await sleep(QUEUE_PENDING_POLL_INTERVAL_MS);
64366
+ continue;
64367
+ }
64169
64368
  nextEntry.status = "running";
64170
64369
  try {
64171
64370
  nextEntry.resolve(await nextEntry.task());
@@ -64426,12 +64625,22 @@ function isInterruptStatusLine(line) {
64426
64625
  }
64427
64626
  return CODEX_WORKING_STATUS_PATTERN.test(trimmed) || CODEX_INTERRUPT_FOOTER_PATTERN.test(trimmed);
64428
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
+ }
64429
64635
  function isTimerDrivenStatusLine(line) {
64430
64636
  const trimmed = line.trim();
64431
64637
  if (!trimmed) {
64432
64638
  return false;
64433
64639
  }
64434
- 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));
64435
64644
  }
64436
64645
  function shouldDropCodexChromeLine(line) {
64437
64646
  const trimmed = line.trim();
@@ -64959,6 +65168,8 @@ var PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS = 40;
64959
65168
  var PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS = 160;
64960
65169
  var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
64961
65170
  var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
65171
+ var SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS = 40;
65172
+ var SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS = 320;
64962
65173
  var TMUX_MISSING_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
64963
65174
  var TMUX_MISSING_SESSION_PATTERN = /(?:can't find session:|no server running on )/i;
64964
65175
  var TMUX_SERVER_UNAVAILABLE_PATTERN = /(?:No such file or directory|error connecting to|failed to connect to server)/i;
@@ -64998,15 +65209,18 @@ async function submitTmuxSessionInput(params) {
64998
65209
  logLatencyDebug("tmux-paste-unconfirmed", params.timingContext, {
64999
65210
  sessionName: params.sessionName
65000
65211
  });
65001
- throw new Error("tmux paste was not confirmed before Enter. The pane state did not change, so clisbot did not treat the prompt as visibly delivered.");
65212
+ preSubmitState = prePasteState;
65213
+ } else {
65214
+ preSubmitState = await params.tmux.getPaneState(params.sessionName);
65002
65215
  }
65003
- preSubmitState = await params.tmux.getPaneState(params.sessionName);
65004
65216
  }
65005
65217
  await params.tmux.sendKey(params.sessionName, "Enter");
65006
65218
  if (await waitForPaneSubmitConfirmation({
65007
65219
  tmux: params.tmux,
65008
65220
  sessionName: params.sessionName,
65009
- baseline: preSubmitState
65221
+ baseline: preSubmitState,
65222
+ baselineSnapshot: prePasteSnapshot,
65223
+ captureLines
65010
65224
  })) {
65011
65225
  return;
65012
65226
  }
@@ -65017,10 +65231,15 @@ async function submitTmuxSessionInput(params) {
65017
65231
  if (await waitForPaneSubmitConfirmation({
65018
65232
  tmux: params.tmux,
65019
65233
  sessionName: params.sessionName,
65020
- baseline: preSubmitState
65234
+ baseline: preSubmitState,
65235
+ baselineSnapshot: prePasteSnapshot,
65236
+ captureLines
65021
65237
  })) {
65022
65238
  return;
65023
65239
  }
65240
+ if (!pasteSettlement.visible) {
65241
+ throw new Error("tmux paste was not confirmed before Enter, and submission still could not be confirmed after Enter. clisbot did not treat the prompt as truthfully delivered.");
65242
+ }
65024
65243
  logLatencyDebug("tmux-submit-unconfirmed", params.timingContext, {
65025
65244
  sessionName: params.sessionName
65026
65245
  });
@@ -65190,6 +65409,16 @@ async function waitForPaneSubmitConfirmation(params) {
65190
65409
  if (hasPaneStateChanged(params.baseline, state)) {
65191
65410
  return true;
65192
65411
  }
65412
+ const snapshotChanged = await waitForPaneSubmitSnapshotConfirmation({
65413
+ tmux: params.tmux,
65414
+ sessionName: params.sessionName,
65415
+ baselineSnapshot: params.baselineSnapshot,
65416
+ captureLines: params.captureLines,
65417
+ maxWaitMs: Math.min(SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS, Math.max(0, deadline - Date.now()))
65418
+ });
65419
+ if (snapshotChanged) {
65420
+ return true;
65421
+ }
65193
65422
  const remainingMs = deadline - Date.now();
65194
65423
  if (remainingMs <= 0) {
65195
65424
  return false;
@@ -65197,6 +65426,20 @@ async function waitForPaneSubmitConfirmation(params) {
65197
65426
  await sleep(Math.min(SUBMIT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
65198
65427
  }
65199
65428
  }
65429
+ async function waitForPaneSubmitSnapshotConfirmation(params) {
65430
+ const deadline = Date.now() + params.maxWaitMs;
65431
+ while (true) {
65432
+ const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65433
+ if (snapshot !== params.baselineSnapshot) {
65434
+ return true;
65435
+ }
65436
+ const remainingMs = deadline - Date.now();
65437
+ if (remainingMs <= 0) {
65438
+ return false;
65439
+ }
65440
+ await sleep(Math.min(SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
65441
+ }
65442
+ }
65200
65443
  async function waitForPanePasteSettlement(params) {
65201
65444
  await sleep(params.minDelayMs);
65202
65445
  let currentState = await params.tmux.getPaneState(params.sessionName);
@@ -65513,32 +65756,38 @@ class RunnerService {
65513
65756
  pollIntervalMs: capture.pollIntervalMs
65514
65757
  });
65515
65758
  }
65516
- async retryFreshStartWithClearedSessionId(target, resolved, options) {
65517
- if (options.allowRetry === false) {
65759
+ async retryFreshStartWithClearedSessionId(target, resolved, remainingFreshRetries) {
65760
+ if (remainingFreshRetries <= 0) {
65518
65761
  return null;
65519
65762
  }
65520
65763
  await this.tmux.killSession(resolved.sessionName);
65521
65764
  await this.sessionState.clearSessionIdEntry(resolved, {
65522
65765
  runnerCommand: resolved.runner.command
65523
65766
  });
65767
+ if (resolved.runner.startupRetryDelayMs > 0) {
65768
+ await sleep(resolved.runner.startupRetryDelayMs);
65769
+ }
65524
65770
  return this.ensureSessionReady(target, {
65525
- allowFreshRetry: options.nextAllowFreshRetry
65771
+ remainingFreshRetries: remainingFreshRetries - 1
65526
65772
  });
65527
65773
  }
65528
- async retryAfterStartupFault(target, resolved, error, allowFreshRetry) {
65774
+ async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
65529
65775
  if (!isRecoverableStartupSessionLoss(error)) {
65530
65776
  return null;
65531
65777
  }
65532
- return this.retryFreshStartWithClearedSessionId(target, resolved, {
65533
- allowRetry: allowFreshRetry,
65534
- nextAllowFreshRetry: false
65535
- });
65778
+ return this.retryFreshStartWithClearedSessionId(target, resolved, remainingFreshRetries);
65536
65779
  }
65537
- async retryAfterStartupTimeout(target, resolved, allowFreshRetry) {
65538
- return this.retryFreshStartWithClearedSessionId(target, resolved, {
65539
- allowRetry: allowFreshRetry,
65540
- nextAllowFreshRetry: false
65541
- });
65780
+ async retryAfterStartupTimeout(target, resolved, remainingFreshRetries) {
65781
+ return this.retryFreshStartWithClearedSessionId(target, resolved, remainingFreshRetries);
65782
+ }
65783
+ resolveRemainingFreshRetries(resolved, options) {
65784
+ if (typeof options.remainingFreshRetries === "number") {
65785
+ return options.remainingFreshRetries;
65786
+ }
65787
+ if (options.allowFreshRetry === false) {
65788
+ return 0;
65789
+ }
65790
+ return resolved.runner.startupRetryCount;
65542
65791
  }
65543
65792
  async abortUnreadySession(resolved, reason, snapshot) {
65544
65793
  await this.tmux.killSession(resolved.sessionName);
@@ -65612,6 +65861,7 @@ class RunnerService {
65612
65861
  sessionKey: resolved.sessionKey,
65613
65862
  sessionName: resolved.sessionName
65614
65863
  };
65864
+ const remainingFreshRetries = this.resolveRemainingFreshRetries(resolved, options);
65615
65865
  logLatencyDebug("ensure-session-ready-start", timingContext);
65616
65866
  await ensureDir2(resolved.workspacePath);
65617
65867
  await ensureDir2(dirname11(this.loadedConfig.raw.tmux.socketPath));
@@ -65684,7 +65934,7 @@ class RunnerService {
65684
65934
  await this.abortUnreadySession(resolved, bootstrapResult.message, bootstrapResult.snapshot);
65685
65935
  }
65686
65936
  if (bootstrapResult.status === "timeout" && resolved.runner.startupReadyPattern) {
65687
- const retried = await this.retryAfterStartupTimeout(target, resolved, options.allowFreshRetry);
65937
+ const retried = await this.retryAfterStartupTimeout(target, resolved, remainingFreshRetries);
65688
65938
  if (retried) {
65689
65939
  return retried;
65690
65940
  }
@@ -65695,7 +65945,7 @@ class RunnerService {
65695
65945
  runnerCommand: runnerLaunch.command
65696
65946
  });
65697
65947
  } catch (error) {
65698
- const retried = await this.retryAfterStartupFault(target, resolved, error, options.allowFreshRetry);
65948
+ const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries);
65699
65949
  if (retried) {
65700
65950
  return retried;
65701
65951
  }
@@ -65750,10 +66000,7 @@ class RunnerService {
65750
66000
  if (options.allowFreshRetryBeforePrompt === false || !isRecoverableStartupSessionLoss(error)) {
65751
66001
  throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65752
66002
  }
65753
- const retried = await this.retryFreshStartWithClearedSessionId(target, resolved, {
65754
- allowRetry: true,
65755
- nextAllowFreshRetry: false
65756
- });
66003
+ const retried = await this.retryFreshStartWithClearedSessionId(target, resolved, resolved.runner.startupRetryCount);
65757
66004
  if (!retried) {
65758
66005
  throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65759
66006
  }
@@ -65924,8 +66171,10 @@ var FIRST_OUTPUT_POLL_INTERVAL_MS = 250;
65924
66171
  async function monitorTmuxRun(params) {
65925
66172
  let previousSnapshot = params.initialSnapshot;
65926
66173
  let previousRunningSnapshot = "";
65927
- let lastActivityAt = params.startedAt;
66174
+ let lastPaneChangeAt = params.startedAt;
65928
66175
  let sawActivity = false;
66176
+ let sawPaneChange = false;
66177
+ let sawPromptSubmission = Boolean(params.prompt);
65929
66178
  let detachedNotified = params.detachedAlready;
65930
66179
  let firstMeaningfulDeltaLogged = false;
65931
66180
  let noOutputThresholdLogged = false;
@@ -65941,6 +66190,8 @@ async function monitorTmuxRun(params) {
65941
66190
  promptSubmitDelayMs: params.promptSubmitDelayMs,
65942
66191
  timingContext: params.timingContext
65943
66192
  });
66193
+ sawPromptSubmission = true;
66194
+ lastPaneChangeAt = Date.now();
65944
66195
  await params.onPromptSubmitted?.();
65945
66196
  logLatencyDebug("tmux-submit-complete", params.timingContext, {
65946
66197
  sessionName: params.sessionName,
@@ -65952,11 +66203,16 @@ async function monitorTmuxRun(params) {
65952
66203
  await sleep(sawActivity ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
65953
66204
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65954
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);
65955
66212
  const runningSnapshot = params.initialSnapshot ? deriveRunningInteractionText(params.initialSnapshot, snapshot) : deriveRunningInteractionSnapshot(snapshot);
65956
66213
  previousSnapshot = snapshot;
65957
66214
  if (runningSnapshot && runningSnapshot !== previousRunningSnapshot) {
65958
66215
  previousRunningSnapshot = runningSnapshot;
65959
- lastActivityAt = now;
65960
66216
  sawActivity = true;
65961
66217
  if (!firstMeaningfulDeltaLogged) {
65962
66218
  firstMeaningfulDeltaLogged = true;
@@ -65979,7 +66235,7 @@ async function monitorTmuxRun(params) {
65979
66235
  initialSnapshot: params.initialSnapshot
65980
66236
  });
65981
66237
  }
65982
- if (sawActivity && now - lastActivityAt >= params.idleTimeoutMs) {
66238
+ if (!hasActiveTimer && (sawActivity || sawPaneChange || sawPromptSubmission) && now - lastPaneChangeAt >= params.idleTimeoutMs) {
65983
66239
  await params.onCompleted({
65984
66240
  snapshot: deriveInteractionText(params.initialSnapshot, previousSnapshot),
65985
66241
  fullSnapshot: previousSnapshot,
@@ -66000,9 +66256,20 @@ async function monitorTmuxRun(params) {
66000
66256
  // src/agents/session-service.ts
66001
66257
  var OBSERVER_RETRYABLE_FAILURE_LIMIT = 3;
66002
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;
66003
66261
  function formatObserverError(error) {
66004
66262
  return error instanceof Error ? error.stack ?? error.message : String(error);
66005
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
+ }
66006
66273
  function listObserverErrorCodes(error) {
66007
66274
  const codes = new Set;
66008
66275
  const visit = (value) => {
@@ -66074,6 +66341,7 @@ class SessionService {
66074
66341
  runnerSessions;
66075
66342
  resolveTarget;
66076
66343
  activeRuns = new Map;
66344
+ nextRunId = 1;
66077
66345
  stopping = false;
66078
66346
  constructor(tmux, sessionState, runnerSessions, resolveTarget) {
66079
66347
  this.tmux = tmux;
@@ -66087,42 +66355,10 @@ class SessionService {
66087
66355
  if (!entry.runtime || entry.runtime.state === "idle") {
66088
66356
  continue;
66089
66357
  }
66090
- const resolved = this.resolveTarget({
66358
+ await this.reconcilePersistedActiveRun({
66091
66359
  agentId: entry.agentId,
66092
66360
  sessionKey: entry.sessionKey
66093
66361
  });
66094
- if (!await this.tmux.hasSession(resolved.sessionName)) {
66095
- await this.sessionState.setSessionRuntime(resolved, {
66096
- state: "idle"
66097
- });
66098
- continue;
66099
- }
66100
- const fullSnapshot = normalizePaneText(await this.tmux.capturePane(resolved.sessionName, resolved.stream.captureLines));
66101
- const initialResult = createDeferred();
66102
- const update = this.createRunUpdate({
66103
- resolved,
66104
- status: entry.runtime.state === "detached" ? "detached" : "running",
66105
- snapshot: deriveInteractionText("", fullSnapshot),
66106
- fullSnapshot,
66107
- initialSnapshot: "",
66108
- note: entry.runtime.state === "detached" ? this.buildDetachedNote(resolved) : undefined
66109
- });
66110
- this.activeRuns.set(resolved.sessionKey, {
66111
- resolved,
66112
- observers: new Map,
66113
- observerFailures: new Map,
66114
- initialResult,
66115
- latestUpdate: update,
66116
- steeringReady: true,
66117
- startedAt: entry.runtime.startedAt ?? Date.now()
66118
- });
66119
- this.startRunMonitor(resolved.sessionKey, {
66120
- prompt: undefined,
66121
- initialSnapshot: fullSnapshot,
66122
- startedAt: entry.runtime.startedAt ?? Date.now(),
66123
- detachedAlready: entry.runtime.state === "detached",
66124
- timingContext: undefined
66125
- });
66126
66362
  }
66127
66363
  }
66128
66364
  async executePrompt(target, prompt, observer, options = {}) {
@@ -66133,21 +66369,15 @@ class SessionService {
66133
66369
  if (existingActiveRun) {
66134
66370
  throw new ActiveRunInProgressError(existingActiveRun.latestUpdate);
66135
66371
  }
66136
- const existingEntry = await this.sessionState.getEntry(target.sessionKey);
66137
- if (existingEntry?.runtime?.state && existingEntry.runtime.state !== "idle") {
66138
- const resolvedExisting = this.resolveTarget(target);
66139
- throw new ActiveRunInProgressError(this.createRunUpdate({
66140
- resolved: resolvedExisting,
66141
- status: existingEntry.runtime.state === "detached" ? "detached" : "running",
66142
- snapshot: "",
66143
- fullSnapshot: "",
66144
- initialSnapshot: "",
66145
- 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."
66146
- }));
66372
+ const reconciledRun = await this.reconcilePersistedActiveRun(target);
66373
+ if (reconciledRun) {
66374
+ throw new ActiveRunInProgressError(reconciledRun.latestUpdate);
66147
66375
  }
66148
66376
  const initialResult = createDeferred();
66149
66377
  const provisionalResolved = this.resolveTarget(target);
66378
+ const runId = this.allocateRunId();
66150
66379
  this.activeRuns.set(provisionalResolved.sessionKey, {
66380
+ runId,
66151
66381
  resolved: provisionalResolved,
66152
66382
  observers: new Map([[observer.id, { ...observer }]]),
66153
66383
  observerFailures: new Map,
@@ -66195,6 +66425,7 @@ class SessionService {
66195
66425
  startedAt
66196
66426
  });
66197
66427
  this.startRunMonitor(resolved.sessionKey, {
66428
+ runId,
66198
66429
  prompt,
66199
66430
  initialSnapshot,
66200
66431
  startedAt,
@@ -66203,12 +66434,12 @@ class SessionService {
66203
66434
  });
66204
66435
  return initialResult.promise;
66205
66436
  } catch (error) {
66206
- await this.failActiveRun(provisionalResolved.sessionKey, error);
66437
+ await this.failActiveRun(provisionalResolved.sessionKey, runId, error);
66207
66438
  throw error;
66208
66439
  }
66209
66440
  }
66210
66441
  async observeRun(target, observer) {
66211
- const existingRun = this.activeRuns.get(target.sessionKey);
66442
+ const existingRun = this.activeRuns.get(target.sessionKey) ?? await this.reconcilePersistedActiveRun(target);
66212
66443
  if (existingRun) {
66213
66444
  existingRun.observers.set(observer.id, {
66214
66445
  ...observer
@@ -66262,6 +66493,29 @@ class SessionService {
66262
66493
  canSteerActiveRun(target) {
66263
66494
  return this.activeRuns.get(target.sessionKey)?.steeringReady ?? false;
66264
66495
  }
66496
+ async submitSessionInput(target, text) {
66497
+ const result = await this.runnerSessions.submitSessionInput(target, text);
66498
+ const run = this.activeRuns.get(target.sessionKey);
66499
+ if (!run) {
66500
+ return result;
66501
+ }
66502
+ const startedAt = Date.now();
66503
+ run.startedAt = startedAt;
66504
+ if (run.latestUpdate.status === "detached") {
66505
+ run.latestUpdate = this.createRunUpdate({
66506
+ resolved: run.resolved,
66507
+ status: "running",
66508
+ snapshot: run.latestUpdate.snapshot,
66509
+ fullSnapshot: run.latestUpdate.fullSnapshot,
66510
+ initialSnapshot: run.latestUpdate.initialSnapshot
66511
+ });
66512
+ }
66513
+ await this.sessionState.setSessionRuntime(run.resolved, {
66514
+ state: "running",
66515
+ startedAt
66516
+ });
66517
+ return result;
66518
+ }
66265
66519
  async stop() {
66266
66520
  this.stopping = true;
66267
66521
  const activeRuns = [...this.activeRuns.values()];
@@ -66348,8 +66602,8 @@ class SessionService {
66348
66602
  }
66349
66603
  }
66350
66604
  }
66351
- async finishActiveRun(sessionKey, update) {
66352
- const run = this.activeRuns.get(sessionKey);
66605
+ async finishActiveRun(sessionKey, runId, update) {
66606
+ const run = this.getRun(sessionKey, runId);
66353
66607
  if (!run) {
66354
66608
  return;
66355
66609
  }
@@ -66360,8 +66614,8 @@ class SessionService {
66360
66614
  run.initialResult.resolve(update);
66361
66615
  this.activeRuns.delete(run.resolved.sessionKey);
66362
66616
  }
66363
- async failActiveRun(sessionKey, error) {
66364
- const run = this.activeRuns.get(sessionKey);
66617
+ async failActiveRun(sessionKey, runId, error) {
66618
+ const run = this.getRun(sessionKey, runId);
66365
66619
  if (!run) {
66366
66620
  return;
66367
66621
  }
@@ -66387,7 +66641,7 @@ class SessionService {
66387
66641
  if (!this.runnerSessions.canRecoverMidRun(error)) {
66388
66642
  return false;
66389
66643
  }
66390
- const run = this.activeRuns.get(sessionKey);
66644
+ const run = this.getRun(sessionKey, params.runId);
66391
66645
  if (!run) {
66392
66646
  return true;
66393
66647
  }
@@ -66404,7 +66658,7 @@ class SessionService {
66404
66658
  }));
66405
66659
  try {
66406
66660
  const recovered = await this.runnerSessions.reopenRunContext(target, params.timingContext);
66407
- const currentRun = this.activeRuns.get(sessionKey);
66661
+ const currentRun = this.getRun(sessionKey, params.runId);
66408
66662
  if (!currentRun) {
66409
66663
  return true;
66410
66664
  }
@@ -66420,6 +66674,7 @@ class SessionService {
66420
66674
  });
66421
66675
  await this.notifyRunObservers(currentRun, currentRun.latestUpdate);
66422
66676
  this.startRunMonitor(sessionKey, {
66677
+ runId: currentRun.runId,
66423
66678
  prompt: MID_RUN_RECOVERY_CONTINUE_PROMPT,
66424
66679
  initialSnapshot: recovered.initialSnapshot,
66425
66680
  startedAt: currentRun.startedAt,
@@ -66432,11 +66687,12 @@ class SessionService {
66432
66687
  } catch (reopenError) {
66433
66688
  if (recoveryAttempt < MID_RUN_RECOVERY_MAX_ATTEMPTS && this.runnerSessions.canRecoverMidRun(reopenError)) {
66434
66689
  return await this.recoverLostMidRun(sessionKey, {
66690
+ runId: params.runId,
66435
66691
  timingContext: params.timingContext,
66436
66692
  recoveryAttempt: recoveryAttempt + 1
66437
66693
  }, reopenError);
66438
66694
  }
66439
- const currentRun = this.activeRuns.get(sessionKey);
66695
+ const currentRun = this.getRun(sessionKey, params.runId);
66440
66696
  if (!currentRun) {
66441
66697
  return true;
66442
66698
  }
@@ -66444,15 +66700,15 @@ class SessionService {
66444
66700
  try {
66445
66701
  await this.runnerSessions.startFreshSession(target, params.timingContext);
66446
66702
  } catch (freshError) {
66447
- 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));
66448
66704
  return true;
66449
66705
  }
66450
- await this.failActiveRun(sessionKey, new Error(buildRunRecoveryNote("fresh-required")));
66706
+ await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("fresh-required")));
66451
66707
  return true;
66452
66708
  }
66453
66709
  }
66454
66710
  startRunMonitor(sessionKey, params) {
66455
- const run = this.activeRuns.get(sessionKey);
66711
+ const run = this.getRun(sessionKey, params.runId);
66456
66712
  if (!run) {
66457
66713
  return;
66458
66714
  }
@@ -66476,14 +66732,14 @@ class SessionService {
66476
66732
  detachedAlready: params.detachedAlready,
66477
66733
  timingContext: params.timingContext,
66478
66734
  onPromptSubmitted: async () => {
66479
- const currentRun = this.activeRuns.get(sessionKey);
66735
+ const currentRun = this.getRun(sessionKey, params.runId);
66480
66736
  if (!currentRun) {
66481
66737
  return;
66482
66738
  }
66483
66739
  currentRun.steeringReady = true;
66484
66740
  },
66485
66741
  onRunning: async (update) => {
66486
- const currentRun = this.activeRuns.get(sessionKey);
66742
+ const currentRun = this.getRun(sessionKey, params.runId);
66487
66743
  if (!currentRun) {
66488
66744
  return;
66489
66745
  }
@@ -66499,7 +66755,7 @@ class SessionService {
66499
66755
  }));
66500
66756
  },
66501
66757
  onDetached: async (update) => {
66502
- const currentRun = this.activeRuns.get(sessionKey);
66758
+ const currentRun = this.getRun(sessionKey, params.runId);
66503
66759
  if (!currentRun) {
66504
66760
  return;
66505
66761
  }
@@ -66521,27 +66777,111 @@ class SessionService {
66521
66777
  currentRun.initialResult.resolve(detachedUpdate);
66522
66778
  },
66523
66779
  onCompleted: async (update) => {
66780
+ const currentRun = this.getRun(sessionKey, params.runId);
66781
+ if (!currentRun) {
66782
+ return;
66783
+ }
66524
66784
  const runUpdate = this.createRunUpdate({
66525
- resolved: run.resolved,
66785
+ resolved: currentRun.resolved,
66526
66786
  status: "completed",
66527
66787
  snapshot: mergeRunSnapshot(params.snapshotPrefix ?? "", update.snapshot),
66528
66788
  fullSnapshot: update.fullSnapshot,
66529
66789
  initialSnapshot: update.initialSnapshot
66530
66790
  });
66531
- await this.finishActiveRun(sessionKey, runUpdate);
66791
+ await this.finishActiveRun(sessionKey, params.runId, runUpdate);
66532
66792
  }
66533
66793
  });
66534
66794
  } catch (error) {
66535
66795
  if (await this.recoverLostMidRun(sessionKey, {
66796
+ runId: params.runId,
66536
66797
  timingContext: params.timingContext,
66537
66798
  recoveryAttempt: (params.recoveryAttempt ?? 0) + 1
66538
66799
  }, error)) {
66539
66800
  return;
66540
66801
  }
66541
- 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));
66542
66803
  }
66543
66804
  })();
66544
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
+ }
66545
66885
  }
66546
66886
 
66547
66887
  // src/agents/agent-service.ts
@@ -66706,7 +67046,7 @@ class AgentService {
66706
67046
  return this.activeRuns.canSteerActiveRun(target);
66707
67047
  }
66708
67048
  async submitSessionInput(target, text) {
66709
- return this.runnerSessions.submitSessionInput(target, text);
67049
+ return this.activeRuns.submitSessionInput(target, text);
66710
67050
  }
66711
67051
  isSessionBusy(target) {
66712
67052
  return this.activeRuns.hasActiveRun(target) || this.queue.isBusy(target.sessionKey);
@@ -66889,7 +67229,8 @@ class AgentService {
66889
67229
  timingContext: callbacks.timingContext,
66890
67230
  onUpdate: callbacks.onUpdate
66891
67231
  }), {
66892
- text: prompt
67232
+ text: prompt,
67233
+ canStart: async () => !this.activeRuns.hasActiveRun(target)
66893
67234
  });
66894
67235
  }
66895
67236
  getMaxMessageChars(agentId) {
@@ -69221,6 +69562,7 @@ async function clearSlackAssistantThreadStatus(client, target) {
69221
69562
  }
69222
69563
 
69223
69564
  // src/channels/processing-indicator.ts
69565
+ var ACTIVE_RUN_WAIT_POLL_INTERVAL_MS = 250;
69224
69566
  function shouldResolveIndicatorWait(update) {
69225
69567
  return isTerminalRunStatus(update.status);
69226
69568
  }
@@ -69247,6 +69589,18 @@ async function waitForProcessingIndicatorLifecycle(params) {
69247
69589
  settled = true;
69248
69590
  resolve();
69249
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
+ })();
69250
69604
  try {
69251
69605
  const observation = await params.agentService.observeRun(params.sessionTarget, {
69252
69606
  id: params.observerId,
@@ -74523,7 +74877,7 @@ function extractLinuxProcState(raw) {
74523
74877
  function getEditableConfigPath5() {
74524
74878
  return process.env.CLISBOT_CONFIG_PATH;
74525
74879
  }
74526
- function parseOptionValue(args, name) {
74880
+ function parseOptionValue2(args, name) {
74527
74881
  const index = args.findIndex((arg) => arg === name);
74528
74882
  if (index === -1) {
74529
74883
  return;
@@ -74534,6 +74888,21 @@ function parseOptionValue(args, name) {
74534
74888
  }
74535
74889
  return value;
74536
74890
  }
74891
+ function parseAliasedOptionValue(args, names, label) {
74892
+ const values = names.flatMap((name) => {
74893
+ const value = parseOptionValue2(args, name);
74894
+ return value === undefined ? [] : [{ name, value }];
74895
+ });
74896
+ if (values.length === 0) {
74897
+ return;
74898
+ }
74899
+ const distinctValues = Array.from(new Set(values.map((entry) => entry.value)));
74900
+ if (distinctValues.length > 1) {
74901
+ const seen = values.map((entry) => `${entry.name}=${entry.value}`).join(", ");
74902
+ throw new Error(`Conflicting values for ${label}: ${seen}`);
74903
+ }
74904
+ return values[values.length - 1]?.value;
74905
+ }
74537
74906
  function hasFlag2(args, name) {
74538
74907
  return args.includes(name);
74539
74908
  }
@@ -74564,13 +74933,14 @@ function renderAccountsHelp() {
74564
74933
  "Usage:",
74565
74934
  " clisbot accounts --help",
74566
74935
  " clisbot accounts help",
74567
- " clisbot accounts add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
74568
- " clisbot accounts add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
74936
+ " clisbot accounts add telegram --account <id> (--token | --telegram-bot-token) <ENV_NAME|${ENV_NAME}|literal> [--persist]",
74937
+ " clisbot accounts add slack --account <id> (--app-token | --slack-app-token) <ENV_NAME|${ENV_NAME}|literal> (--bot-token | --slack-bot-token) <ENV_NAME|${ENV_NAME}|literal> [--persist]",
74569
74938
  " clisbot accounts persist --channel <slack|telegram> --account <id>",
74570
74939
  " clisbot accounts persist --all",
74571
74940
  "",
74572
74941
  "Notes:",
74573
74942
  " - env-style input such as `TELEGRAM_BOT_TOKEN` or `${TELEGRAM_BOT_TOKEN}` keeps the account env-backed in config",
74943
+ " - `accounts add` accepts both the short account-local flags and the bootstrap-style channel flags",
74574
74944
  " - literal token input without `--persist` stays runtime-only and requires a running clisbot runtime",
74575
74945
  " - `--persist` writes canonical token files so later plain `clisbot start` can reuse the account safely",
74576
74946
  " - `persist --all` converts every configured `credentialType=mem` account into canonical token files"
@@ -74578,8 +74948,8 @@ function renderAccountsHelp() {
74578
74948
  `);
74579
74949
  }
74580
74950
  async function addTelegramAccount(args, deps) {
74581
- const accountId = parseOptionValue(args, "--account") ?? "default";
74582
- const token = parseTokenInput(parseOptionValue(args, "--token") ?? "");
74951
+ const accountId = parseOptionValue2(args, "--account") ?? "default";
74952
+ const token = parseTokenInput(parseAliasedOptionValue(args, ["--token", "--telegram-bot-token"], "telegram bot token") ?? "");
74583
74953
  const persist = hasFlag2(args, "--persist");
74584
74954
  const runtimeStatus = await deps.getRuntimeStatus();
74585
74955
  if (token.kind === "mem" && !persist && !runtimeStatus.running) {
@@ -74615,9 +74985,9 @@ async function addTelegramAccount(args, deps) {
74615
74985
  console.log(`config: ${configPath}`);
74616
74986
  }
74617
74987
  async function addSlackAccount(args, deps) {
74618
- const accountId = parseOptionValue(args, "--account") ?? "default";
74619
- const appToken = parseTokenInput(parseOptionValue(args, "--app-token") ?? "");
74620
- const botToken = parseTokenInput(parseOptionValue(args, "--bot-token") ?? "");
74988
+ const accountId = parseOptionValue2(args, "--account") ?? "default";
74989
+ const appToken = parseTokenInput(parseAliasedOptionValue(args, ["--app-token", "--slack-app-token"], "slack app token") ?? "");
74990
+ const botToken = parseTokenInput(parseAliasedOptionValue(args, ["--bot-token", "--slack-bot-token"], "slack bot token") ?? "");
74621
74991
  const persist = hasFlag2(args, "--persist");
74622
74992
  const runtimeStatus = await deps.getRuntimeStatus();
74623
74993
  if (appToken.kind !== botToken.kind) {
@@ -74772,8 +75142,8 @@ async function runAccountsCli(args, deps = {}) {
74772
75142
  await persistAllConfiguredAccounts(resolvedDeps);
74773
75143
  return;
74774
75144
  }
74775
- const provider = parseOptionValue(args, "--channel");
74776
- const accountId = parseOptionValue(args, "--account") ?? "default";
75145
+ const provider = parseOptionValue2(args, "--channel");
75146
+ const accountId = parseOptionValue2(args, "--account") ?? "default";
74777
75147
  if (provider !== "slack" && provider !== "telegram") {
74778
75148
  throw new Error(renderAccountsHelp());
74779
75149
  }
@@ -75273,7 +75643,7 @@ function parseResponseModeTarget(channel, raw) {
75273
75643
  }
75274
75644
  return target;
75275
75645
  }
75276
- function parseOptionValue2(args, name) {
75646
+ function parseOptionValue3(args, name) {
75277
75647
  const index = args.findIndex((arg) => arg === name);
75278
75648
  if (index === -1) {
75279
75649
  return;
@@ -75285,7 +75655,7 @@ function parseOptionValue2(args, name) {
75285
75655
  return value;
75286
75656
  }
75287
75657
  function parseBooleanOption(args, name, fallback) {
75288
- const raw = parseOptionValue2(args, name);
75658
+ const raw = parseOptionValue3(args, name);
75289
75659
  if (!raw) {
75290
75660
  return fallback;
75291
75661
  }
@@ -75298,7 +75668,7 @@ function parseBooleanOption(args, name, fallback) {
75298
75668
  throw new Error(`${name} requires true or false`);
75299
75669
  }
75300
75670
  function getAgentId(args) {
75301
- return parseOptionValue2(args, "--agent") ?? "default";
75671
+ return parseOptionValue3(args, "--agent") ?? "default";
75302
75672
  }
75303
75673
  async function setChannelEnabled(action, channel) {
75304
75674
  const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
@@ -75322,7 +75692,7 @@ async function addTelegramGroup(args) {
75322
75692
  throw new Error("Usage: clisbot channels add telegram-group <chatId> [--topic <topicId>] [--agent <id>] [--require-mention true|false]");
75323
75693
  }
75324
75694
  const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
75325
- const topicId = parseOptionValue2(args, "--topic");
75695
+ const topicId = parseOptionValue3(args, "--topic");
75326
75696
  const agentId = getAgentId(args);
75327
75697
  const requireMention = parseBooleanOption(args, "--require-mention", true);
75328
75698
  const groupRoute = config.channels.telegram.groups[chatId] ?? {
@@ -75375,7 +75745,7 @@ async function removeTelegramGroup(args) {
75375
75745
  throw new Error("Usage: clisbot channels remove telegram-group <chatId> [--topic <topicId>]");
75376
75746
  }
75377
75747
  const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
75378
- const topicId = parseOptionValue2(args, "--topic");
75748
+ const topicId = parseOptionValue3(args, "--topic");
75379
75749
  const groupRoute = config.channels.telegram.groups[chatId];
75380
75750
  if (!groupRoute) {
75381
75751
  console.log(`telegram group route ${chatId} is not configured`);
@@ -75517,9 +75887,9 @@ async function runResponseModeCli(args) {
75517
75887
  }
75518
75888
  const responseMode = action === "set" ? parseResponseMode2(args[1]) : undefined;
75519
75889
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
75520
- const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
75521
- const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
75522
- const topic = parseOptionValue2(optionArgs, "--topic");
75890
+ const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
75891
+ const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
75892
+ const topic = parseOptionValue3(optionArgs, "--topic");
75523
75893
  if (channel === "slack" && topic) {
75524
75894
  throw new Error("Slack response-mode commands do not support --topic");
75525
75895
  }
@@ -75551,9 +75921,9 @@ async function runAdditionalMessageModeCli(args) {
75551
75921
  }
75552
75922
  const additionalMessageMode = action === "set" ? parseAdditionalMessageMode2(args[1]) : undefined;
75553
75923
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
75554
- const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
75555
- const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
75556
- const topic = parseOptionValue2(optionArgs, "--topic");
75924
+ const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
75925
+ const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
75926
+ const topic = parseOptionValue3(optionArgs, "--topic");
75557
75927
  if (channel === "slack" && topic) {
75558
75928
  throw new Error("Slack additional-message-mode commands do not support --topic");
75559
75929
  }
@@ -75843,7 +76213,7 @@ function parseRepeatedOption3(args, name) {
75843
76213
  }
75844
76214
  return values;
75845
76215
  }
75846
- function parseOptionValue3(args, name) {
76216
+ function parseOptionValue4(args, name) {
75847
76217
  const values = parseRepeatedOption3(args, name);
75848
76218
  return values.length > 0 ? values.at(-1) : undefined;
75849
76219
  }
@@ -75856,7 +76226,7 @@ function parseMessageBodyFileOption(args) {
75856
76226
  return bodyFileValues.at(-1) ?? messageFileValues.at(-1);
75857
76227
  }
75858
76228
  function parseIntegerOption(args, name) {
75859
- const raw = parseOptionValue3(args, name);
76229
+ const raw = parseOptionValue4(args, name);
75860
76230
  if (!raw) {
75861
76231
  return;
75862
76232
  }
@@ -75885,34 +76255,34 @@ function parseMessageCommand(args) {
75885
76255
  }
75886
76256
  const action = rawAction;
75887
76257
  const rest = args.slice(1);
75888
- const channel = parseOptionValue3(rest, "--channel");
76258
+ const channel = parseOptionValue4(rest, "--channel");
75889
76259
  if (channel !== "slack" && channel !== "telegram") {
75890
76260
  throw new Error("--channel <slack|telegram> is required");
75891
76261
  }
75892
76262
  return {
75893
76263
  action,
75894
76264
  channel,
75895
- account: parseOptionValue3(rest, "--account"),
75896
- target: parseOptionValue3(rest, "--target"),
75897
- message: parseOptionValue3(rest, "--message") ?? parseOptionValue3(rest, "-m"),
76265
+ account: parseOptionValue4(rest, "--account"),
76266
+ target: parseOptionValue4(rest, "--target"),
76267
+ message: parseOptionValue4(rest, "--message") ?? parseOptionValue4(rest, "-m"),
75898
76268
  messageFile: parseMessageBodyFileOption(rest),
75899
- media: parseOptionValue3(rest, "--media"),
75900
- messageId: parseOptionValue3(rest, "--message-id"),
75901
- emoji: parseOptionValue3(rest, "--emoji"),
76269
+ media: parseOptionValue4(rest, "--media"),
76270
+ messageId: parseOptionValue4(rest, "--message-id"),
76271
+ emoji: parseOptionValue4(rest, "--emoji"),
75902
76272
  remove: hasFlag4(rest, "--remove"),
75903
- threadId: parseOptionValue3(rest, "--thread-id"),
75904
- replyTo: parseOptionValue3(rest, "--reply-to"),
76273
+ threadId: parseOptionValue4(rest, "--thread-id"),
76274
+ replyTo: parseOptionValue4(rest, "--reply-to"),
75905
76275
  limit: parseIntegerOption(rest, "--limit"),
75906
- query: parseOptionValue3(rest, "--query"),
75907
- pollQuestion: parseOptionValue3(rest, "--poll-question"),
76276
+ query: parseOptionValue4(rest, "--query"),
76277
+ pollQuestion: parseOptionValue4(rest, "--poll-question"),
75908
76278
  pollOptions: parseRepeatedOption3(rest, "--poll-option"),
75909
76279
  forceDocument: hasFlag4(rest, "--force-document"),
75910
76280
  silent: hasFlag4(rest, "--silent"),
75911
76281
  progress: hasFlag4(rest, "--progress"),
75912
76282
  final: hasFlag4(rest, "--final"),
75913
76283
  json: hasFlag4(rest, "--json"),
75914
- inputFormat: parseMessageInputFormat(parseOptionValue3(rest, "--input")),
75915
- renderMode: parseMessageRenderMode(parseOptionValue3(rest, "--render"))
76284
+ inputFormat: parseMessageInputFormat(parseOptionValue4(rest, "--input")),
76285
+ renderMode: parseMessageRenderMode(parseOptionValue4(rest, "--render"))
75916
76286
  };
75917
76287
  }
75918
76288
  function renderMessageHelp() {
@@ -76017,160 +76387,214 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
76017
76387
  dependencies.print(JSON.stringify(execution.result, null, 2));
76018
76388
  }
76019
76389
 
76020
- // src/control/channel-bootstrap-flags.ts
76021
- function isLiteralToken(token) {
76022
- return token?.kind === "mem";
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
+ }
76023
76397
  }
76024
- function parseBotType(rawValue) {
76025
- const value = rawValue.trim().toLowerCase();
76026
- if (value === "personal" || value === "personal-assistant") {
76027
- return "personal-assistant";
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;
76028
76411
  }
76029
- if (value === "team" || value === "team-assistant") {
76030
- return "team-assistant";
76412
+ if (command.name === "help" || command.name === "version") {
76413
+ return;
76031
76414
  }
76032
- throw new Error(`Invalid bot type: ${rawValue}`);
76415
+ throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
76033
76416
  }
76034
- function parseOptionValue4(args, name, index) {
76035
- const value = args[index + 1]?.trim();
76036
- if (!value) {
76037
- throw new Error(`Missing value for ${name}`);
76038
- }
76039
- return value;
76417
+ function getCliErrorExitCode(error) {
76418
+ return error instanceof CliCommandError ? error.exitCode : 1;
76040
76419
  }
76041
- function getOrCreateSlackAccount(accounts, accountId) {
76042
- let account = accounts.find((entry) => entry.accountId === accountId);
76043
- if (!account) {
76044
- account = { accountId };
76045
- accounts.push(account);
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);
76046
76442
  }
76047
- return account;
76443
+ return values;
76048
76444
  }
76049
- function getOrCreateTelegramAccount(accounts, accountId) {
76050
- let account = accounts.find((entry) => entry.accountId === accountId);
76051
- if (!account) {
76052
- account = { accountId };
76053
- accounts.push(account);
76445
+ function parseSingleOption3(args, name) {
76446
+ const values = parseRepeatedOption4(args, name);
76447
+ if (values.length === 0) {
76448
+ return;
76054
76449
  }
76055
- return account;
76450
+ return values[values.length - 1];
76056
76451
  }
76057
- function ensureUniqueAccount(accounts, accountId, flagName) {
76058
- if (accounts.some((entry) => entry.accountId === accountId)) {
76059
- throw new Error(`Duplicate ${flagName} ${accountId}`);
76060
- }
76452
+ function hasFlag5(args, name) {
76453
+ return args.includes(name);
76061
76454
  }
76062
- function validateSlackAccount(account) {
76063
- if (!account.appToken || !account.botToken) {
76064
- throw new Error(`Slack account ${account.accountId} requires both app token and bot token`);
76455
+ function isOneOf(value, allowed) {
76456
+ return allowed.includes(value);
76457
+ }
76458
+ function parseTimeoutMs(raw) {
76459
+ if (!raw) {
76460
+ return;
76065
76461
  }
76066
- if (account.appToken.kind !== account.botToken.kind) {
76067
- throw new Error(`Slack account ${account.accountId} must use one credential source kind for both app and bot tokens`);
76462
+ const parsed = Number.parseInt(raw, 10);
76463
+ if (!Number.isInteger(parsed) || parsed <= 0) {
76464
+ throw new CliCommandError("Invalid value for --timeout-ms", 2);
76068
76465
  }
76466
+ return parsed;
76069
76467
  }
76070
- function validateTelegramAccount(account) {
76071
- if (!account.botToken) {
76072
- throw new Error(`Telegram account ${account.accountId} requires a bot token`);
76073
- }
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
+ `);
76074
76493
  }
76075
- function parseBootstrapFlags(args) {
76076
- const slackAccounts = [];
76077
- const telegramAccounts = [];
76078
- let currentSlackAccountId;
76079
- let currentTelegramAccountId;
76080
- let cliTool;
76081
- let bootstrap;
76082
- let persist = false;
76083
- let sawCredentialFlags = false;
76084
- let sawSlackFlags = false;
76085
- let sawTelegramFlags = false;
76086
- for (let index = 0;index < args.length; index += 1) {
76087
- const arg = args[index];
76088
- if (arg === "--cli") {
76089
- cliTool = parseOptionValue4(args, arg, index);
76090
- index += 1;
76091
- continue;
76092
- }
76093
- if (arg === "--bootstrap") {
76094
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
76095
- index += 1;
76096
- continue;
76097
- }
76098
- if (arg === "--bot-type") {
76099
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
76100
- index += 1;
76101
- continue;
76102
- }
76103
- if (arg === "--persist") {
76104
- persist = true;
76105
- continue;
76106
- }
76107
- if (arg === "--slack-account") {
76108
- const accountId = parseOptionValue4(args, arg, index);
76109
- ensureUniqueAccount(slackAccounts, accountId, "--slack-account");
76110
- currentSlackAccountId = accountId;
76111
- getOrCreateSlackAccount(slackAccounts, accountId);
76112
- sawSlackFlags = true;
76113
- index += 1;
76114
- continue;
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);
76115
76519
  }
76116
- if (arg === "--telegram-account") {
76117
- const accountId = parseOptionValue4(args, arg, index);
76118
- ensureUniqueAccount(telegramAccounts, accountId, "--telegram-account");
76119
- currentTelegramAccountId = accountId;
76120
- getOrCreateTelegramAccount(telegramAccounts, accountId);
76121
- sawTelegramFlags = true;
76122
- index += 1;
76123
- continue;
76520
+ if (!suite) {
76521
+ throw new CliCommandError("--backend all requires --suite launch-trio", 2);
76124
76522
  }
76125
- if (arg === "--slack-app-token") {
76126
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
76127
- const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
76128
- account.appToken = token;
76129
- sawCredentialFlags = true;
76130
- sawSlackFlags = true;
76131
- index += 1;
76132
- continue;
76523
+ } else {
76524
+ if (suite) {
76525
+ throw new CliCommandError(`--suite is only valid with --backend all`, 2);
76133
76526
  }
76134
- if (arg === "--slack-bot-token") {
76135
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
76136
- const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
76137
- account.botToken = token;
76138
- sawCredentialFlags = true;
76139
- sawSlackFlags = true;
76140
- index += 1;
76141
- continue;
76527
+ if (!scenario) {
76528
+ throw new CliCommandError(`--backend ${backend} requires --scenario`, 2);
76142
76529
  }
76143
- if (arg === "--telegram-bot-token") {
76144
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
76145
- const account = getOrCreateTelegramAccount(telegramAccounts, currentTelegramAccountId ?? "default");
76146
- account.botToken = token;
76147
- sawCredentialFlags = true;
76148
- sawTelegramFlags = true;
76149
- index += 1;
76150
- continue;
76151
- }
76152
- throw new Error(`Unknown option for start/init: ${arg}`);
76153
76530
  }
76154
- for (const account of slackAccounts) {
76155
- validateSlackAccount(account);
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;
76156
76569
  }
76157
- for (const account of telegramAccounts) {
76158
- validateTelegramAccount(account);
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
+ `));
76159
76584
  }
76160
- return {
76161
- cliTool,
76162
- bootstrap,
76163
- persist,
76164
- slackAccounts,
76165
- telegramAccounts,
76166
- sawCredentialFlags,
76167
- sawSlackFlags,
76168
- sawTelegramFlags,
76169
- literalWarnings: []
76170
- };
76585
+ process.exitCode = 3;
76171
76586
  }
76172
- function hasLiteralBootstrapCredentials(flags) {
76173
- return flags.slackAccounts.some((account) => isLiteralToken(account.appToken) || isLiteralToken(account.botToken)) || flags.telegramAccounts.some((account) => isLiteralToken(account.botToken));
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);
76174
76598
  }
76175
76599
 
76176
76600
  // src/control/activity-store.ts
@@ -76413,10 +76837,11 @@ function appendBootstrapGuidance(lines, summary) {
76413
76837
  lines.push("");
76414
76838
  lines.push("Guidance:");
76415
76839
  for (const agent of pendingBootstrap) {
76840
+ const botType = agent.bootstrapMode === "team-assistant" ? "team" : "personal";
76416
76841
  if (agent.bootstrapState === "missing") {
76417
76842
  lines.push(` Agent ${agent.id} is missing bootstrap files.`);
76418
76843
  lines.push(` workspace: ${agent.workspacePath}`);
76419
- lines.push(` run: clisbot agents bootstrap ${agent.id} --mode ${agent.bootstrapMode}`);
76844
+ lines.push(` run: clisbot agents bootstrap ${agent.id} --bot-type ${botType}`);
76420
76845
  continue;
76421
76846
  }
76422
76847
  lines.push(` Agent ${agent.id} still needs bootstrap completion.`);
@@ -76707,27 +77132,6 @@ function renderOperatorErrorWithHelpLines(error) {
76707
77132
  ];
76708
77133
  }
76709
77134
 
76710
- // src/control/runtime-cli-shared.ts
76711
- function printCommandOutcomeBanner(outcome) {
76712
- console.log("");
76713
- console.log("+---------+");
76714
- console.log(outcome === "success" ? "| SUCCESS |" : "| FAILED |");
76715
- console.log("+---------+");
76716
- console.log("");
76717
- }
76718
- function printCommandOutcomeFooter(outcome) {
76719
- printCommandOutcomeBanner(outcome);
76720
- }
76721
- function assertSupportedPlatform(command) {
76722
- if (process.platform !== "win32") {
76723
- return;
76724
- }
76725
- if (command.name === "help" || command.name === "version") {
76726
- return;
76727
- }
76728
- throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
76729
- }
76730
-
76731
77135
  // src/control/runtime-bootstrap-cli.ts
76732
77136
  function hasHelpFlag(args) {
76733
77137
  return args.includes("--help") || args.includes("-h") || args.includes("help");
@@ -77817,6 +78221,10 @@ async function runControlCommand(command) {
77817
78221
  await runAuthCli(command.args);
77818
78222
  return true;
77819
78223
  }
78224
+ if (command.name === "runner") {
78225
+ await runRunnerCli(command.args);
78226
+ return true;
78227
+ }
77820
78228
  if (command.name === "pairing") {
77821
78229
  await runPairingCli(command.args);
77822
78230
  return true;
@@ -77838,5 +78246,5 @@ try {
77838
78246
  printCommandOutcomeBanner("failure");
77839
78247
  }
77840
78248
  await printCliError(error);
77841
- process.exit(1);
78249
+ process.exit(getCliErrorExitCode(error));
77842
78250
  }