@rallycry/conveyor-agent 10.2.5 → 10.3.1

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.
@@ -2035,6 +2035,10 @@ var SessionStopRequestSchema = z.object({
2035
2035
  sessionId: z.string(),
2036
2036
  reason: z.string().optional()
2037
2037
  });
2038
+ var EndReviewSessionRequestSchema = z.object({
2039
+ sessionId: z.string(),
2040
+ reason: z.enum(["approved", "changes_requested", "finished"]).optional()
2041
+ });
2038
2042
  var ConnectAgentRequestSchema = z.object({
2039
2043
  sessionId: z.string()
2040
2044
  });
@@ -2647,9 +2651,15 @@ var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
2647
2651
  var TASK_CHAT_HISTORY_LIMIT = 20;
2648
2652
  var PM_CHAT_HISTORY_LIMIT = 40;
2649
2653
  var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
2654
+ var PRE_BUILD_TASK_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
2655
+ function hasTaskPlan(plan) {
2656
+ return !!plan?.trim();
2657
+ }
2658
+ function taskNeedsPlanning(task) {
2659
+ return !hasTaskPlan(task.plan) && PRE_BUILD_TASK_STATUSES.has(task.status) && !task.githubPRUrl;
2660
+ }
2650
2661
 
2651
2662
  // src/runner/mode-controller.ts
