@rallycry/conveyor-agent 10.6.0 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2308,6 +2308,9 @@ var SpawnTaskSessionRequestSchema = z.object({
2308
2308
  taskId: z.string(),
2309
2309
  kind: z.enum(["tui", "shell"])
2310
2310
  });
2311
+ var SpawnTaskReviewRequestSchema = z.object({
2312
+ taskId: z.string()
2313
+ });
2311
2314
  var ReportSessionSpawnFailureRequestSchema = z.object({
2312
2315
  sessionId: z.string(),
2313
2316
  spawnedSessionId: z.string(),
@@ -2499,6 +2502,9 @@ var ListMyLiveSessionsRequestSchema = z2.object({
2499
2502
  /** Admin-only: list another member's sessions instead of the caller's. */
2500
2503
  targetUserId: z2.string().optional()
2501
2504
  });
2505
+ var ListActiveSessionUsersRequestSchema = z2.object({
2506
+ projectId: z2.string()
2507
+ });
2502
2508
  var GetProjectAvailableTuisRequestSchema = z2.object({
2503
2509
  projectId: z2.string()
2504
2510
  });
@@ -3024,13 +3030,12 @@ var ClaudeCodeHarness = class {
3024
3030
  }
3025
3031
  createMcpServer(config) {
3026
3032
  const sdkTools = config.tools.map(
3027
- (t) => tool(
3028
- t.name,
3029
- t.description,
3030
- t.schema,
3031
- t.handler,
3032
- t.annotations ? { annotations: t.annotations } : void 0
3033
- )
3033
+ (t) => tool(t.name, t.description, t.schema, t.handler, {
3034
+ ...t.annotations ? { annotations: t.annotations } : {},
3035
+ // Preloads the tool into the prompt (skips ToolSearch deferral) via
3036
+ // `_meta['anthropic/alwaysLoad']` on the SDK MCP transport.
3037
+ ...t.alwaysLoad ? { alwaysLoad: true } : {}
3038
+ })
3034
3039
  );
3035
3040
  return createSdkMcpServer({ name: config.name, tools: sdkTools });
3036
3041
  }
@@ -3858,7 +3863,16 @@ var PtyToolServer = class {
3858
3863
  const inputSchema = tool2.strict ? z3.strictObject(tool2.schema) : tool2.schema;
3859
3864
  register(
3860
3865
  tool2.name,
3861
- { description: tool2.description, inputSchema },
3866
+ {
3867
+ description: tool2.description,
3868
+ inputSchema,
3869
+ // The spawned CLI reads `_meta['anthropic/alwaysLoad']` from the
3870
+ // `tools/list` response and preloads the tool into the prompt
3871
+ // instead of deferring it behind ToolSearch. Per-tool (vs per-server
3872
+ // `alwaysLoad` in the mcp-config) so only the hot protocol tools pay
3873
+ // the prompt cost while the long tail stays deferred.
3874
+ ...tool2.alwaysLoad ? { _meta: { "anthropic/alwaysLoad": true } } : {}
3875
+ },
3862
3876
  (args) => tool2.handler(args)
3863
3877
  );
3864
3878
  }
@@ -4116,18 +4130,48 @@ async function readRaw(path4) {
4116
4130
  return null;
4117
4131
  }
4118
4132
  }
4133
+ var READ_BACK_DELAYS_MS = [250, 500, 1e3, 2e3];
4134
+ var defaultSleep = (ms) => new Promise((resolve) => {
4135
+ setTimeout(resolve, ms);
4136
+ });
4137
+ async function writeWithReadBackRetry(io2, contents, delaysMs = READ_BACK_DELAYS_MS) {
4138
+ const sleep4 = io2.sleep ?? defaultSleep;
4139
+ for (let attempt = 0; ; attempt++) {
4140
+ await io2.write(contents);
4141
+ if (await io2.read() === contents) return true;
4142
+ if (attempt >= delaysMs.length) return false;
4143
+ await sleep4(delaysMs[attempt]);
4144
+ }
4145
+ }
4146
+ function fsWriteIo(path4, mode) {
4147
+ return {
4148
+ write: (contents) => writeFile4(path4, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
4149
+ read: () => readRaw(path4)
4150
+ };
4151
+ }
4119
4152
  async function ensureClaudeCredentials(env = process.env) {
4153
+ const isCloud = isConveyorCloudEnv(env);
4154
+ const token = env.CLAUDE_CODE_OAUTH_TOKEN;
4155
+ if (isCloud && token) {
4156
+ await sanitizeApprovedApiKeys(token);
4157
+ }
4120
4158
  try {
4121
4159
  const path4 = claudeCredentialsPath();
4122
4160
  const plan = planCredentialsWrite({
4123
- isCloud: isConveyorCloudEnv(env),
4124
- token: env.CLAUDE_CODE_OAUTH_TOKEN,
4161
+ isCloud,
4162
+ token,
4125
4163
  existingRaw: await readRaw(path4),
4126
4164
  now: Date.now()
4127
4165
  });
4128
4166
  if (plan.action === "skip") return;
4129
4167
  await mkdir2(claudeConfigHome(), { recursive: true });
4130
- await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
4168
+ const verified = await writeWithReadBackRetry(fsWriteIo(path4, 384), plan.contents);
4169
+ if (!verified) {
4170
+ process.stderr.write(
4171
+ `[conveyor-agent] claude credentials read-back still stale after retries at ${path4} \u2014 TUI may land on the login picker
4172
+ `
4173
+ );
4174
+ }
4131
4175
  await chmod2(path4, 384).catch(() => {
4132
4176
  });
4133
4177
  } catch (err) {
@@ -4136,6 +4180,41 @@ async function ensureClaudeCredentials(env = process.env) {
4136
4180
  `);
4137
4181
  }
4138
4182
  }
4183
+ function apiKeyApprovalSuffix(token) {
4184
+ return token.slice(-20);
4185
+ }
4186
+ function planApprovedApiKeyCleanup(existingRaw, oauthToken) {
4187
+ if (!oauthToken) return null;
4188
+ const config = parseClaudeJson(existingRaw);
4189
+ const responses = config.customApiKeyResponses;
4190
+ if (typeof responses !== "object" || responses === null || Array.isArray(responses)) {
4191
+ return null;
4192
+ }
4193
+ const record = responses;
4194
+ const approved = record.approved;
4195
+ if (!Array.isArray(approved)) return null;
4196
+ const suffix = apiKeyApprovalSuffix(oauthToken);
4197
+ const filtered = approved.filter((entry) => entry !== suffix);
4198
+ if (filtered.length === approved.length) return null;
4199
+ record.approved = filtered;
4200
+ return JSON.stringify(config);
4201
+ }
4202
+ async function sanitizeApprovedApiKeys(oauthToken) {
4203
+ try {
4204
+ const path4 = claudeJsonPath();
4205
+ const cleaned = planApprovedApiKeyCleanup(await readRaw(path4), oauthToken);
4206
+ if (cleaned === null) return;
4207
+ const verified = await writeWithReadBackRetry(fsWriteIo(path4), cleaned);
4208
+ process.stderr.write(
4209
+ verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${path4} \u2014 CLI may still see the poisoned entry
4210
+ `
4211
+ );
4212
+ } catch (err) {
4213
+ const message = err instanceof Error ? err.message : String(err);
4214
+ process.stderr.write(`[conveyor-agent] claude approved-key sanitize failed: ${message}
4215
+ `);
4216
+ }
4217
+ }
4139
4218
  function claudeJsonPath() {
4140
4219
  const configDir = process.env.CLAUDE_CONFIG_DIR;
4141
4220
  return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
@@ -4187,16 +4266,16 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4187
4266
  const path4 = claudeJsonPath();
4188
4267
  const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
4189
4268
  if (contents === null) return;
4190
- await writeFile4(path4, contents, "utf8");
4191
- const verify = await readRaw(path4);
4192
- if (verify === contents) {
4269
+ const verified = await writeWithReadBackRetry(fsWriteIo(path4), contents);
4270
+ if (verified) {
4193
4271
  process.stderr.write(
4194
4272
  `[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
4195
4273
  `
4196
4274
  );
4197
4275
  } else {
4276
+ const verify = await readRaw(path4);
4198
4277
  process.stderr.write(
4199
- `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4278
+ `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4200
4279
  `
4201
4280
  );
4202
4281
  }
@@ -4255,7 +4334,10 @@ function resolvePlanDialogTiming() {
4255
4334
  return {
4256
4335
  firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
4257
4336
  intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
4258
- slowIntervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS", PLAN_DIALOG_SLOW_INTERVAL_MS),
4337
+ slowIntervalMs: envMs(
4338
+ "CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS",
4339
+ PLAN_DIALOG_SLOW_INTERVAL_MS
4340
+ ),
4259
4341
  fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
4260
4342
  windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
4261
4343
  };
@@ -4289,6 +4371,9 @@ function inheritedEnv(socketPath) {
4289
4371
  for (const [key, value] of Object.entries(process.env)) {
4290
4372
  if (typeof value === "string") env[key] = value;
4291
4373
  }
4374
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
4375
+ delete env.ANTHROPIC_API_KEY;
4376
+ }
4292
4377
  if (socketPath) {
4293
4378
  env.CONVEYOR_HOOK_SOCKET = socketPath;
4294
4379
  }
@@ -6136,7 +6221,7 @@ Environment (fully ready \u2014 do NOT verify or set up):`,
6136
6221
  `- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`, \`bun run --cwd\`); never spend a tool call on a bare \`cd\`.`,
6137
6222
  `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
6138
6223
  `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. Anything longer: launch it with run_in_background and STOP \u2014 a completion notification arrives when it finishes. Never busy-wait with sleep/pgrep/tail loops (foreground shell commands are killed at ~2 minutes), and never re-run the suite to escape a wait that looks stalled.`,
6139
- `- Deferred tools have no schema loaded until fetched \u2014 before first use, load it via ToolSearch (query "select:<ToolName>", e.g. "select:Monitor"); never guess a deferred tool's parameters.`,
6224
+ `- The core mcp__conveyor__* tools are preloaded \u2014 call them directly; do NOT spend a ToolSearch call on them. For any tool that IS still deferred (schema not loaded), load ALL the schemas you expect to need in ONE ToolSearch call using fully-qualified names (query "select:Monitor,mcp__conveyor__<name>" \u2014 bare MCP tool names without the mcp__<server>__ prefix do not match); never guess a deferred tool's parameters.`,
6140
6225
  `- The \`gh\` CLI may not be installed \u2014 use the mcp__conveyor__* tools for PR and task state.`,
6141
6226
  `- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
6142
6227
  `
@@ -6389,9 +6474,10 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
6389
6474
  }
6390
6475
  if (isPm) {
6391
6476
  return [
6392
- `You are the project manager for this task.`,
6477
+ `You are the project manager for this task \u2014 a thoughtful, collaborative planner, not the implementer.`,
6478
+ `Your job right now is to help the team turn this request into a clear, well-scoped plan. Be a helpful planning partner: think out loud, surface trade-offs, and keep the human in the loop.`,
6393
6479
  `Read the task description and chat history carefully \u2014 the team has provided the initial context below. Acknowledge what they've asked for and respond in chat before taking silent tool actions.`,
6394
- `Start planning now \u2014 explore the codebase, ask clarifying questions if needed, and draft a plan. Do not wait for additional team input before engaging; the initial message IS the team engaging.`,
6480
+ `Start planning now \u2014 explore the codebase, ask clarifying questions if anything is ambiguous, and draft a plan. Do not wait for additional team input before engaging; the initial message IS the team engaging.`,
6395
6481
  `When you finish planning, save the plan with update_task_plan, post a summary of the plan for the team with post_to_chat (your turn output is NOT shown in chat), then end your turn. A separate task agent will execute the plan after review.`
6396
6482
  ];
6397
6483
  }
@@ -7804,6 +7890,28 @@ function getModeTools(agentMode, connection, config, context) {
7804
7890
  return config.mode === "pm" ? buildPmTools(connection, { includePackTools: false }) : [];
7805
7891
  }
7806
7892
  }
7893
+ var ALWAYS_LOADED_TOOLS = /* @__PURE__ */ new Set([
7894
+ // Every mode/session
7895
+ "post_to_chat",
7896
+ "read_task_chat",
7897
+ "get_task",
7898
+ "get_current_plan",
7899
+ "set_manual_tests",
7900
+ "create_pull_request",
7901
+ // Planning/auto/building
7902
+ "update_task_plan",
7903
+ "update_task_properties",
7904
+ // Review mode
7905
+ "approve_code_review",
7906
+ "request_code_changes",
7907
+ // Pack/parent orchestration
7908
+ "list_subtasks"
7909
+ ]);
7910
+ function withAlwaysLoad(tools) {
7911
+ return tools.map(
7912
+ (tool2) => ALWAYS_LOADED_TOOLS.has(tool2.name) ? { ...tool2, alwaysLoad: true } : tool2
7913
+ );
7914
+ }
7807
7915
  function buildConveyorTools(connection, config, context, agentMode) {
7808
7916
  const effectiveMode = agentMode ?? context?.agentMode ?? void 0;
7809
7917
  const commonTools = buildCommonTools(connection, config);
@@ -7811,7 +7919,13 @@ function buildConveyorTools(connection, config, context, agentMode) {
7811
7919
  const discoveryTools = effectiveMode === "discovery" || effectiveMode === "auto" || effectiveMode === "building" ? buildDiscoveryTools(connection) : [];
7812
7920
  const codeReviewTools = effectiveMode === "review" ? buildCodeReviewTools(connection) : [];
7813
7921
  const emergencyTools = [buildForceUpdateTaskStatusTool(connection)];
7814
- return [...commonTools, ...modeTools, ...discoveryTools, ...codeReviewTools, ...emergencyTools];
7922
+ return withAlwaysLoad([
7923
+ ...commonTools,
7924
+ ...modeTools,
7925
+ ...discoveryTools,
7926
+ ...codeReviewTools,
7927
+ ...emergencyTools
7928
+ ]);
7815
7929
  }
7816
7930
  function createConveyorMcpServer(harness, connection, config, context, agentMode) {
7817
7931
  return harness.createMcpServer({
@@ -8417,7 +8531,6 @@ async function handleExitPlanMode(host, input) {
8417
8531
  if (gate) return gate;
8418
8532
  if (host.agentMode === "discovery") {
8419
8533
  host.hasExitedPlanMode = true;
8420
- host.discoveryCompleted = true;
8421
8534
  try {
8422
8535
  await host.connection.triggerIdentification();
8423
8536
  } catch (triggerErr) {
@@ -8426,13 +8539,8 @@ async function handleExitPlanMode(host, input) {
8426
8539
  );
8427
8540
  }
8428
8541
  await host.connection.postChatMessageAwait(
8429
- "Planning complete \u2014 awaiting team approval. Icon and agent assignment will be set automatically."
8542
+ "Plan posted \u2014 switching to auto mode. I'll start building now; send a message any time to steer or add follow-ups."
8430
8543
  );
8431
- if (host.harnessKind === "pty") {
8432
- setTimeout(() => host.requestStop(), 250);
8433
- } else {
8434
- host.requestStop();
8435
- }
8436
8544
  return { behavior: "allow", updatedInput: input };
8437
8545
  }
8438
8546
  try {
@@ -8477,19 +8585,6 @@ async function handleAskUserQuestion(host, input) {
8477
8585
  }
8478
8586
  return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
8479
8587
  }
8480
- var EXPLORATION_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob"]);
8481
- function trackExploration(tracker, toolName, input) {
8482
- if (!EXPLORATION_TOOLS.has(toolName)) return null;
8483
- if (toolName === "Read") {
8484
- const filePath = String(input.file_path ?? "");
8485
- if (filePath) return tracker.recordFileRead(filePath);
8486
- }
8487
- if (toolName === "Grep" || toolName === "Glob") {
8488
- const pattern = String(input.pattern ?? "");
8489
- if (pattern) return tracker.recordSearch(pattern);
8490
- }
8491
- return null;
8492
- }
8493
8588
  var DENIAL_WARNING_THRESHOLD = 3;
8494
8589
  var DENIAL_FORCE_STOP_THRESHOLD = 8;
8495
8590
  function handleDenialEscalation(host, consecutiveDenials) {
@@ -8535,12 +8630,6 @@ function buildCanUseTool(host) {
8535
8630
  return await handleAskUserQuestion(host, input);
8536
8631
  }
8537
8632
  const result = resolveToolAccess(host, toolName, input);
8538
- if (result.behavior === "allow" && host.explorationTracker && !host.hasExitedPlanMode) {
8539
- const signal = trackExploration(host.explorationTracker, toolName, input);
8540
- if (signal) {
8541
- host.connection.postChatMessage(signal.message);
8542
- }
8543
- }
8544
8633
  if (result.behavior === "deny") {
8545
8634
  consecutiveDenials++;
8546
8635
  handleDenialEscalation(host, consecutiveDenials);
@@ -8717,7 +8806,7 @@ function resolvePromptDelivery(inputs) {
8717
8806
  if (inputs.runnerMode === "code-review") return "submit";
8718
8807
  if (inputs.isFollowUp || inputs.hasExistingSession) return "submit";
8719
8808
  if (inputs.isAuto || inputs.agentMode === "auto") return "submit";
8720
- if (inputs.agentMode !== "discovery" && inputs.agentMode !== "help") return "submit";
8809
+ if (inputs.agentMode !== "help") return "submit";
8721
8810
  return "prefill";
8722
8811
  }
8723
8812
  function isReadOnlyMode(mode, hasExitedPlanMode) {
@@ -9195,108 +9284,6 @@ async function runWithRetry(initialQuery, context, host, options) {
9195
9284
  }
9196
9285
  }
9197
9286
 
9198
- // src/execution/exploration-tracker.ts
9199
- var FILE_REREAD_NUDGE_L1 = 3;
9200
- var FILE_REREAD_NUDGE_L2 = 5;
9201
- var SEARCH_REPEAT_NUDGE_L1 = 3;
9202
- var SEARCH_REPEAT_NUDGE_L2 = 5;
9203
- var RATIO_MIN_READS_L1 = 10;
9204
- var RATIO_THRESHOLD_L1 = 0.4;
9205
- var RATIO_MIN_READS_L2 = 15;
9206
- var RATIO_THRESHOLD_L2 = 0.3;
9207
- function normalizePath(filePath) {
9208
- let normalized = filePath.replace(/^\.\//, "");
9209
- normalized = normalized.replace(/\/+/g, "/");
9210
- return normalized;
9211
- }
9212
- var ExplorationTracker = class {
9213
- fileReadCounts = /* @__PURE__ */ new Map();
9214
- searchCounts = /* @__PURE__ */ new Map();
9215
- totalReads = 0;
9216
- highestNudgeEmitted = 0;
9217
- complexityMultiplier;
9218
- constructor(isParentTask) {
9219
- this.complexityMultiplier = isParentTask ? 1.5 : 1;
9220
- }
9221
- /** Record a file read and return a staleness signal if thresholds are crossed. */
9222
- recordFileRead(filePath) {
9223
- const normalized = normalizePath(filePath);
9224
- const count = (this.fileReadCounts.get(normalized) ?? 0) + 1;
9225
- this.fileReadCounts.set(normalized, count);
9226
- this.totalReads++;
9227
- return this.checkFileRereadSignal(normalized, count) ?? this.checkExplorationRatio();
9228
- }
9229
- /** Record a search pattern and return a staleness signal if thresholds are crossed. */
9230
- recordSearch(pattern) {
9231
- const count = (this.searchCounts.get(pattern) ?? 0) + 1;
9232
- this.searchCounts.set(pattern, count);
9233
- return this.checkSearchRepeatSignal(pattern, count);
9234
- }
9235
- scaledThreshold(base) {
9236
- return Math.ceil(base * this.complexityMultiplier);
9237
- }
9238
- checkFileRereadSignal(filePath, count) {
9239
- const l2 = this.scaledThreshold(FILE_REREAD_NUDGE_L2);
9240
- const l1 = this.scaledThreshold(FILE_REREAD_NUDGE_L1);
9241
- if (count >= l2 && this.highestNudgeEmitted < 2) {
9242
- this.highestNudgeEmitted = 2;
9243
- return {
9244
- level: 2,
9245
- message: `\u26A0\uFE0F Exploration warning: You've read "${filePath}" ${count} times, suggesting analysis paralysis. Write your plan with the context you have and call ExitPlanMode. If something specific is blocking you, describe the blocker in your plan.`
9246
- };
9247
- }
9248
- if (count >= l1 && this.highestNudgeEmitted < 1) {
9249
- this.highestNudgeEmitted = 1;
9250
- return {
9251
- level: 1,
9252
- message: `Exploration note: You've read "${filePath}" ${count} times. Consider whether you have enough context to start writing your plan. If you're still exploring, try looking at different files.`
9253
- };
9254
- }
9255
- return null;
9256
- }
9257
- checkSearchRepeatSignal(pattern, count) {
9258
- const l2 = this.scaledThreshold(SEARCH_REPEAT_NUDGE_L2);
9259
- const l1 = this.scaledThreshold(SEARCH_REPEAT_NUDGE_L1);
9260
- const displayPattern = pattern.length > 60 ? pattern.slice(0, 57) + "..." : pattern;
9261
- if (count >= l2 && this.highestNudgeEmitted < 2) {
9262
- this.highestNudgeEmitted = 2;
9263
- return {
9264
- level: 2,
9265
- message: `\u26A0\uFE0F Exploration warning: You've searched for "${displayPattern}" ${count} times. Repeated searches for the same pattern suggest you may be stuck. Proceed with the information you have.`
9266
- };
9267
- }
9268
- if (count >= l1 && this.highestNudgeEmitted < 1) {
9269
- this.highestNudgeEmitted = 1;
9270
- return {
9271
- level: 1,
9272
- message: `Exploration note: You've searched for "${displayPattern}" ${count} times. If you're not finding what you need, try a different approach or broader pattern.`
9273
- };
9274
- }
9275
- return null;
9276
- }
9277
- checkExplorationRatio() {
9278
- const uniqueReads = this.fileReadCounts.size;
9279
- const ratio = this.totalReads > 0 ? uniqueReads / this.totalReads : 1;
9280
- const minReadsL2 = this.scaledThreshold(RATIO_MIN_READS_L2);
9281
- if (this.totalReads >= minReadsL2 && ratio < RATIO_THRESHOLD_L2 && this.highestNudgeEmitted < 2) {
9282
- this.highestNudgeEmitted = 2;
9283
- return {
9284
- level: 2,
9285
- message: `\u26A0\uFE0F Exploration warning: Only ${Math.round(ratio * 100)}% of your ${this.totalReads} file reads are unique. This indicates significant re-reading. Write your plan with the context you have.`
9286
- };
9287
- }
9288
- const minReadsL1 = this.scaledThreshold(RATIO_MIN_READS_L1);
9289
- if (this.totalReads >= minReadsL1 && ratio < RATIO_THRESHOLD_L1 && this.highestNudgeEmitted < 1) {
9290
- this.highestNudgeEmitted = 1;
9291
- return {
9292
- level: 1,
9293
- message: `Exploration note: ${Math.round(ratio * 100)}% of your ${this.totalReads} file reads are unique. You're re-reading files more than exploring new ones. Consider starting your plan.`
9294
- };
9295
- }
9296
- return null;
9297
- }
9298
- };
9299
-
9300
9287
  // src/runner/query-bridge.ts
9301
9288
  var logger3 = createServiceLogger("QueryBridge");
9302
9289
  function resolveHarnessKind() {
@@ -9480,7 +9467,6 @@ var QueryBridge = class {
9480
9467
  harness: this.harness,
9481
9468
  harnessKind: this.harnessKind,
9482
9469
  setupLog: [],
9483
- explorationTracker: bridge.mode.effectiveMode === "discovery" || bridge.mode.effectiveMode === "auto" && !bridge.mode.hasExitedPlanMode ? new ExplorationTracker(bridge._isParentTask) : null,
9484
9470
  sessionIds: this.sessionIds,
9485
9471
  pendingToolOutputs: this.pendingToolOutputs,
9486
9472
  // Live getters/setters delegating to ModeController + bridge state
@@ -9969,9 +9955,7 @@ function isHeavyGateActive() {
9969
9955
  }
9970
9956
 
9971
9957
  // src/runner/session-runner.ts
9972
- function isPostToChatTool(name) {
9973
- return typeof name === "string" && (name === "post_to_chat" || name.endsWith("__post_to_chat"));
9974
- }
9958
+ var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery"]);
9975
9959
  var SessionRunner = class _SessionRunner {
9976
9960
  connection;
9977
9961
  mode;
@@ -9996,13 +9980,6 @@ var SessionRunner = class _SessionRunner {
9996
9980
  queryBridge = null;
9997
9981
  inputResolver = null;
9998
9982
  pendingMessages = [];
9999
- prNudgeCount = 0;
10000
- /** Set when the agent posts a substantive message to the task chat (the
10001
- * `post_to_chat` tool) during the current turn; reset at the start of every
10002
- * query. Read by the stuck-detection: an auto agent that finished without a
10003
- * PR but DID explain itself / ask for help in chat is waiting on a human,
10004
- * not silently stuck, so it is left attached rather than nudged. */
10005
- agentSpokeThisTurn = false;
10006
9983
  /** Guards overlapping runs of the periodic git flush. */
10007
9984
  periodicFlushInFlight = false;
10008
9985
  /** Consecutive flush ticks skipped because a heavy gate was running. */
@@ -10192,9 +10169,6 @@ var SessionRunner = class _SessionRunner {
10192
10169
  );
10193
10170
  this.queryBridge.isDiscoveryCompleted = false;
10194
10171
  }
10195
- if (!this.stopped && this.pendingMessages.length === 0) {
10196
- await this.maybeHandleStuckAuto();
10197
- }
10198
10172
  if (!this.stopped) {
10199
10173
  process.stderr.write(
10200
10174
  `[conveyor-agent] Listening for messages (mode: ${this.mode.effectiveMode})
@@ -10270,9 +10244,6 @@ var SessionRunner = class _SessionRunner {
10270
10244
  this.interrupted = false;
10271
10245
  continue;
10272
10246
  }
10273
- if (!this.stopped && this.pendingMessages.length === 0) {
10274
- await this.maybeHandleStuckAuto();
10275
- }
10276
10247
  if (!this.stopped) await this.setState("idle");
10277
10248
  } else if (this._state === "error") {
10278
10249
  await this.setState("idle");
@@ -10328,7 +10299,7 @@ var SessionRunner = class _SessionRunner {
10328
10299
  return true;
10329
10300
  }
10330
10301
  const delivery = this.pendingMessages.length > 0 ? "submit" : intrinsicDelivery;
10331
- const shouldRun = effectiveMode === "building" || effectiveMode === "auto" || effectiveMode === "review" || delivery === "prefill";
10302
+ const shouldRun = AUTO_RUN_MODES.has(effectiveMode) || delivery === "prefill";
10332
10303
  if (!shouldRun) {
10333
10304
  await this.setState("idle");
10334
10305
  return false;
@@ -10463,7 +10434,6 @@ var SessionRunner = class _SessionRunner {
10463
10434
  /** Run queryBridge.execute, swallowing abort errors from stop/softStop. */
10464
10435
  async executeQuery(followUpContent, promptDelivery) {
10465
10436
  if (!this.fullContext || !this.queryBridge) return;
10466
- this.agentSpokeThisTurn = false;
10467
10437
  try {
10468
10438
  await this.queryBridge.execute(this.fullContext, followUpContent, promptDelivery);
10469
10439
  } catch (err) {
@@ -10485,7 +10455,6 @@ var SessionRunner = class _SessionRunner {
10485
10455
  /** Run queryBridge.executePassive, swallowing abort errors from stop/softStop. */
10486
10456
  async executePassive() {
10487
10457
  if (!this.fullContext || !this.queryBridge) return;
10488
- this.agentSpokeThisTurn = false;
10489
10458
  try {
10490
10459
  await this.queryBridge.executePassive(this.fullContext);
10491
10460
  } catch (err) {
@@ -10627,119 +10596,6 @@ var SessionRunner = class _SessionRunner {
10627
10596
  resolver(null);
10628
10597
  }
10629
10598
  }
10630
- // ── Auto-mode stuck detection & nudge ───────────────────────────────
10631
- static MAX_STUCK_NUDGES = 3;
10632
- /** The build-phase nudge (In Progress, no PR). Offers BOTH escape routes so
10633
- * the agent never just goes idle. */
10634
- static STUCK_PROMPT = "You are in Auto mode, your task is still In Progress, and there is no open pull request. Do ONE of these right now: (1) open a pull request with the create_pull_request tool, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
10635
- /** The pre-build nudge (still Planning/Open, identification/plan unfinished).
10636
- * Same two-escape-route shape, aimed at landing the plan + continuing to build. */
10637
- static STUCK_PROMPT_PLANNING = "You are in Auto mode and the card is still in a pre-build status \u2014 identification and/or the plan are not finished. Do ONE of these right now: (1) set the story points, save the plan with the update_task_plan tool, and continue implementing, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
10638
- /**
10639
- * Classify how an auto task is "stuck" after its turn ended (call sites run
10640
- * post-turn, so the TUI is no longer working), or null if it isn't. Shared
10641
- * guards: only auto, not rate-limited, under the nudge cap, and the agent
10642
- * stayed silent this turn — if it posted to chat (explained itself / asked for
10643
- * help) it's waiting on a human, not silently stuck.
10644
- *
10645
- * - "pr": In Progress with no open PR (build finished without shipping).
10646
- * - "planning": still in a pre-build status (Planning/Open) without
10647
- * (identified + saved plan) — e.g. the server-side InProgress bump failed
10648
- * and the agent idled before documenting its plan. An agent that idles
10649
- * here would otherwise clean-exit → its session Ends → the workspace is
10650
- * reaped "agent_gone" before a plan ever landed. Nudging (and, on
10651
- * exhaustion, going dormant) keeps the session Active so the pod is never
10652
- * prematurely slept.
10653
- */
10654
- autoStuckKind() {
10655
- if (!this.mode.isAuto || this.stopped) return null;
10656
- if (this.queryBridge?.wasRateLimited) return null;
10657
- if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return null;
10658
- if (this.agentSpokeThisTurn) return null;
10659
- if (!this.taskContext) return null;
10660
- const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
10661
- if (status === "InProgress" && !githubPRUrl) return "pr";
10662
- if (PRE_BUILD_TASK_STATUSES.has(status) && !(storyPointId !== null && !!plan?.trim())) {
10663
- return "planning";
10664
- }
10665
- return null;
10666
- }
10667
- isAutoStuck() {
10668
- return this.autoStuckKind() !== null;
10669
- }
10670
- /**
10671
- * Stop driving the agent and route the core loop into dormant idle —
10672
- * connected and attachable, but NOT running (so no tokens are spent while it
10673
- * waits). A human reply (chat or TUI keystroke after the next wake) resumes
10674
- * it. This replaces the old hard `stop()`: the auto agent must never kill its
10675
- * own session, so a person can always take over.
10676
- */
10677
- enterDormantForHumanTakeover(message) {
10678
- this.connection.postChatMessage(message);
10679
- this.completedThisTurn = true;
10680
- void this.connection.sendHeartbeat();
10681
- }
10682
- async refreshTaskContext() {
10683
- try {
10684
- const ctx = await this.connection.call("getTaskContext", {
10685
- sessionId: this.sessionId,
10686
- includeHistory: false
10687
- });
10688
- this.taskContext = {
10689
- status: ctx.status,
10690
- plan: ctx.plan,
10691
- storyPointId: ctx.storyPoints === null || ctx.storyPoints === void 0 ? null : String(ctx.storyPoints),
10692
- model: ctx.model,
10693
- githubPRUrl: ctx.githubPRUrl
10694
- };
10695
- if (this.fullContext) {
10696
- this.fullContext.status = ctx.status;
10697
- this.fullContext.plan = ctx.plan;
10698
- this.fullContext.githubPRUrl = ctx.githubPRUrl;
10699
- this.fullContext.model = ctx.model;
10700
- }
10701
- } catch {
10702
- }
10703
- }
10704
- /** Prompt + chat-message label for a stuck-nudge of the given kind. */
10705
- stuckNudgeContent(kind) {
10706
- if (kind === "planning") {
10707
- return {
10708
- prompt: _SessionRunner.STUCK_PROMPT_PLANNING,
10709
- situation: "still Planning with no identified plan"
10710
- };
10711
- }
10712
- return { prompt: _SessionRunner.STUCK_PROMPT, situation: "still no PR" };
10713
- }
10714
- async maybeHandleStuckAuto() {
10715
- await this.refreshTaskContext();
10716
- if (!this.isAutoStuck()) return;
10717
- while (!this.stopped && !this.interrupted && this.isAutoStuck()) {
10718
- this.prNudgeCount++;
10719
- if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) {
10720
- this.enterDormantForHumanTakeover(
10721
- `Auto-mode: I couldn't finish or get unstuck after ${_SessionRunner.MAX_STUCK_NUDGES} attempts. Staying connected \u2014 reply here or in the terminal and I'll keep working.`
10722
- );
10723
- return;
10724
- }
10725
- const { prompt, situation } = this.stuckNudgeContent(this.autoStuckKind());
10726
- const attempt = this.prNudgeCount;
10727
- const chatMsg = attempt === 1 ? `Auto-nudge: ${situation} \u2014 prompting the agent to finish or explain where it's stuck...` : `Auto-nudge (attempt ${attempt}/${_SessionRunner.MAX_STUCK_NUDGES}): ${situation} \u2014 prompting the agent again...`;
10728
- this.connection.postChatMessage(chatMsg);
10729
- await this.setState("running");
10730
- await this.callbacks.onEvent({ type: "pr_nudge", prompt });
10731
- if (this.interrupted || this.stopped) return;
10732
- await this.executeQuery(prompt);
10733
- if (this.interrupted || this.stopped) return;
10734
- await this.refreshTaskContext();
10735
- if (this.agentSpokeThisTurn && !this.taskContext?.githubPRUrl) {
10736
- this.enterDormantForHumanTakeover(
10737
- "Auto-mode: the agent posted an update and is waiting for input. Staying connected \u2014 reply here or in the terminal to continue."
10738
- );
10739
- return;
10740
- }
10741
- }
10742
- }
10743
10599
  // ── Context & bridge construction ────────────────────────────────
10744
10600
  // oxlint-disable-next-line complexity
10745
10601
  buildFullContext(ctx) {
@@ -10796,10 +10652,6 @@ var SessionRunner = class _SessionRunner {
10796
10652
  return this.callbacks.onStatusChange(status);
10797
10653
  },
10798
10654
  onEvent: (event) => {
10799
- const evt = event;
10800
- if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
10801
- this.agentSpokeThisTurn = true;
10802
- }
10803
10655
  if (event.type === "completed") {
10804
10656
  this.completedThisTurn = true;
10805
10657
  void this.connection.sendHeartbeat();
@@ -11149,4 +11001,4 @@ export {
11149
11001
  runStartCommand,
11150
11002
  unshallowRepo
11151
11003
  };
11152
- //# sourceMappingURL=chunk-5FRDOFI4.js.map
11004
+ //# sourceMappingURL=chunk-TIEFSYIY.js.map