2652
- var FRESH_AUTO_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
2653
2663
  var ModeController = class {
2654
2664
  _mode;
2655
2665
  _hasExitedPlanMode = false;
@@ -2734,28 +2744,28 @@ var ModeController = class {
2734
2744
  return this._mode;
2735
2745
  }
2736
2746
  /**
2737
- * A FRESH auto task runs the read-only plan turn first (`--permission-mode plan`):
2738
- * resolveInitialMode leaves it in `auto` with `_hasExitedPlanMode = false`, and
2739
- * once the agent finishes it calls ExitPlanMode → building. Only tasks that have
2740
- * already progressed past the fresh stage bypass planning and build immediately
2741
- * with `--dangerously-skip-permissions`:
2747
+ * An auto task that still NEEDS PLANNING (no plan saved, pre-build status, no
2748
+ * PR see `taskNeedsPlanning` in @project/shared) runs the read-only plan turn
2749
+ * first (`--permission-mode plan`): resolveInitialMode leaves it in `auto` with
2750
+ * `_hasExitedPlanMode = false`, and once the agent finishes it calls
2751
+ * ExitPlanMode → building. Everything else bypasses planning and builds
2752
+ * immediately with `--dangerously-skip-permissions`:
2742
2753
  *
2743
- * - a build already underway (status past `Planning`/`Open`),
2744
- * - a PR already open (safety net if status lags),
2745
- * - a release (booted `InProgress`), or
2746
- * - a pod restart mid-build (DB `agentMode` stays `"auto"` but status advanced).
2754
+ * - a plan already on the card (e.g. a Planning card planned by a human/PM),
2755
+ * - a build already underway (status past `Planning`/`Open` releases booted
2756
+ * `InProgress`, pod restarts mid-build where DB `agentMode` stays `"auto"`),
2757
+ * - a PR already open (safety net if status lags).
2747
2758
  *
2748
2759
  * Runtime Build-after-Discovery never reaches this gate as `auto` — the server's
2749
- * `resolveModeToSend` rewrites it to `"building"` for any non-`Planning` task.
2760
+ * `resolveModeToSend` rewrites it to `"building"` for any task with a plan.
2750
2761
  */
2751
2762
  canBypassPlanning(context) {
2752
2763
  if (this._mode !== "auto") return false;
2753
- if (!FRESH_AUTO_STATUSES.has(context.status)) return true;
2754
- return !!context.githubPRUrl;
2764
+ return !taskNeedsPlanning(context);
2755
2765
  }
2756
2766
  // ── Mode transitions ───────────────────────────────────────────────
2757
2767
  /** Handle mode change from server */
2758
- handleModeChange(newMode) {
2768
+ handleModeChange(newMode, context) {
2759
2769
  if (newMode === this._mode) return { type: "noop" };
2760
2770
  if (this._runnerMode === "task" && newMode === "review") {
2761
2771
  this._mode = newMode;
@@ -2770,7 +2780,7 @@ var ModeController = class {
2770
2780
  if (this._runnerMode === "task" && newMode === "auto") {
2771
2781
  this._mode = newMode;
2772
2782
  this._isAuto = true;
2773
- this._hasExitedPlanMode = true;
2783
+ this._hasExitedPlanMode = !context || !taskNeedsPlanning(context);
2774
2784
  return { type: "restart_query", newMode: "auto" };
2775
2785
  }
2776
2786
  if (this._mode === "auto" && this._hasExitedPlanMode && newMode === "building") {
@@ -2941,6 +2951,11 @@ var Lifecycle = class {
2941
2951
  };
2942
2952
 
2943
2953
  // src/harness/types.ts
2954
+ function isExternalMcpStdioServer(value) {
2955
+ if (typeof value !== "object" || value === null) return false;
2956
+ const v = value;
2957
+ return v.type === "stdio" && typeof v.command === "string";
2958
+ }
2944
2959
  function defineTool(name, description, schema, handler, options) {
2945
2960
  return {
2946
2961
  name,
@@ -3905,6 +3920,15 @@ async function startToolServers(mcpServers, tempDir) {
3905
3920
  const servers = [];
3906
3921
  const config = {};
3907
3922
  for (const [name, handle] of Object.entries(mcpServers)) {
3923
+ if (isExternalMcpStdioServer(handle)) {
3924
+ config[name] = {
3925
+ type: "stdio",
3926
+ command: handle.command,
3927
+ ...handle.args ? { args: handle.args } : {},
3928
+ ...handle.env ? { env: handle.env } : {}
3929
+ };
3930
+ continue;
3931
+ }
3908
3932
  const tools = handle instanceof PtyMcpServer ? handle.tools : [];
3909
3933
  if (tools.length === 0) continue;
3910
3934
  const server = new PtyToolServer(name, tools);
@@ -4164,11 +4188,14 @@ var PLAN_DIALOG_INTERVAL_MS = 1500;
4164
4188
  var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
4165
4189
  var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
4166
4190
  var PLAN_DIALOG_WINDOW_MS = 9e4;
4191
+ function envMs(name, fallback) {
4192
+ const raw = Number(process.env[name]);
4193
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
4194
+ }
4195
+ function resolveSubmitSettleMs() {
4196
+ return envMs("CONVEYOR_PTY_SUBMIT_SETTLE_MS", SUBMIT_SETTLE_MS);
4197
+ }
4167
4198
  function resolveSubmitNudgeTiming() {
4168
- const envMs = (name, fallback) => {
4169
- const raw = Number(process.env[name]);
4170
- return Number.isFinite(raw) && raw > 0 ? raw : fallback;
4171
- };
4172
4199
  return {
4173
4200
  intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
4174
4201
  slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
@@ -4176,6 +4203,15 @@ function resolveSubmitNudgeTiming() {
4176
4203
  windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
4177
4204
  };
4178
4205
  }
4206
+ function resolvePlanDialogTiming() {
4207
+ return {
4208
+ firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
4209
+ intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
4210
+ slowIntervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS", PLAN_DIALOG_SLOW_INTERVAL_MS),
4211
+ fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
4212
+ windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
4213
+ };
4214
+ }
4179
4215
  function turnOptionsFrom(options) {
4180
4216
  return {
4181
4217
  canUseTool: options.canUseTool,
@@ -4727,7 +4763,7 @@ var PtySession = class {
4727
4763
  if (text === "" && !this.adapter.capabilities.prefill) return;
4728
4764
  this.writeStdin(this.adapter.encodePromptBytes(text));
4729
4765
  if (this.turn.promptDelivery === "prefill") return;
4730
- await sleep3(SUBMIT_SETTLE_MS);
4766
+ await sleep3(resolveSubmitSettleMs());
4731
4767
  if (this._toreDown) return;
4732
4768
  this.writeStdin("\r");
4733
4769
  if (this.adapter.capabilities.structuredEvents) this.armSubmitNudge();
@@ -4844,10 +4880,11 @@ var PtySession = class {
4844
4880
  if (this.pendingPlanApproval) return;
4845
4881
  this.pendingPlanApproval = true;
4846
4882
  const startedAt = Date.now();
4883
+ const { firstPressMs, intervalMs, slowIntervalMs, fastWindowMs, windowMs } = resolvePlanDialogTiming();
4847
4884
  const press = () => {
4848
4885
  if (this._toreDown || !this.pendingPlanApproval) return;
4849
4886
  const elapsed = Date.now() - startedAt;
4850
- if (elapsed >= PLAN_DIALOG_WINDOW_MS) {
4887
+ if (elapsed >= windowMs) {
4851
4888
  process.stderr.write(
4852
4889
  "[PtySession] plan-dialog auto-accept window expired without ExitPlanMode executing \u2014 the approval dialog may still be parked\n"
4853
4890
  );
@@ -4855,10 +4892,10 @@ var PtySession = class {
4855
4892
  return;
4856
4893
  }
4857
4894
  this.writeStdin("\r");
4858
- const interval = elapsed < PLAN_DIALOG_FAST_WINDOW_MS ? PLAN_DIALOG_INTERVAL_MS : PLAN_DIALOG_SLOW_INTERVAL_MS;
4895
+ const interval = elapsed < fastWindowMs ? intervalMs : slowIntervalMs;
4859
4896
  this.planApprovalTimer = setTimeout(press, interval);
4860
4897
  };
4861
- this.planApprovalTimer = setTimeout(press, PLAN_DIALOG_FIRST_PRESS_MS);
4898
+ this.planApprovalTimer = setTimeout(press, firstPressMs);
4862
4899
  }
4863
4900
  disarmPlanDialogAutoAccept() {
4864
4901
  this.pendingPlanApproval = false;
@@ -5141,7 +5178,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
5141
5178
  ` - "ReviewDev" / "Complete": Already done. Skip.`,
5142
5179
  ` - "Planning": Not ready. If blocking progress, notify team.`,
5143
5180
  ``,
5144
- `3. Fire ALL ready "Open" tasks whose dependencies are met, not just one. Independent tasks can run in parallel.`,
5181
+ `3. Fire ALL ready "Open" tasks whose dependencies are met, not just one \u2014 independent tasks run in parallel. There is a concurrency limit: if start_child_cloud_build returns a PACK_CHILD_LIMIT error, that is backpressure, not a failure. Merge or wait on in-flight children, then start more as slots free up.`,
5145
5182
  ``,
5146
5183
  `4. After merging a PR: run \`git pull origin ${context.baseBranch}\` then re-check list_subtasks \u2014 previously blocked tasks may now be ready.`,
5147
5184
  ``,
@@ -5150,9 +5187,9 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
5150
5187
  `6. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with force_update_task_status("Complete").`,
5151
5188
  ``,
5152
5189
  `## Important Rules`,
5153
- `- When dependencies are set on children, use them to determine execution order. Fire all ready tasks in parallel.`,
5190
+ `- When dependencies are set on children, use them to determine execution order. Fire all ready tasks in parallel (up to the PACK_CHILD_LIMIT backpressure \u2014 see the loop above).`,
5154
5191
  `- When NO dependencies are set on any children, fall back to ordinal order (one at a time). This preserves backward compatibility.`,
5155
- `- After firing builds OR when waiting on CI, explicitly state you are going idle. The system will disconnect you and relaunch when there's a status change.`,
5192
+ `- After firing builds OR when waiting on CI, explicitly state you are going idle. Go idle when waiting \u2014 the system wakes you (or relaunches this environment) when a child changes status.`,
5156
5193
  `- Do NOT attempt to write code yourself. Your role is coordination only.`,
5157
5194
  `- If a child is stuck in "InProgress" for an unusually long time, use get_execution_logs(childTaskId) to check its logs and escalate to the team if it appears stuck.`,
5158
5195
  `- You can use get_task(childTaskId) to get a child's full details including PR URL and branch.`,
@@ -6064,7 +6101,7 @@ Git safety \u2014 STRICT rules:`,
6064
6101
  function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
6065
6102
  const isPm = mode === "pm";
6066
6103
  const isPmActive = isPm && agentMode === "building";
6067
- const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
6104
+ const isPackRunner = mode === "pack" || isPm && !!config.isAuto && !!context.isParentTask;
6068
6105
  if (isPackRunner) {
6069
6106
  return buildPackRunnerSystemPrompt(context, config, setupLog);
6070
6107
  }
@@ -6393,7 +6430,7 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
6393
6430
  return parts;
6394
6431
  }
6395
6432
  function buildIdleRelaunchInstructions(context, isPm, agentMode, isAuto) {
6396
- if (isPm && agentMode === "auto" && context.status === "Planning") {
6433
+ if (isPm && agentMode === "auto" && PRE_BUILD_TASK_STATUSES.has(context.status ?? "")) {
6397
6434
  if (context.plan?.trim()) {
6398
6435
  return [
6399
6436
  `You were relaunched in auto mode. A plan already exists for this task.`,
@@ -6455,7 +6492,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
6455
6492
  return parts;
6456
6493
  }
6457
6494
  async function buildInitialPrompt(mode, context, isAuto, agentMode) {
6458
- const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
6495
+ const isPackRunner = mode === "pack" || mode === "pm" && !!isAuto && !!context.isParentTask;
6459
6496
  if (!isPackRunner) {
6460
6497
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
6461
6498
  if (sessionRelaunch) return sessionRelaunch;
@@ -7562,6 +7599,12 @@ function buildDiscoveryTools(connection) {
7562
7599
 
7563
7600
  // src/tools/code-review-tools.ts
7564
7601
  import { z as z10 } from "zod";
7602
+ async function endReviewSession(connection, reason) {
7603
+ await connection.call("endReviewSession", {
7604
+ sessionId: connection.sessionId,
7605
+ reason
7606
+ });
7607
+ }
7565
7608
  function buildCodeReviewTools(connection) {
7566
7609
  return [
7567
7610
  defineTool(
@@ -7584,6 +7627,7 @@ ${summary}`;
7584
7627
  result: "approved",
7585
7628
  summary
7586
7629
  });
7630
+ await endReviewSession(connection, "approved");
7587
7631
  return textResult("Code review approved. Exiting.");
7588
7632
  }
7589
7633
  ),
@@ -7622,6 +7666,7 @@ ${issueLines}`;
7622
7666
  summary,
7623
7667
  issues
7624
7668
  });
7669
+ await endReviewSession(connection, "changes_requested");
7625
7670
  return textResult("Code review complete \u2014 changes requested. Exiting.");
7626
7671
  }
7627
7672
  )
@@ -7636,6 +7681,9 @@ function getTaskModeTools(agentMode, connection) {
7636
7681
  return [];
7637
7682
  }
7638
7683
  function getModeTools(agentMode, connection, config, context) {
7684
+ if (config.mode === "pack") {
7685
+ return buildPmTools(connection, { includePackTools: true });
7686
+ }
7639
7687
  if (config.mode === "task") return getTaskModeTools(agentMode, connection);
7640
7688
  switch (agentMode) {
7641
7689
  case "building":
@@ -7667,6 +7715,49 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
7667
7715
  });
7668
7716
  }
7669
7717
 
7718
+ // src/harness/pty/adapters/types.ts
7719
+ import { accessSync, constants, statSync } from "fs";
7720
+ import { join as join6 } from "path";
7721
+ var TuiUnavailableError = class extends Error {
7722
+ constructor(tui, message) {
7723
+ super(message);
7724
+ this.tui = tui;
7725
+ this.name = "TuiUnavailableError";
7726
+ }
7727
+ tui;
7728
+ };
7729
+ function isExecutable(path4) {
7730
+ try {
7731
+ if (!statSync(path4).isFile()) return false;
7732
+ accessSync(path4, constants.X_OK);
7733
+ return true;
7734
+ } catch {
7735
+ return false;
7736
+ }
7737
+ }
7738
+ function findOnPath(binary, env = process.env) {
7739
+ if (binary.includes("/")) {
7740
+ return isExecutable(binary) ? binary : null;
7741
+ }
7742
+ for (const dir of (env.PATH ?? "").split(":")) {
7743
+ if (!dir) continue;
7744
+ const candidate = join6(dir, binary);
7745
+ if (isExecutable(candidate)) return candidate;
7746
+ }
7747
+ return null;
7748
+ }
7749
+
7750
+ // src/execution/playwright-mcp.ts
7751
+ var PLAYWRIGHT_MCP_BINARIES = ["playwright-mcp", "mcp-server-playwright"];
7752
+ var PLAYWRIGHT_MCP_ARGS = ["--browser", "chromium", "--headless", "--no-sandbox", "--isolated"];
7753
+ function resolvePlaywrightMcpServer(env = process.env) {
7754
+ for (const binary of PLAYWRIGHT_MCP_BINARIES) {
7755
+ const command = findOnPath(binary, env);
7756
+ if (command) return { type: "stdio", command, args: [...PLAYWRIGHT_MCP_ARGS] };
7757
+ }
7758
+ return null;
7759
+ }
7760
+
7670
7761
  // src/execution/event-handlers.ts
7671
7762
  var logger = createServiceLogger("event-handlers");
7672
7763
  function safeVoid(promise, context) {
@@ -8560,7 +8651,13 @@ function buildQueryOptions(host, context) {
8560
8651
  planDialogAutoAccept: mode === "auto" && !host.hasExitedPlanMode,
8561
8652
  tools: { type: "preset", preset: "claude_code" },
8562
8653
  mcpServers: {
8563
- conveyor: createConveyorMcpServer(host.harness, host.connection, host.config, context, mode)
8654
+ conveyor: createConveyorMcpServer(host.harness, host.connection, host.config, context, mode),
8655
+ // Baked-in browser automation (claudespace pods) — omitted when the
8656
+ // binary isn't on PATH (dev machines, legacy images).
8657
+ ...(() => {
8658
+ const playwright = resolvePlaywrightMcpServer();
8659
+ return playwright ? { playwright } : {};
8660
+ })()
8564
8661
  },
8565
8662
  sandbox: context.useSandbox ? { enabled: true } : { enabled: false },
8566
8663
  hooks: buildHooks(host),
@@ -9337,7 +9434,7 @@ var QueryBridge = class {
9337
9434
 
9338
9435
  // src/runner/session-runner-helpers.ts
9339
9436
  import { readFileSync as readFileSync2 } from "fs";
9340
- import { dirname as dirname2, join as join6 } from "path";
9437
+ import { dirname as dirname2, join as join7 } from "path";
9341
9438
  import { fileURLToPath as fileURLToPath2 } from "url";
9342
9439
  function mapChatHistory(messages) {
9343
9440
  if (!messages) return [];
@@ -9366,7 +9463,7 @@ function readAgentVersion() {
9366
9463
  const here = dirname2(fileURLToPath2(import.meta.url));
9367
9464
  for (const rel of ["../package.json", "../../package.json"]) {
9368
9465
  try {
9369
- const pkg = JSON.parse(readFileSync2(join6(here, rel), "utf-8"));
9466
+ const pkg = JSON.parse(readFileSync2(join7(here, rel), "utf-8"));
9370
9467
  if (pkg.version) return pkg.version;
9371
9468
  } catch {
9372
9469
  }
@@ -9454,8 +9551,22 @@ async function sampleKeyUsage(token) {
9454
9551
  import { readFile as readFile4 } from "fs/promises";
9455
9552
  import { execFile as execFile3 } from "child_process";
9456
9553
  var PROC_TCP_LISTEN_STATE = "0A";
9554
+ function isLoopbackHexAddress(hex) {
9555
+ const addr = hex.toUpperCase();
9556
+ if (addr.length === 8) {
9557
+ return addr.slice(6, 8) === "7F";
9558
+ }
9559
+ if (addr.length === 32) {
9560
+ if (addr === "00000000000000000000000001000000") return true;
9561
+ if (addr.slice(0, 16) === "0000000000000000" && addr.slice(16, 24) === "FFFF0000") {
9562
+ return addr.slice(30, 32) === "7F";
9563
+ }
9564
+ return false;
9565
+ }
9566
+ return false;
9567
+ }
9457
9568
  function parseProcNetTcpListeners(content) {
9458
- const ports = [];
9569
+ const sockets = [];
9459
9570
  const lines = content.split("\n");
9460
9571
  for (let i = 1; i < lines.length; i++) {
9461
9572
  const line = lines[i];
@@ -9464,26 +9575,40 @@ function parseProcNetTcpListeners(content) {
9464
9575
  if (cols.length < 4 || cols[3] !== PROC_TCP_LISTEN_STATE) continue;
9465
9576
  const local = cols[1];
9466
9577
  if (!local) continue;
9467
- const portHex = local.split(":").pop();
9468
- if (!portHex) continue;
9578
+ const [addrHex, portHex] = local.split(":");
9579
+ if (!addrHex || !portHex) continue;
9469
9580
  const port = Number.parseInt(portHex, 16);
9470
- if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.push(port);
9581
+ if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
9582
+ sockets.push({ port, loopback: isLoopbackHexAddress(addrHex) });
9583
+ }
9584
+ return sockets;
9585
+ }
9586
+ function collectScan(sockets) {
9587
+ const ports = /* @__PURE__ */ new Set();
9588
+ const hasExternal = /* @__PURE__ */ new Set();
9589
+ for (const { port, loopback } of sockets) {
9590
+ ports.add(port);
9591
+ if (!loopback) hasExternal.add(port);
9592
+ }
9593
+ const loopbackOnly = /* @__PURE__ */ new Set();
9594
+ for (const port of ports) {
9595
+ if (!hasExternal.has(port)) loopbackOnly.add(port);
9471
9596
  }
9472
- return ports;
9597
+ return { ports, loopbackOnly };
9473
9598
  }
9474
9599
  var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
9475
9600
  async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
9476
- const ports = /* @__PURE__ */ new Set();
9601
+ const sockets = [];
9477
9602
  let readable = false;
9478
9603
  for (const path4 of procPaths) {
9479
9604
  try {
9480
9605
  const content = await readFile4(path4, "utf8");
9481
9606
  readable = true;
9482
- for (const port of parseProcNetTcpListeners(content)) ports.add(port);
9607
+ sockets.push(...parseProcNetTcpListeners(content));
9483
9608
  } catch {
9484
9609
  }
9485
9610
  }
9486
- return readable ? ports : null;
9611
+ return readable ? collectScan(sockets) : null;
9487
9612
  }
9488
9613
  async function readNetstatListeningPorts() {
9489
9614
  const output = await new Promise((resolve) => {
@@ -9492,17 +9617,21 @@ async function readNetstatListeningPorts() {
9492
9617
  });
9493
9618
  });
9494
9619
  if (output === null) return null;
9495
- const ports = /* @__PURE__ */ new Set();
9620
+ const sockets = [];
9496
9621
  for (const line of output.split("\n")) {
9497
9622
  if (!line.includes("LISTEN")) continue;
9498
9623
  const cols = line.trim().split(/\s+/);
9499
9624
  const local = cols[3];
9500
9625
  if (!local) continue;
9501
- const portStr = local.split(".").pop();
9502
- const port = Number(portStr);
9503
- if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.add(port);
9626
+ const lastDot = local.lastIndexOf(".");
9627
+ if (lastDot < 0) continue;
9628
+ const host = local.slice(0, lastDot);
9629
+ const port = Number(local.slice(lastDot + 1));
9630
+ if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
9631
+ const loopback = host.startsWith("127.") || host === "::1" || host === "localhost";
9632
+ sockets.push({ port, loopback });
9504
9633
  }
9505
- return ports;
9634
+ return collectScan(sockets);
9506
9635
  }
9507
9636
  async function readListeningPorts() {
9508
9637
  const proc = await readProcListeningPorts();
@@ -9526,6 +9655,8 @@ var PortDiscovery = class {
9526
9655
  log;
9527
9656
  baseline = null;
9528
9657
  tracked = /* @__PURE__ */ new Map();
9658
+ /** Loopback-only candidates already warned about (once per port). */
9659
+ warnedLoopback = /* @__PURE__ */ new Set();
9529
9660
  timer = null;
9530
9661
  ticking = false;
9531
9662
  disabled = false;
@@ -9553,7 +9684,7 @@ var PortDiscovery = class {
9553
9684
  this.log("Port discovery disabled: no listening-socket source available");
9554
9685
  return;
9555
9686
  }
9556
- this.baseline = baseline;
9687
+ this.baseline = baseline.ports;
9557
9688
  this.timer = setInterval(() => void this.tick(), this.intervalMs);
9558
9689
  this.timer.unref?.();
9559
9690
  }
@@ -9590,8 +9721,21 @@ var PortDiscovery = class {
9590
9721
  return true;
9591
9722
  }
9592
9723
  updateTracking(current) {
9593
- for (const port of current) {
9724
+ const reachable = /* @__PURE__ */ new Set();
9725
+ for (const port of current.ports) {
9594
9726
  if (!this.isCandidate(port)) continue;
9727
+ if (current.loopbackOnly.has(port)) {
9728
+ if (!this.warnedLoopback.has(port)) {
9729
+ this.warnedLoopback.add(port);
9730
+ this.log(
9731
+ `Port ${port} is listening on loopback only and cannot be previewed \u2014 bind 0.0.0.0 (or the pod IP) to make it reachable through the preview proxy`
9732
+ );
9733
+ }
9734
+ continue;
9735
+ }
9736
+ reachable.add(port);
9737
+ }
9738
+ for (const port of reachable) {
9595
9739
  const entry = this.tracked.get(port);
9596
9740
  if (!entry) {
9597
9741
  this.tracked.set(port, { seen: 1, missed: 0, confirmed: false, detectedAt: "" });
@@ -9605,7 +9749,7 @@ var PortDiscovery = class {
9605
9749
  }
9606
9750
  }
9607
9751
  for (const [port, entry] of this.tracked) {
9608
- if (current.has(port)) continue;
9752
+ if (reachable.has(port)) continue;
9609
9753
  entry.missed += 1;
9610
9754
  entry.seen = 0;
9611
9755
  if (entry.missed >= CONFIRM_SCANS || !entry.confirmed) this.tracked.delete(port);
@@ -9907,7 +10051,7 @@ var SessionRunner = class _SessionRunner {
9907
10051
  }
9908
10052
  this.mode.applyServerMode(this.fullContext?.agentMode, this.fullContext?.isAuto);
9909
10053
  this.mode.resolveInitialMode(this.taskContext);
9910
- if (this.fullContext?.isAuto && this.taskContext.status === "Open" && this.mode.isBuildCapable) {
10054
+ if (this.fullContext?.isAuto && PRE_BUILD_TASK_STATUSES.has(this.taskContext.status) && this.mode.isBuildCapable) {
9911
10055
  void this.connection.triggerIdentification().catch(() => {
9912
10056
  });
9913
10057
  }
@@ -10378,11 +10522,12 @@ var SessionRunner = class _SessionRunner {
10378
10522
  * help) it's waiting on a human, not silently stuck.
10379
10523
  *
10380
10524
  * - "pr": In Progress with no open PR (build finished without shipping).
10381
- * - "planning": still Planning without (identified + saved plan). A fresh auto
10382
- * agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
10383
- * → clean-exit → its session Ends → the workspace is reaped "agent_gone"
10384
- * before a plan ever landed. Nudging (and, on exhaustion, going dormant)
10385
- * keeps the session Active so the pod is never prematurely slept.
10525
+ * - "planning": still in a pre-build status (Planning/Open) without
10526
+ * (identified + saved plan). A fresh auto agent that finishes its plan turn
10527
+ * WITHOUT ExitPlanMode would otherwise idle → clean-exit → its session Ends
10528
+ * → the workspace is reaped "agent_gone" before a plan ever landed. Nudging
10529
+ * (and, on exhaustion, going dormant) keeps the session Active so the pod
10530
+ * is never prematurely slept.
10386
10531
  */
10387
10532
  autoStuckKind() {
10388
10533
  if (!this.mode.isAuto || this.stopped) return null;
@@ -10392,7 +10537,7 @@ var SessionRunner = class _SessionRunner {
10392
10537
  if (!this.taskContext) return null;
10393
10538
  const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
10394
10539
  if (status === "InProgress" && !githubPRUrl) return "pr";
10395
- if (status === "Planning" && !(storyPointId !== null && !!plan?.trim())) {
10540
+ if (PRE_BUILD_TASK_STATUSES.has(status) && !(storyPointId !== null && !!plan?.trim())) {
10396
10541
  return "planning";
10397
10542
  }
10398
10543
  return null;
@@ -10496,7 +10641,7 @@ var SessionRunner = class _SessionRunner {
10496
10641
  githubPRUrl: ctx.githubPRUrl,
10497
10642
  claudeSessionId: ctx.claudeSessionId ?? null,
10498
10643
  lastSeenMessageId: ctx.lastSeenMessageId ?? null,
10499
- isParentTask: !!ctx.parentTaskId,
10644
+ isParentTask: ctx.isParentTask ?? false,
10500
10645
  storyPoints: ctx.storyPoints ?? void 0,
10501
10646
  projectAgents: ctx.projectAgents ?? void 0,
10502
10647
  projectTags: ctx.projectTags ?? void 0,
@@ -10564,7 +10709,7 @@ var SessionRunner = class _SessionRunner {
10564
10709
  this.connection.onSoftStop(() => this.softStop());
10565
10710
  this.connection.onReconnected = () => this.queryBridge?.forceRepaint();
10566
10711
  this.connection.onModeChange((data) => {
10567
- const action = this.mode.handleModeChange(data.agentMode);
10712
+ const action = this.mode.handleModeChange(data.agentMode, this.taskContext);
10568
10713
  if (action.type === "start_auto") {
10569
10714
  this.connection.emitModeChanged(this.mode.effectiveMode);
10570
10715
  this.softStop();
@@ -10721,12 +10866,12 @@ var SessionRunner = class _SessionRunner {
10721
10866
 
10722
10867
  // src/setup/config.ts
10723
10868
  import { readFile as readFile5 } from "fs/promises";
10724
- import { join as join7 } from "path";
10869
+ import { join as join8 } from "path";
10725
10870
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
10726
10871
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
10727
10872
  async function loadForwardPorts(workspaceDir) {
10728
10873
  try {
10729
- const raw = await readFile5(join7(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10874
+ const raw = await readFile5(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10730
10875
  const parsed = JSON.parse(raw);
10731
10876
  const ports = (parsed.forwardPorts ?? []).filter(
10732
10877
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -10860,6 +11005,9 @@ export {
10860
11005
  updateRemoteToken,
10861
11006
  flushPendingChanges,
10862
11007
  pushToOrigin,
11008
+ TuiUnavailableError,
11009
+ findOnPath,
11010
+ resolvePlaywrightMcpServer,
10863
11011
  resolveSessionStart,
10864
11012
  sampleKeyUsage,
10865
11013
  awaitGitReady,
@@ -10878,4 +11026,4 @@ export {
10878
11026
  runStartCommand,
10879
11027
  unshallowRepo
10880
11028
  };
10881
- //# sourceMappingURL=chunk-CFELRV35.js.map
11029
+ //# sourceMappingURL=chunk-HAH2S5AD.js.map