@rallycry/conveyor-agent 11.0.17 → 11.0.19

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-7TSQXIN3.js";
3
+ } from "./chunk-EIHZRSYI.js";
4
4
  import {
5
5
  TOMBSTONE_MESSAGE,
6
6
  isLegacyEntrypointLaunch
@@ -12,7 +12,7 @@ import {
12
12
  refreshSkillsAfterCheckout,
13
13
  runPreReadyBinds
14
14
  } from "./chunk-SR66HQKB.js";
15
- import "./chunk-QLMNQSJL.js";
15
+ import "./chunk-W5INK3NE.js";
16
16
  import {
17
17
  GitPrepJob,
18
18
  defaultGit,
@@ -20,6 +20,8 @@ import {
20
20
  reportBootMilestone
21
21
  } from "./chunk-GL2DIQEQ.js";
22
22
  import "./chunk-W4LZ7R6Z.js";
23
+ import "./chunk-372R6E4C.js";
24
+ import "./chunk-SQM2BQ7H.js";
23
25
  import {
24
26
  workbenchPort
25
27
  } from "./chunk-KMB3BU4S.js";
@@ -0,0 +1,147 @@
1
+ import {
2
+ getWorkbenchClient
3
+ } from "./chunk-SQM2BQ7H.js";
4
+ import {
5
+ sharedDir,
6
+ workbenchEnabled
7
+ } from "./chunk-KMB3BU4S.js";
8
+
9
+ // src/runner/heavy-gate.ts
10
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
11
+ import path from "path";
12
+ var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
13
+ var GATES_DIR_NAME = "gates";
14
+ function runDir() {
15
+ return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
16
+ }
17
+ function gatesDir() {
18
+ return path.join(sharedDir() ?? runDir(), GATES_DIR_NAME);
19
+ }
20
+ function pidAlive(pid) {
21
+ if (process.platform === "linux" && pidIsZombie(pid)) return false;
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function pidIsZombie(pid) {
30
+ try {
31
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
32
+ const closingParen = stat.lastIndexOf(")");
33
+ return closingParen >= 0 && stat.slice(closingParen + 1).trimStart().startsWith("Z");
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ function parsePid(raw) {
39
+ const pid = Number.parseInt(raw.trim(), 10);
40
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
41
+ }
42
+ function singletonPidFiles() {
43
+ return GATE_KEYS.map((key) => path.join(runDir(), `${key}.pid`));
44
+ }
45
+ var splitGenericGateStatus = null;
46
+ var splitGenericGateStatusRefresh = null;
47
+ function getLocalGenericGateStatus() {
48
+ const activeLabels = [];
49
+ const abandonedReceipts = [];
50
+ let entries;
51
+ try {
52
+ entries = readdirSync(gatesDir());
53
+ } catch {
54
+ return { activeLabels, abandonedReceipts };
55
+ }
56
+ for (const entry of entries) {
57
+ if (!entry.endsWith(".pid")) continue;
58
+ const pidPath = path.join(gatesDir(), entry);
59
+ const label = entry.slice(0, -".pid".length);
60
+ try {
61
+ const pid = parsePid(readFileSync(pidPath, "utf8"));
62
+ if (pid === null) continue;
63
+ if (pidAlive(pid)) {
64
+ activeLabels.push(label);
65
+ } else if (!existsSync(path.join(gatesDir(), `${label}.exit`))) {
66
+ abandonedReceipts.push({ path: pidPath, label, pid, mtimeMs: statSync(pidPath).mtimeMs });
67
+ }
68
+ } catch {
69
+ }
70
+ }
71
+ abandonedReceipts.sort((a, b) => a.mtimeMs - b.mtimeMs);
72
+ return { activeLabels, abandonedReceipts };
73
+ }
74
+ function genericGateStatus() {
75
+ if (!workbenchEnabled()) return getLocalGenericGateStatus();
76
+ return splitGenericGateStatus ?? { activeLabels: ["unknown"], abandonedReceipts: [] };
77
+ }
78
+ function refreshGenericGateStatus() {
79
+ if (!workbenchEnabled()) return Promise.resolve(getLocalGenericGateStatus());
80
+ if (splitGenericGateStatusRefresh) return splitGenericGateStatusRefresh;
81
+ splitGenericGateStatusRefresh = getWorkbenchClient().gateStatus().then((status) => {
82
+ splitGenericGateStatus = {
83
+ activeLabels: status.activeLabels,
84
+ abandonedReceipts: status.abandonedReceipts.map((receipt) => ({
85
+ ...receipt,
86
+ path: path.join(gatesDir(), `${receipt.label}.pid`)
87
+ }))
88
+ };
89
+ return splitGenericGateStatus;
90
+ }).catch(() => {
91
+ splitGenericGateStatus = null;
92
+ return genericGateStatus();
93
+ }).finally(() => {
94
+ splitGenericGateStatusRefresh = null;
95
+ });
96
+ return splitGenericGateStatusRefresh;
97
+ }
98
+ function listAbandonedGateReceipts() {
99
+ return genericGateStatus().abandonedReceipts;
100
+ }
101
+ function listGateExitSentinels() {
102
+ const sentinels = [];
103
+ let entries;
104
+ try {
105
+ entries = readdirSync(gatesDir());
106
+ } catch {
107
+ return sentinels;
108
+ }
109
+ for (const entry of entries) {
110
+ if (!entry.endsWith(".exit")) continue;
111
+ const file = path.join(gatesDir(), entry);
112
+ try {
113
+ const mtimeMs = statSync(file).mtimeMs;
114
+ let code = null;
115
+ try {
116
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
117
+ const value = parsed?.code;
118
+ if (typeof value === "number") code = value;
119
+ } catch {
120
+ }
121
+ sentinels.push({ path: file, label: entry.slice(0, -".exit".length), code, mtimeMs });
122
+ } catch {
123
+ }
124
+ }
125
+ sentinels.sort((a, b) => a.mtimeMs - b.mtimeMs);
126
+ return sentinels;
127
+ }
128
+ function isHeavyGateActive() {
129
+ if (genericGateStatus().activeLabels.length > 0) return true;
130
+ for (const file of singletonPidFiles()) {
131
+ try {
132
+ const pid = parsePid(readFileSync(file, "utf8"));
133
+ if (pid !== null && pidAlive(pid)) return true;
134
+ } catch {
135
+ }
136
+ }
137
+ return false;
138
+ }
139
+
140
+ export {
141
+ gatesDir,
142
+ getLocalGenericGateStatus,
143
+ refreshGenericGateStatus,
144
+ listAbandonedGateReceipts,
145
+ listGateExitSentinels,
146
+ isHeavyGateActive
147
+ };
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  loadPtySpawn
3
- } from "./chunk-QLMNQSJL.js";
3
+ } from "./chunk-W5INK3NE.js";
4
4
  import {
5
5
  terminateProcessGroup
6
6
  } from "./chunk-W4LZ7R6Z.js";
7
+ import {
8
+ getLocalGenericGateStatus
9
+ } from "./chunk-372R6E4C.js";
7
10
  import {
8
11
  FrameReader,
9
12
  timingSafeTokenEqual,
@@ -112,21 +115,25 @@ async function runRefreshSkills(socket, opts) {
112
115
  }
113
116
  socket.end();
114
117
  }
118
+ function immediateResponse(req, opts) {
119
+ if (req.op === "ping") return { t: "pong", version: opts.version };
120
+ if (req.op === "gitStatus") return opts.getGitStatus?.() ?? { t: "gitStatus", state: "ready" };
121
+ if (req.op === "gateStatus") return { t: "gateStatus", ...getLocalGenericGateStatus() };
122
+ return null;
123
+ }
115
124
  function dispatch(socket, req, sink, opts, liveOps) {
116
125
  if (!req || typeof req !== "object" || !timingSafeTokenEqual(req.token ?? "", opts.token)) {
117
126
  writeFrame(socket, { t: "error", message: "unauthorized", code: "unauthorized" });
118
127
  socket.destroy();
119
128
  return;
120
129
  }
130
+ const immediate = immediateResponse(req, opts);
131
+ if (immediate) {
132
+ writeFrame(socket, immediate);
133
+ socket.end();
134
+ return;
135
+ }
121
136
  switch (req.op) {
122
- case "ping":
123
- writeFrame(socket, { t: "pong", version: opts.version });
124
- socket.end();
125
- return;
126
- case "gitStatus":
127
- writeFrame(socket, opts.getGitStatus?.() ?? { t: "gitStatus", state: "ready" });
128
- socket.end();
129
- return;
130
137
  case "refreshSkills":
131
138
  void runRefreshSkills(socket, opts);
132
139
  return;
@@ -5,7 +5,7 @@ import {
5
5
  readWorkspaceBytes,
6
6
  statWorkspacePath,
7
7
  workspacePathExists
8
- } from "./chunk-M4SD2ESQ.js";
8
+ } from "./chunk-WS7QRB37.js";
9
9
  import {
10
10
  reportBootMilestone
11
11
  } from "./chunk-GL2DIQEQ.js";
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-W4LZ7R6Z.js";
17
17
  import {
18
18
  getWorkbenchClient
19
- } from "./chunk-B54XFOXL.js";
19
+ } from "./chunk-SQM2BQ7H.js";
20
20
  import {
21
21
  workbenchEnabled
22
22
  } from "./chunk-KMB3BU4S.js";
@@ -29,7 +29,7 @@ import {
29
29
  spawnOptionsFingerprint,
30
30
  transcriptSize,
31
31
  turnOptionsFrom
32
- } from "./chunk-QLMNQSJL.js";
32
+ } from "./chunk-W5INK3NE.js";
33
33
  import {
34
34
  AgentConnection,
35
35
  CodespacePortVisibility,
@@ -59,7 +59,7 @@ import {
59
59
  statWorkspacePath,
60
60
  updateRemoteToken,
61
61
  verifyGitCredential
62
- } from "./chunk-M4SD2ESQ.js";
62
+ } from "./chunk-WS7QRB37.js";
63
63
  import {
64
64
  registerBootMilestoneSocketFallback,
65
65
  reportBootMilestone
@@ -72,15 +72,17 @@ import {
72
72
  } from "./chunk-W4LZ7R6Z.js";
73
73
  import {
74
74
  isHeavyGateActive,
75
- listGateExitSentinels
76
- } from "./chunk-7P6QZHXZ.js";
75
+ listAbandonedGateReceipts,
76
+ listGateExitSentinels,
77
+ refreshGenericGateStatus
78
+ } from "./chunk-372R6E4C.js";
77
79
  import {
78
80
  LoopLagMonitor,
79
81
  loopStatusForRunnerStatus
80
82
  } from "./chunk-IA45XHOA.js";
81
83
  import {
82
84
  getWorkbenchClient
83
- } from "./chunk-B54XFOXL.js";
85
+ } from "./chunk-SQM2BQ7H.js";
84
86
  import {
85
87
  workbenchEnabled
86
88
  } from "./chunk-KMB3BU4S.js";
@@ -1414,6 +1416,7 @@ var ListProjectSessionGroupsRequestSchema = z5.object({
1414
1416
  projectId: z5.string()
1415
1417
  });
1416
1418
  var ListMyLiveSessionsAcrossProjectsRequestSchema = z5.object({});
1419
+ var ListSessionGroupsAcrossProjectsRequestSchema = z5.object({});
1417
1420
  var GetProjectAvailableTuisRequestSchema = z5.object({
1418
1421
  projectId: z5.string()
1419
1422
  });
@@ -2040,6 +2043,11 @@ var ANTHROPIC_CATALOG = [
2040
2043
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
2041
2044
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
2042
2045
  ];
2046
+ var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
2047
+ var DEFAULT_CODEX_CODING_MODEL = "gpt-5.6-terra";
2048
+ function isCodexReasoningEffort(value) {
2049
+ return typeof value === "string" && CODEX_REASONING_EFFORTS.includes(value);
2050
+ }
2043
2051
  var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
2044
2052
  When you write prose a person will read \u2014 chat messages, plan updates, PR titles and bodies, PR review guides (the \`publish_review_guide\` overview and section explanations), review comments \u2014 follow these rules (based on ASD-STE100 Simplified Technical English):
2045
2053
  - Use active voice. Say who does what ("The API rejects the request", not "the request is rejected").
@@ -2181,6 +2189,10 @@ var CATALOG = {
2181
2189
  // rejects (media_type_header_exception), breaking search/audit indexing
2182
2190
  // in pods. 512m heap matches the project compose sizing.
2183
2191
  image: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
2192
+ mirror: {
2193
+ src: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
2194
+ dest: "mirror-elasticsearch:9.4.0"
2195
+ },
2184
2196
  ports: [9200],
2185
2197
  // Baked service images are `docker commit`s of a recently-running ES, so
2186
2198
  // they carry a stale data-dir node.lock; ES 9 hard-fails on it at boot
@@ -3245,6 +3257,13 @@ var JsonlTailer = class {
3245
3257
  };
3246
3258
 
3247
3259
  // src/harness/codex/event-source.ts
3260
+ var USAGE_CAP_PATTERN = /usage_limit_(?:reached|exceeded)|rate_limit_reached|UsageLimitReached|hit your usage limit/i;
3261
+ var WEEKLY_WINDOW_MINUTES = 24 * 60;
3262
+ function isUsageCap(error) {
3263
+ return [error.codex_error_info, error.code, error.type, error.message].some(
3264
+ (value) => typeof value === "string" && USAGE_CAP_PATTERN.test(value)
3265
+ );
3266
+ }
3248
3267
  var CodexEventSource = class {
3249
3268
  constructor(emit, onSession, chat) {
3250
3269
  this.emit = emit;
@@ -3257,6 +3276,8 @@ var CodexEventSource = class {
3257
3276
  sessionId = null;
3258
3277
  completed = /* @__PURE__ */ new Set();
3259
3278
  transcript = null;
3279
+ /** Last reported reading per rate-limit window, keyed by `five_hour` / `seven_day`. */
3280
+ windows = /* @__PURE__ */ new Map();
3260
3281
  handleRecord(raw) {
3261
3282
  if (!raw || typeof raw !== "object") return;
3262
3283
  const e = raw;
@@ -3311,11 +3332,16 @@ var CodexEventSource = class {
3311
3332
  if (record.type !== "event_msg" || !record.payload || typeof record.payload !== "object")
3312
3333
  return;
3313
3334
  const event = record.payload;
3335
+ if (event.type === "token_count") {
3336
+ this.handleTokenCount(event);
3337
+ return;
3338
+ }
3314
3339
  if (event.type !== "task_complete" || !event.error || typeof event.error !== "object") return;
3315
3340
  const turn = event.turn_id;
3316
3341
  if (typeof turn !== "string" || this.completed.has(turn)) return;
3317
3342
  this.rememberTurn(turn);
3318
3343
  const error = event.error;
3344
+ if (isUsageCap(error)) this.emitUsageCap();
3319
3345
  this.emit({
3320
3346
  type: "result",
3321
3347
  subtype: "error",
@@ -3323,6 +3349,59 @@ var CodexEventSource = class {
3323
3349
  });
3324
3350
  this.chat({ kind: "turn_end" });
3325
3351
  }
3352
+ /**
3353
+ * `token_count` carries the account's rate-limit gauges on every turn:
3354
+ * `rate_limits.primary` (the 5-hour window) and `.secondary` (weekly), each
3355
+ * with `used_percent`, `window_minutes` and `resets_at` in epoch seconds.
3356
+ * Classified by window length rather than by name, and reported only when a
3357
+ * window's numbers changed — the event fires per turn, and every report
3358
+ * rewrites the key row and fans out `project:codingAgentKeyUpdated`.
3359
+ */
3360
+ handleTokenCount(event) {
3361
+ const limits = event.rate_limits;
3362
+ if (!limits || typeof limits !== "object") return;
3363
+ for (const name of ["primary", "secondary"]) {
3364
+ const window = limits[name];
3365
+ if (!window || typeof window !== "object") continue;
3366
+ const reading = window;
3367
+ if (typeof reading.used_percent !== "number") continue;
3368
+ const minutes = typeof reading.window_minutes === "number" ? reading.window_minutes : name === "secondary" ? WEEKLY_WINDOW_MINUTES : 0;
3369
+ const rateLimitType = minutes >= WEEKLY_WINDOW_MINUTES ? "seven_day" : "five_hour";
3370
+ const resetsAt = typeof reading.resets_at === "number" ? reading.resets_at : void 0;
3371
+ const last = this.windows.get(rateLimitType);
3372
+ if (last && last.usedPercent === reading.used_percent && last.resetsAt === resetsAt) continue;
3373
+ this.windows.set(rateLimitType, { usedPercent: reading.used_percent, resetsAt });
3374
+ this.emit({
3375
+ type: "rate_limit_event",
3376
+ rate_limit_info: {
3377
+ status: "allowed",
3378
+ rateLimitType,
3379
+ utilization: Math.max(0, Math.min(1, reading.used_percent / 100)),
3380
+ resetsAt
3381
+ }
3382
+ });
3383
+ }
3384
+ }
3385
+ /**
3386
+ * The CLI does not say which window capped. The fullest window last reported
3387
+ * is the one that did; with no reading at all, the 5-hour window is the
3388
+ * conservative guess (its fallback pause is the shorter one).
3389
+ */
3390
+ emitUsageCap() {
3391
+ let rateLimitType = "five_hour";
3392
+ let resetsAt;
3393
+ let fullest = -Infinity;
3394
+ for (const [type, window] of this.windows) {
3395
+ if (window.usedPercent <= fullest) continue;
3396
+ fullest = window.usedPercent;
3397
+ rateLimitType = type;
3398
+ resetsAt = window.resetsAt;
3399
+ }
3400
+ this.emit({
3401
+ type: "rate_limit_event",
3402
+ rate_limit_info: { status: "rejected", rateLimitType, resetsAt }
3403
+ });
3404
+ }
3326
3405
  rememberTurn(turn) {
3327
3406
  this.completed.add(turn);
3328
3407
  const oldest = this.completed.values().next().value;
@@ -6465,13 +6544,16 @@ async function seedOpenCodeOauth(env) {
6465
6544
  }
6466
6545
 
6467
6546
  // src/harness/openai-model.ts
6468
- var DEFAULT_OPENAI_CODING_MODEL = "gpt-5.6-terra";
6547
+ var DEFAULT_OPENAI_CODING_MODEL = DEFAULT_CODEX_CODING_MODEL;
6469
6548
  function resolveOpenAiCodingModel(model) {
6470
6549
  if (!model || model.startsWith("claude-") || model.includes("/") && !model.startsWith("openai/")) {
6471
6550
  return DEFAULT_OPENAI_CODING_MODEL;
6472
6551
  }
6473
6552
  return model.replace(/^openai\//, "");
6474
6553
  }
6554
+ function resolveCodexReasoningEffort(...candidates) {
6555
+ return candidates.find(isCodexReasoningEffort);
6556
+ }
6475
6557
 
6476
6558
  // src/harness/opencode/credentials.ts
6477
6559
  var PROVIDER_KEY_ENV = {
@@ -6981,11 +7063,16 @@ var CodexTuiAdapter = class {
6981
7063
  sandbox_mode: "danger-full-access",
6982
7064
  developer_instructions: [
6983
7065
  input.options.appendSystemPrompt,
6984
- "Conveyor workflow skills are stored in .claude/skills in the workspace. When instructed to run a /skill-name skill, read .claude/skills/skill-name/SKILL.md and follow its instructions. This is a file-based workflow, not a native slash command."
7066
+ "Conveyor workflow skills are stored in .claude/skills in the workspace. When instructed to run a /skill-name skill, read .claude/skills/skill-name/SKILL.md and follow its instructions. This is a file-based workflow, not a native slash command. When a skill's Goal and finish line section tells you to create a thread goal, do so with your goal tool before the first step; the objective is that section's finish line."
6985
7067
  ].filter(Boolean).join("\n\n"),
6986
7068
  mcp_servers: codexMcpConfig(input.mcpEntries ?? {}),
6987
7069
  projects: { [trustedCwd]: { trust_level: "trusted" } }
6988
7070
  };
7071
+ const effort = resolveCodexReasoningEffort(
7072
+ this.env.CONVEYOR_AGENT_REASONING_EFFORT,
7073
+ input.options.codex?.effort
7074
+ );
7075
+ if (effort) config.model_reasoning_effort = effort;
6989
7076
  if (input.pluginPath && input.eventsSinkPath) {
6990
7077
  const profile = basename(dirname3(input.pluginPath));
6991
7078
  const profilePath = join14(env.CODEX_HOME, `${profile}.config.toml`);
@@ -7003,7 +7090,9 @@ var CodexTuiAdapter = class {
7003
7090
  if (input.pluginPath) args.push("--profile", basename(dirname3(input.pluginPath)));
7004
7091
  for (const [key, value] of Object.entries(config))
7005
7092
  args.push("-c", `${key}=${tomlValue(value)}`);
7006
- const model = resolveOpenAiCodingModel(this.env.CONVEYOR_AGENT_MODEL ?? input.options.model);
7093
+ const model = resolveOpenAiCodingModel(
7094
+ this.env.CONVEYOR_AGENT_MODEL ?? input.options.codex?.model ?? input.options.model
7095
+ );
7007
7096
  args.push("--model", model);
7008
7097
  if (input.resume) args.push("resume", input.resume);
7009
7098
  if (input.initialPrompt) args.push("--", input.initialPrompt);
@@ -13237,17 +13326,21 @@ function handleRateLimitEvent(event, host) {
13237
13326
  const { rate_limit_info } = event;
13238
13327
  logger4.info("Rate limit event received", { rate_limit_info });
13239
13328
  const status = rate_limit_info.status;
13329
+ const resetsAt = epochSecondsToISO(rate_limit_info.resetsAt);
13240
13330
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
13241
13331
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
13242
13332
  host.connection.sendEvent({
13243
13333
  type: "rate_limit_update",
13244
13334
  rateLimitType: rate_limit_info.rateLimitType,
13245
13335
  utilization,
13246
- status
13336
+ status,
13337
+ // The server parses this into the key's sessionResetsAt / weeklyResetsAt;
13338
+ // Codex reports a reset instant with every gauge, so allowed events carry
13339
+ // it too, not only rejections.
13340
+ ...resetsAt ? { resetsAt } : {}
13247
13341
  });
13248
13342
  }
13249
13343
  if (status === "rejected") {
13250
- const resetsAt = epochSecondsToISO(rate_limit_info.resetsAt);
13251
13344
  const resetsAtDisplay = resetsAt ?? "unknown";
13252
13345
  const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAtDisplay})`;
13253
13346
  host.connection.sendEvent({ type: "error", message });
@@ -14157,6 +14250,7 @@ function buildQueryOptions(host, context) {
14157
14250
  effort: settings.effort,
14158
14251
  thinking: settings.thinking,
14159
14252
  betas: settings.betas,
14253
+ codex: settings.codex,
14160
14254
  abortController: host.abortController ?? void 0,
14161
14255
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
14162
14256
  enableFileCheckpointing: settings.enableFileCheckpointing,
@@ -14312,6 +14406,7 @@ async function* watchForParkedTui(inner, host, opts) {
14312
14406
  const timer = setTimeout(() => {
14313
14407
  parked = true;
14314
14408
  host.connection.emitStatus("waiting_for_input");
14409
+ host.callbacks.onSubmittedPtyParked?.();
14315
14410
  void host.callbacks.onStatusChange("waiting_for_input");
14316
14411
  }, PARKED_TUI_GRACE_MS);
14317
14412
  const silenceTimeoutMs = resolveTurnSilenceTimeoutMs();
@@ -15367,6 +15462,9 @@ async function runUsageProbe(deps = {}) {
15367
15462
  // src/execution/usage-sampler.ts
15368
15463
  var logger7 = createServiceLogger("usage-sampler");
15369
15464
  var NO_SAMPLES = { samples: [], unmeasurable: null };
15465
+ function usesNativeUsageReporting(env = process.env) {
15466
+ return env.CONVEYOR_TUI === "codex";
15467
+ }
15370
15468
  function isAttributable(identity, sessionToken) {
15371
15469
  if (!identity) return { ok: true };
15372
15470
  if (identity.hasRefreshToken && !identity.isConveyorOwned) {
@@ -15688,7 +15786,7 @@ var SessionRunner = class _SessionRunner {
15688
15786
  onHeartbeat: () => {
15689
15787
  const loopStatus = this.refreshLoopStatus();
15690
15788
  this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
15691
- this.maybeWakeForOrphanedBackgroundWork();
15789
+ void this.refreshGateStatusAndMaybeWake();
15692
15790
  },
15693
15791
  onIdleTimeout: () => {
15694
15792
  if (this.deferShutdownForLiveChild("idle")) return;
@@ -15829,6 +15927,10 @@ var SessionRunner = class _SessionRunner {
15829
15927
  /** One-shot latch: at most one watchdog wake per idle episode. Reset when a
15830
15928
  * turn runs. */
15831
15929
  backgroundWakeFiredThisIdle = false;
15930
+ /** Start of the last active episode. A gate can be launched shortly before
15931
+ * the agent declares completion, so idle-start freshness would reject its
15932
+ * terminal receipt as stale. */
15933
+ activeEpisodeStartedAt = Date.now();
15832
15934
  /** When the current idle episode began — an exit sentinel older than this
15833
15935
  * appeared while a turn could still have consumed it, so it never wakes. */
15834
15936
  idleEpisodeStartedAt = 0;
@@ -15843,22 +15945,30 @@ var SessionRunner = class _SessionRunner {
15843
15945
  * evidence of orphaned background work — a tracked launch with no live gate
15844
15946
  * process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
15845
15947
  * `gates/<label>.exit` sentinel that appeared this idle episode — never on
15846
- * "the agent seems stuck". A `completed` agent is never woken (the
15847
- * completion guard holds), and a live pid always suppresses the wake.
15948
+ * "the agent seems stuck". A `completed` agent remains parked unless a
15949
+ * current-episode generic receipt proves a required gate died without an
15950
+ * exit record, and a live pid always suppresses the wake.
15848
15951
  */
15849
15952
  maybeWakeForOrphanedBackgroundWork() {
15850
15953
  try {
15851
- if (this._state !== "idle" || this.stopped || this.completedThisTurn) {
15954
+ if (this._state !== "idle" || this.stopped) {
15852
15955
  this.orphanGraceStartedAt = null;
15853
15956
  return;
15854
15957
  }
15855
15958
  if (this.backgroundWakeFiredThisIdle) return;
15856
15959
  if (!this.inputResolver) return;
15960
+ const abandoned = listAbandonedGateReceipts().find(
15961
+ (receipt) => receipt.mtimeMs >= this.activeEpisodeStartedAt
15962
+ );
15963
+ if (this.completedThisTurn && !abandoned) {
15964
+ this.orphanGraceStartedAt = null;
15965
+ return;
15966
+ }
15857
15967
  if (isHeavyGateActive()) {
15858
15968
  this.orphanGraceStartedAt = null;
15859
15969
  return;
15860
15970
  }
15861
- const evidence = this.findOrphanEvidence(Date.now());
15971
+ const evidence = abandoned ? this.abandonedReceiptEvidence(abandoned) : this.findOrphanEvidence(Date.now());
15862
15972
  if (!evidence) return;
15863
15973
  process.stderr.write(
15864
15974
  `[conveyor-agent] Background-work watchdog: waking agent \u2014 ${evidence.description}
@@ -15867,11 +15977,16 @@ var SessionRunner = class _SessionRunner {
15867
15977
  this.backgroundWakeFiredThisIdle = true;
15868
15978
  this.orphanGraceStartedAt = null;
15869
15979
  if (evidence.sentinel) rmSync(evidence.sentinel.path, { force: true });
15980
+ if (evidence.abandonedReceipt) rmSync(evidence.abandonedReceipt.path, { force: true });
15870
15981
  this.backgroundWork.clear();
15982
+ if (evidence.abandonedReceipt) {
15983
+ this.completedThisTurn = false;
15984
+ this.dormantDeadline = null;
15985
+ }
15871
15986
  const resolver = this.inputResolver;
15872
15987
  this.inputResolver = null;
15873
15988
  resolver({
15874
- content: `A background job you started appears to have finished or died without delivering its completion notification (${evidence.description}). Read the job's output \u2014 its log file, or the corresponding file under $CONVEYOR_RUN_DIR/gates/ \u2014 to determine the real result, then continue your plan. Do not assume the job succeeded; verify its output first, and rerun it if it was killed.`,
15989
+ content: `A required gate you started appears to have finished or died without delivering its completion notification (${evidence.description}). Read the job's output \u2014 its log file, or the corresponding file under $CONVEYOR_SHARED_DIR/gates on split pods \u2014 to determine the real result, then continue your plan. Do not assume the job succeeded; verify its output first. If it died, rerun it in the foreground without shell '&' and retain the command session until its exit result.`,
15875
15990
  userId: "system",
15876
15991
  source: "background_work_check"
15877
15992
  });
@@ -15902,6 +16017,25 @@ var SessionRunner = class _SessionRunner {
15902
16017
  description: `${this.backgroundWork.pendingCount()} tracked background launch(es) with no live gate process for ${Math.round(ORPHAN_WAKE_GRACE_MS / 6e4)}+ minutes`
15903
16018
  };
15904
16019
  }
16020
+ abandonedReceiptEvidence(receipt) {
16021
+ return {
16022
+ abandonedReceipt: receipt,
16023
+ description: `gate '${receipt.label}' stopped before writing an exit receipt (pid ${receipt.pid})`
16024
+ };
16025
+ }
16026
+ /** Refresh PID liveness in the owning workbench namespace before checking
16027
+ * terminal receipts. The local path is immediate; socket failures preserve
16028
+ * the conservative cached state and never interrupt the heartbeat. */
16029
+ async refreshGateStatusAndMaybeWake() {
16030
+ try {
16031
+ await refreshGenericGateStatus();
16032
+ this.refreshLoopStatus();
16033
+ this.maybeWakeForOrphanedBackgroundWork();
16034
+ } catch (err) {
16035
+ process.stderr.write(`[conveyor-agent] Gate status refresh failed: ${err}
16036
+ `);
16037
+ }
16038
+ }
15905
16039
  get sessionId() {
15906
16040
  return this.connection.sessionId;
15907
16041
  }
@@ -16564,7 +16698,7 @@ var SessionRunner = class _SessionRunner {
16564
16698
  * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
16565
16699
  * pod has no OAuth token (e.g. API-key projects). */
16566
16700
  async sampleAndReportKeyUsage() {
16567
- if (this.stopped) return;
16701
+ if (this.stopped || usesNativeUsageReporting()) return;
16568
16702
  const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
16569
16703
  const { samples, unmeasurable } = await sampleKeyUsage(
16570
16704
  process.env.CLAUDE_CODE_OAUTH_TOKEN,
@@ -16712,13 +16846,8 @@ var SessionRunner = class _SessionRunner {
16712
16846
  isAuto: this.config.isAuto
16713
16847
  };
16714
16848
  const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
16715
- onStatusChange: (status) => {
16716
- if (status === "running" && this._state === "waiting_for_input") {
16717
- this._state = "running";
16718
- this.lifecycle.cancelIdleTimer();
16719
- }
16720
- return this.callbacks.onStatusChange(status);
16721
- },
16849
+ onStatusChange: (status) => this.handleQueryStatus(status),
16850
+ onSubmittedPtyParked: () => this.applySubmittedPtyParkedStatus(),
16722
16851
  onEvent: (event) => {
16723
16852
  if (!this.agentLiveReported) {
16724
16853
  this.agentLiveReported = true;
@@ -16963,8 +17092,7 @@ ${outcome.failures.join("\n")}
16963
17092
  }
16964
17093
  async setState(status) {
16965
17094
  if (status === "running") {
16966
- this.backgroundDeferStartedAt = null;
16967
- this.backgroundWakeFiredThisIdle = false;
17095
+ this.beginRunningEpisode();
16968
17096
  }
16969
17097
  if (status === "idle" && this._state !== "idle") this.idleEpisodeStartedAt = Date.now();
16970
17098
  this._state = status;
@@ -16972,6 +17100,39 @@ ${outcome.failures.join("\n")}
16972
17100
  await this.connection.emitStatus(status);
16973
17101
  await this.callbacks.onStatusChange(status);
16974
17102
  }
17103
+ /**
17104
+ * A submitted PTY query can park on an unexpected terminal dialog without
17105
+ * returning its turn. Keep the runner's local state (which drives heartbeat
17106
+ * liveness) in sync with that specific watchdog signal, but do not emit it
17107
+ * again: QueryBridge already sent it.
17108
+ *
17109
+ * Retry delays and pending questionnaires also report `waiting_for_input`,
17110
+ * but they retain their existing bounded liveness holds and must not change
17111
+ * `_state` here.
17112
+ */
17113
+ applySubmittedPtyParkedStatus() {
17114
+ this._state = "waiting_for_input";
17115
+ this.refreshLoopStatus();
17116
+ }
17117
+ /** Forward an ordinary query status without changing liveness for transient
17118
+ * waiting states. The submitted-PTY watchdog calls its dedicated handler. */
17119
+ handleQueryStatus(status) {
17120
+ if (status === "running") {
17121
+ this.beginRunningEpisode();
17122
+ if (this._state === "waiting_for_input") {
17123
+ this._state = "running";
17124
+ this.lifecycle.cancelIdleTimer();
17125
+ }
17126
+ }
17127
+ return this.callbacks.onStatusChange(status);
17128
+ }
17129
+ /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
17130
+ * prefilled PTY callback already emitted `running` before it reaches here. */
17131
+ beginRunningEpisode() {
17132
+ this.activeEpisodeStartedAt = Date.now();
17133
+ this.backgroundDeferStartedAt = null;
17134
+ this.backgroundWakeFiredThisIdle = false;
17135
+ }
16975
17136
  async shutdown(finalState) {
16976
17137
  process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
16977
17138
  `);
@@ -17070,6 +17231,7 @@ export {
17070
17231
  resolveSessionStart,
17071
17232
  parseUsageGauges,
17072
17233
  runUsageProbe,
17234
+ usesNativeUsageReporting,
17073
17235
  sampleKeyUsage,
17074
17236
  buildRateLimitEvents,
17075
17237
  buildUnmeasurableEvent,
@@ -37,7 +37,31 @@ var RemoteProcessHandle = class extends EventEmitter {
37
37
  }
38
38
  };
39
39
 
40
+ // src/workbench/gate-status.ts
41
+ function readGateStatus(open, timeoutMs) {
42
+ return new Promise((resolve, reject) => {
43
+ let status = null;
44
+ let timer = null;
45
+ const socket = open(
46
+ (frame) => {
47
+ if (frame.t === "gateStatus") status = frame;
48
+ },
49
+ (error) => {
50
+ if (timer) clearTimeout(timer);
51
+ if (status) resolve(status);
52
+ else reject(error ?? new WorkbenchError("connection closed before gateStatus"));
53
+ }
54
+ );
55
+ timer = setTimeout(
56
+ () => socket.destroy(new WorkbenchError("gateStatus timed out", "workbench_timeout")),
57
+ timeoutMs
58
+ );
59
+ timer.unref();
60
+ });
61
+ }
62
+
40
63
  // src/workbench/client.ts
64
+ var DEFAULT_GATE_STATUS_TIMEOUT_MS = 5e3;
41
65
  var WORKBENCH_COMMAND_ENV_DENYLIST = /* @__PURE__ */ new Set([
42
66
  "POD_BOOTSTRAP_TOKEN",
43
67
  "CONVEYOR_BOOTSTRAP_TOKEN",
@@ -107,10 +131,12 @@ var WorkbenchClient = class {
107
131
  port;
108
132
  host;
109
133
  token;
134
+ gateStatusTimeoutMs;
110
135
  constructor(options = {}) {
111
136
  this.port = options.port ?? workbenchPort() ?? DEFAULT_WORKBENCH_PORT;
112
137
  this.host = options.host ?? "127.0.0.1";
113
138
  this.token = options.token ?? workbenchToken();
139
+ this.gateStatusTimeoutMs = options.gateStatusTimeoutMs ?? DEFAULT_GATE_STATUS_TIMEOUT_MS;
114
140
  }
115
141
  /** Open a connection, send the request, route response frames. */
116
142
  open(request, onFrame, onClose) {
@@ -168,6 +194,12 @@ var WorkbenchClient = class {
168
194
  );
169
195
  });
170
196
  }
197
+ gateStatus() {
198
+ return readGateStatus(
199
+ (onFrame, onClose) => this.open({ op: "gateStatus" }, onFrame, onClose),
200
+ this.gateStatusTimeoutMs
201
+ );
202
+ }
171
203
  /** Ask the workbench — which owns the repo filesystem in split mode — to
172
204
  * re-run the skill floor after the agent's authoritative task-branch
173
205
  * checkout. A daemon without a provider answers an empty result. */
@@ -200,7 +200,7 @@ async function loadPtySpawn() {
200
200
  async function resolvePtySpawn() {
201
201
  const { workbenchEnabled } = await import("./mode-ZJSOSLGU.js");
202
202
  if (!workbenchEnabled()) return loadPtySpawn();
203
- const { getWorkbenchClient } = await import("./client-DVVHMIOO.js");
203
+ const { getWorkbenchClient } = await import("./client-VIFYMBT2.js");
204
204
  return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);
205
205
  }
206
206
  function sessionTempBase() {
@@ -20,7 +20,7 @@ import {
20
20
  import {
21
21
  WorkbenchError,
22
22
  getWorkbenchClient
23
- } from "./chunk-B54XFOXL.js";
23
+ } from "./chunk-SQM2BQ7H.js";
24
24
  import {
25
25
  workbenchEnabled
26
26
  } from "./chunk-KMB3BU4S.js";
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-X2MKHJBZ.js";
10
+ } from "./chunk-GZRZGBIK.js";
11
11
  import {
12
12
  DEFAULT_SONNET_MODEL,
13
13
  PtyHarness,
@@ -26,14 +26,15 @@ import {
26
26
  resolveTuiAdapter,
27
27
  resolveTuiKindFromEnv,
28
28
  runUsageProbe,
29
- sampleKeyUsage
30
- } from "./chunk-GVDQQZCO.js";
29
+ sampleKeyUsage,
30
+ usesNativeUsageReporting
31
+ } from "./chunk-N5LEVAGA.js";
31
32
  import "./chunk-SR66HQKB.js";
32
33
  import {
33
34
  inheritedEnv,
34
35
  resolvePtySpawn,
35
36
  sessionTempBase
36
- } from "./chunk-QLMNQSJL.js";
37
+ } from "./chunk-W5INK3NE.js";
37
38
  import {
38
39
  AgentConnection,
39
40
  CodespacePortVisibility,
@@ -44,12 +45,12 @@ import {
44
45
  createServiceLogger,
45
46
  fetchBootstrap,
46
47
  loadConveyorConfig
47
- } from "./chunk-M4SD2ESQ.js";
48
+ } from "./chunk-WS7QRB37.js";
48
49
  import "./chunk-GL2DIQEQ.js";
49
50
  import "./chunk-W4LZ7R6Z.js";
50
- import "./chunk-7P6QZHXZ.js";
51
+ import "./chunk-372R6E4C.js";
51
52
  import "./chunk-IA45XHOA.js";
52
- import "./chunk-B54XFOXL.js";
53
+ import "./chunk-SQM2BQ7H.js";
53
54
  import "./chunk-KMB3BU4S.js";
54
55
  import "./chunk-6Q6LQBWO.js";
55
56
 
@@ -236,7 +237,7 @@ var ProjectSessionRunner = class {
236
237
  /** Report the running subscription key's rate-limit utilization (same path as
237
238
  * the task runner). Best-effort — never throws, no-op without an OAuth token. */
238
239
  async sampleAndReportKeyUsage() {
239
- if (this.stopped) return;
240
+ if (this.stopped || usesNativeUsageReporting()) return;
240
241
  const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
241
242
  const { samples, unmeasurable } = await sampleKeyUsage(
242
243
  process.env.CLAUDE_CODE_OAUTH_TOKEN,
@@ -575,7 +576,7 @@ var AdhocSessionRunner = class {
575
576
  }
576
577
  /** Single-key fallback: sample only the key this pod booted under. */
577
578
  async sampleAndReportOwnKey() {
578
- if (this.stopped) return;
579
+ if (this.stopped || usesNativeUsageReporting()) return;
579
580
  const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
580
581
  const { samples, unmeasurable } = await sampleKeyUsage(
581
582
  process.env.CLAUDE_CODE_OAUTH_TOKEN,
@@ -1205,7 +1206,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1205
1206
 
1206
1207
  // src/cli.ts
1207
1208
  if (process.argv[2] === "boot") {
1208
- const { runBoot } = await import("./boot-CCRZMY2P.js");
1209
+ const { runBoot } = await import("./boot-4ZNOCKFZ.js");
1209
1210
  process.exit(await runBoot(process.argv.slice(3)));
1210
1211
  }
1211
1212
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1307,7 +1308,7 @@ process.on("unhandledRejection", (reason) => {
1307
1308
  process.exit(1);
1308
1309
  });
1309
1310
  if (process.env.CONVEYOR_MODE === "workbench") {
1310
- const { startWorkbenchServer } = await import("./server-WM23BC5C.js");
1311
+ const { startWorkbenchServer } = await import("./server-L5CQ5EJB.js");
1311
1312
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1312
1313
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1313
1314
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
@@ -1458,7 +1459,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1458
1459
  process.exit(1);
1459
1460
  }
1460
1461
  if (CONVEYOR_MODE === "serving") {
1461
- const { runServingSession } = await import("./serve-boot-VJR6J5PL.js");
1462
+ const { runServingSession } = await import("./serve-boot-RU5BYIK4.js");
1462
1463
  exitContext.runnerMode = "serving";
1463
1464
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1464
1465
  const outcome = await runServingSession({
@@ -4,7 +4,7 @@ import {
4
4
  WorkbenchError,
5
5
  getWorkbenchClient,
6
6
  resetWorkbenchClient
7
- } from "./chunk-B54XFOXL.js";
7
+ } from "./chunk-SQM2BQ7H.js";
8
8
  import "./chunk-KMB3BU4S.js";
9
9
  import "./chunk-6Q6LQBWO.js";
10
10
  export {
package/dist/gate-cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  gatesDir
4
- } from "./chunk-7P6QZHXZ.js";
4
+ } from "./chunk-372R6E4C.js";
5
+ import "./chunk-SQM2BQ7H.js";
6
+ import "./chunk-KMB3BU4S.js";
7
+ import "./chunk-6Q6LQBWO.js";
5
8
 
6
9
  // src/runner/gate-wrapper.ts
7
10
  import { mkdirSync, rmSync, writeFileSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -810,6 +810,10 @@ declare class SessionRunner {
810
810
  /** One-shot latch: at most one watchdog wake per idle episode. Reset when a
811
811
  * turn runs. */
812
812
  private backgroundWakeFiredThisIdle;
813
+ /** Start of the last active episode. A gate can be launched shortly before
814
+ * the agent declares completion, so idle-start freshness would reject its
815
+ * terminal receipt as stale. */
816
+ private activeEpisodeStartedAt;
813
817
  /** When the current idle episode began — an exit sentinel older than this
814
818
  * appeared while a turn could still have consumed it, so it never wakes. */
815
819
  private idleEpisodeStartedAt;
@@ -824,13 +828,19 @@ declare class SessionRunner {
824
828
  * evidence of orphaned background work — a tracked launch with no live gate
825
829
  * process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
826
830
  * `gates/<label>.exit` sentinel that appeared this idle episode — never on
827
- * "the agent seems stuck". A `completed` agent is never woken (the
828
- * completion guard holds), and a live pid always suppresses the wake.
831
+ * "the agent seems stuck". A `completed` agent remains parked unless a
832
+ * current-episode generic receipt proves a required gate died without an
833
+ * exit record, and a live pid always suppresses the wake.
829
834
  */
830
835
  private maybeWakeForOrphanedBackgroundWork;
831
836
  /** Orphan evidence that has held past the grace, or null. Maintains the
832
837
  * grace clock for the tracked-work condition as a side effect. */
833
838
  private findOrphanEvidence;
839
+ private abandonedReceiptEvidence;
840
+ /** Refresh PID liveness in the owning workbench namespace before checking
841
+ * terminal receipts. The local path is immediate; socket failures preserve
842
+ * the conservative cached state and never interrupt the heartbeat. */
843
+ private refreshGateStatusAndMaybeWake;
834
844
  get sessionId(): string;
835
845
  get isStopped(): boolean;
836
846
  /** Wire the boot supervisor handle post-construction — cli.ts constructs it
@@ -1097,6 +1107,23 @@ declare class SessionRunner {
1097
1107
  * the tool starts work that outlives the turn. */
1098
1108
  private noteToolUseForBackgroundWork;
1099
1109
  private setState;
1110
+ /**
1111
+ * A submitted PTY query can park on an unexpected terminal dialog without
1112
+ * returning its turn. Keep the runner's local state (which drives heartbeat
1113
+ * liveness) in sync with that specific watchdog signal, but do not emit it
1114
+ * again: QueryBridge already sent it.
1115
+ *
1116
+ * Retry delays and pending questionnaires also report `waiting_for_input`,
1117
+ * but they retain their existing bounded liveness holds and must not change
1118
+ * `_state` here.
1119
+ */
1120
+ private applySubmittedPtyParkedStatus;
1121
+ /** Forward an ordinary query status without changing liveness for transient
1122
+ * waiting states. The submitted-PTY watchdog calls its dedicated handler. */
1123
+ private handleQueryStatus;
1124
+ /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
1125
+ * prefilled PTY callback already emitted `running` before it reaches here. */
1126
+ private beginRunningEpisode;
1100
1127
  private shutdown;
1101
1128
  private _finalState;
1102
1129
  /** The final status after run() completes. Use to determine exit code. */
@@ -1152,6 +1179,10 @@ type UserQuestionSignal = {
1152
1179
  interface AgentRunnerCallbacks {
1153
1180
  onEvent: (event: Record<string, unknown>) => void | Promise<void>;
1154
1181
  onStatusChange: (status: string) => void | Promise<void>;
1182
+ /** Fired only when a submitted PTY query stays silent past the parked-TUI
1183
+ * grace window. This differs from ordinary `waiting_for_input` reports:
1184
+ * retry delays and questionnaires keep the runner locally live. */
1185
+ onSubmittedPtyParked?: () => void;
1155
1186
  /** Optional — fired on question-pending transitions (see UserQuestionSignal). */
1156
1187
  onUserQuestion?: (signal: UserQuestionSignal) => void;
1157
1188
  }
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-GVDQQZCO.js";
4
+ } from "./chunk-N5LEVAGA.js";
5
5
  import "./chunk-SR66HQKB.js";
6
- import "./chunk-QLMNQSJL.js";
6
+ import "./chunk-W5INK3NE.js";
7
7
  import {
8
8
  AgentConnection,
9
9
  GIT_TIMEOUT_MS,
@@ -17,18 +17,18 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-M4SD2ESQ.js";
20
+ } from "./chunk-WS7QRB37.js";
21
21
  import "./chunk-GL2DIQEQ.js";
22
22
  import {
23
23
  runAuthTokenCommand,
24
24
  runSetupCommand,
25
25
  runStartCommand
26
26
  } from "./chunk-W4LZ7R6Z.js";
27
- import "./chunk-7P6QZHXZ.js";
27
+ import "./chunk-372R6E4C.js";
28
28
  import "./chunk-IA45XHOA.js";
29
29
  import {
30
30
  getWorkbenchClient
31
- } from "./chunk-B54XFOXL.js";
31
+ } from "./chunk-SQM2BQ7H.js";
32
32
  import {
33
33
  workbenchEnabled
34
34
  } from "./chunk-KMB3BU4S.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-X2MKHJBZ.js";
3
+ } from "./chunk-GZRZGBIK.js";
4
4
  import {
5
5
  AgentConnection,
6
6
  CodespacePortVisibility,
@@ -11,11 +11,11 @@ import {
11
11
  createServiceLogger,
12
12
  ensureOnTaskBranch,
13
13
  loadConveyorConfig
14
- } from "./chunk-M4SD2ESQ.js";
14
+ } from "./chunk-WS7QRB37.js";
15
15
  import "./chunk-GL2DIQEQ.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
18
- import "./chunk-B54XFOXL.js";
18
+ import "./chunk-SQM2BQ7H.js";
19
19
  import "./chunk-KMB3BU4S.js";
20
20
  import "./chunk-6Q6LQBWO.js";
21
21
 
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-7TSQXIN3.js";
4
- import "./chunk-QLMNQSJL.js";
3
+ } from "./chunk-EIHZRSYI.js";
4
+ import "./chunk-W5INK3NE.js";
5
5
  import "./chunk-W4LZ7R6Z.js";
6
+ import "./chunk-372R6E4C.js";
7
+ import "./chunk-SQM2BQ7H.js";
8
+ import "./chunk-KMB3BU4S.js";
6
9
  import "./chunk-6Q6LQBWO.js";
7
10
  import "./chunk-6W6UZ4SJ.js";
8
11
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.17",
3
+ "version": "11.0.19",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -38,6 +38,42 @@ out in an **Environment** note — never assume; check which one you are in.
38
38
  serially. Parallel fan-out is a deliberate choice the user makes by pressing
39
39
  Build on the parent, not something a build session opts into.
40
40
 
41
+ ## Goal and finish line
42
+
43
+ Which goal applies depends on the route below. State it once, then hold it: a
44
+ run ends when its finish line is true, not earlier.
45
+
46
+ **Task path goal:** a pull request for this card.
47
+ **Finish line:** the card in ReviewPR, the PR open, CI started. Then aftercare
48
+ per `## Aftercare` — in a pod you stay wake-able; standalone locally you stop.
49
+ **A task turn may end only when:** a harness-tracked background gate is in
50
+ flight (Claude Code only); you are blocked on a human with the question posted;
51
+ or the finish line is true.
52
+
53
+ **Pack path goal:** the WHOLE pack, not one child. Per child the goal covers
54
+ the full cycle: plan reviewed and promoted → implemented on the pack branch →
55
+ gated → reviewer-of-record pass → merged (locally) or
56
+ `mcp__conveyor__update_task` → ReviewDev (pod) → base synced. Then
57
+ cross-reference, then the finale.
58
+ **Finish line:** every child is ReviewDev, Complete, or Cancelled AND the
59
+ parent's finale PR into the base branch is open with CI started. One child done
60
+ is not the finish line. A turn that ends with a ready child and no wake armed
61
+ is the failure this section exists to prevent.
62
+ **A pack turn may end only when:** a harness-tracked background gate is in
63
+ flight (Claude Code only — Codex holds the gate's command session open
64
+ instead); you are blocked on a human with the parked protocol posted; or the
65
+ finale PR is open and green-and-quiet. Nothing else. Anything else is a stalled
66
+ run, not a paused one.
67
+
68
+ **Runtime:** *Codex CLI* — before the first step (Setup step 1 on the pack
69
+ path), create a thread goal with your goal tool whose objective is the finish
70
+ line sentence for this route, naming the card slug. Call `update_goal` with
71
+ `complete` only when that finish line is true (for a pack: the finale PR is
72
+ open); `blocked` only per the parked protocol; never because the budget is
73
+ low. *Claude Code* — there is no goal tool; the pacing rule in
74
+ [references/pack-path.md](references/pack-path.md) is the loop, and its pack
75
+ heartbeat is the fallback.
76
+
41
77
  ## Environment
42
78
 
43
79
  Resolve which one you are in before touching git or the filesystem.
@@ -96,18 +96,39 @@ End a turn with children remaining ONLY when a wake is guaranteed:
96
96
 
97
97
  - **A background gate is in flight.** Its completion notification re-invokes
98
98
  you. (Launch it with the harness's `run_in_background`; hand-rolled
99
- backgrounding wakes nothing.)
99
+ backgrounding wakes nothing.) *Codex CLI substitution:* Codex has no
100
+ background gate. Hold the gate's command session open until it returns an
101
+ exit result, then continue in the same turn — ending the turn is not how a
102
+ Codex runner waits.
100
103
  - **You are blocked on a human** — the parked protocol below. The user's chat
101
104
  reply is the wake.
102
105
  - **The finale PR is open and quiet** — the Babysit tier. Green and quiet is
103
106
  the end.
104
107
 
105
- Any other end-of-turn must arm `ScheduleWakeup` first: 60–90s, prompt
106
- restating which child comes next. That is the pod-sanctioned mechanism (the
107
- pod prompt allows `ScheduleWakeup` exactly when nothing else will notify you),
108
- and it is a rare exception, not the cadence. Never end a turn "to report
109
- progress" chat is where progress goes, and posting there does not end the
110
- pack.
108
+ Any other end-of-turn is runtime-specific:
109
+
110
+ **Claude Code.** Arm `ScheduleWakeup` first: 60–90s, prompt restating which
111
+ child comes next. That is the pod-sanctioned mechanism (the pod prompt allows
112
+ `ScheduleWakeup` exactly when nothing else will notify you), and it is a rare
113
+ exception, not the cadence. On top of it, run a **pack heartbeat**: at Setup,
114
+ before the first child, arm ONE long fallback wake (`ScheduleWakeup`,
115
+ 1200–1500s) whose prompt is the pack continuation — "re-derive with
116
+ `mcp__conveyor__list_subtasks` and take the next ready child; if nothing is
117
+ ready and no gate is in flight, stop" — and re-arm it at every child boundary
118
+ so exactly one is outstanding. A heartbeat that fires mid-work re-derives
119
+ state and is a no-op. This is NOT the "insurance wakeup" the pod prompt
120
+ forbids: that rule is about a background job's own completion notification,
121
+ which does fire. Nothing at all notifies a pack between children, so the
122
+ heartbeat is the only guaranteed wake a Claude pack has. Do not delete it.
123
+
124
+ **Codex CLI.** There is no `ScheduleWakeup` and no `/loop`. The thread goal
125
+ created per the SKILL.md *Goal and finish line* section is the continuation:
126
+ an idle thread is re-prompted to keep working toward it. The only way to leave
127
+ a child ready is therefore to keep working in the same turn — merge, next
128
+ child, merge. Mark the goal complete only when the finale PR is open.
129
+
130
+ Never end a turn "to report progress" — in either runtime, chat is where
131
+ progress goes, and posting there does not end the pack.
111
132
 
112
133
  **Pre-exit invariant.** Before ending ANY turn, re-run
113
134
  `mcp__conveyor__list_subtasks`. A child that is `Open` with its dependencies
@@ -19,6 +19,34 @@ identification decides.** Moving the card to Open fires identification
19
19
  automatically (story points, icon, agent, and tags if you set none). Never set
20
20
  icon or story points yourself, and never `start_task` unless the user asks.
21
21
 
22
+ ## Goal and finish line
23
+
24
+ **Goal:** a plan on this card that a context-free executor can build, or one
25
+ batched round of questions for a human when scope is genuinely ambiguous.
26
+ **Finish line:** the plan is saved with `mcp__conveyor__update_task` AND the
27
+ recommendation is posted with `mcp__conveyor__post_to_chat` and
28
+ `milestone: "plan_ready"`. In a pod, story points and risk are also set
29
+ (`mcp__conveyor__update_task_properties`) and plan mode is exited.
30
+ **A turn may end only when:** (a) the finish line above is true, or (b) an
31
+ `AskUserQuestion` is pending. Nothing else. Anything else is a stalled run, not
32
+ a paused one.
33
+
34
+ **Never ask whether to start building, and never offer to.** Once the plan is
35
+ saved, the handoff is not yours: in the cloud the system spawns a separate
36
+ Builder session for the card, and locally a human runs `/conveyor-build` when
37
+ they are ready. Until one of those happens, every further turn on this card is
38
+ a plan iteration — read the feedback, revise, re-save, re-post `plan_ready`.
39
+ Do not claim the card, do not touch the tree, and do not invoke
40
+ `/conveyor-build` unprompted. (Locally the user may ask for it in the same
41
+ session; that request is the only trigger.)
42
+
43
+ **Runtime:** *Codex CLI* — before Phase 0, create a thread goal with your goal
44
+ tool whose objective is: "Save a plan on card <slug> that a context-free
45
+ builder can execute, or ask the blocking questions; do not start
46
+ implementation." Mark it complete only when the finish line is true, never
47
+ because the budget is low. *Claude Code* — there is no goal tool; the finish
48
+ line and the two turn endings above are the loop.
49
+
22
50
  ## Phase 0 — Resolve context
23
51
 
24
52
  1. `mcp__conveyor__get_connection_context` (all Conveyor tools fully-qualified;
@@ -135,7 +163,10 @@ touching Conveyor, unless they asked you to just ship it.
135
163
  > does not exist in a pod. In a pod, the card exists and you are on it: save
136
164
  > the plan with `mcp__conveyor__update_task` (`plan`, and `description` if it
137
165
  > needs sharpening), post the same recommendation message to chat with
138
- > `milestone: "plan_ready"`, and stop a pod does not promote its own card to
166
+ > `milestone: "plan_ready"`, and stop. "Stop" means end the turn with no
167
+ > question about starting work — per *Goal and finish line*, the Builder is a
168
+ > separate session the system spawns, and every later turn here is a plan
169
+ > iteration. A pod does not promote its own card to
139
170
  > Open. A parked plan is never `blocked`: that milestone means you cannot
140
171
  > continue until a human acts, and it pages nobody. To page a person, ask them
141
172
  > with AskUserQuestion. If the pod also has to leave
@@ -26,6 +26,30 @@ process, this skill loses: a project's own rules outrank a general one.
26
26
  PR touches are not this PR's job — note them in chat if they matter, or file
27
27
  a suggestion, but do not block on them.
28
28
 
29
+ ## Goal and finish line
30
+
31
+ **Goal:** one verdict on this PR.
32
+ **Finish line:** the environment's verdict tool returned success —
33
+ `mcp__conveyor__approve_code_review` / `mcp__conveyor__request_code_changes`
34
+ in a pod, `mcp__conveyor__approve_task` / `mcp__conveyor__request_changes`
35
+ locally — OR a blocked note is posted with the specific blocker and no verdict
36
+ (the `## Blocked` case).
37
+ **A turn may end only when:** (a) the verdict call succeeded — read the
38
+ response; a "superseded by a newer review cycle" error is not success; (b) the
39
+ blocked note is posted; or (c) you pushed your own fix and are waiting for CI
40
+ before approving. Nothing else. Findings posted with no verdict called is a
41
+ stalled review, not a paused one.
42
+
43
+ **Reviewer of record for a pack child** has no verdict tool: its finish line is
44
+ the merge (locally) or the `mcp__conveyor__update_task` → ReviewDev write
45
+ (pod), and the pack path's goal wording in conveyor-build governs instead.
46
+
47
+ **Runtime:** *Codex CLI* — before reading the change, create a thread goal with
48
+ your goal tool: "Render exactly one review verdict on PR <n> for card <slug>."
49
+ Mark it complete only after the verdict call succeeded, never because the
50
+ budget is low. *Claude Code* — there is no goal tool; the finish line above is
51
+ the loop.
52
+
29
53
  ## Read the change
30
54
 
31
55
  1. `mcp__conveyor__get_task` for the plan — you cannot judge "does this do what
@@ -142,8 +166,10 @@ is the reviewer, and the independent review happens later on the pack's PR into
142
166
 
143
167
  - Apply the same criteria above. The absence of an automated pass makes this
144
168
  review more load-bearing, not less.
145
- - There is **no verdict tool** — merging the child PR is the approval. Say what
146
- you checked in chat so the record exists.
169
+ - There is **no verdict tool** — merging the child PR (locally) or the
170
+ `mcp__conveyor__update_task` ReviewDev write (pod) is the approval, and
171
+ that write or merge is this review's finish line; the pack path's goal
172
+ wording applies. Say what you checked in chat so the record exists.
147
173
  - You wrote this code, which makes self-review the weak point. An independent
148
174
  reviewer with the diff and no memory of writing it catches what you cannot.
149
175
 
@@ -1,76 +0,0 @@
1
- // src/runner/heavy-gate.ts
2
- import { readdirSync, readFileSync, statSync } from "fs";
3
- import path from "path";
4
- var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
5
- var GATES_DIR_NAME = "gates";
6
- function runDir() {
7
- return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
8
- }
9
- function gatesDir() {
10
- return path.join(runDir(), GATES_DIR_NAME);
11
- }
12
- function pidAlive(pid) {
13
- try {
14
- process.kill(pid, 0);
15
- return true;
16
- } catch {
17
- return false;
18
- }
19
- }
20
- function parsePid(raw) {
21
- const pid = Number.parseInt(raw.trim(), 10);
22
- return Number.isInteger(pid) && pid > 0 ? pid : null;
23
- }
24
- function gatePidFiles() {
25
- const files = GATE_KEYS.map((key) => path.join(runDir(), `${key}.pid`));
26
- try {
27
- for (const entry of readdirSync(gatesDir())) {
28
- if (entry.endsWith(".pid")) files.push(path.join(gatesDir(), entry));
29
- }
30
- } catch {
31
- }
32
- return files;
33
- }
34
- function listGateExitSentinels() {
35
- const sentinels = [];
36
- let entries;
37
- try {
38
- entries = readdirSync(gatesDir());
39
- } catch {
40
- return sentinels;
41
- }
42
- for (const entry of entries) {
43
- if (!entry.endsWith(".exit")) continue;
44
- const file = path.join(gatesDir(), entry);
45
- try {
46
- const mtimeMs = statSync(file).mtimeMs;
47
- let code = null;
48
- try {
49
- const parsed = JSON.parse(readFileSync(file, "utf8"));
50
- const value = parsed?.code;
51
- if (typeof value === "number") code = value;
52
- } catch {
53
- }
54
- sentinels.push({ path: file, label: entry.slice(0, -".exit".length), code, mtimeMs });
55
- } catch {
56
- }
57
- }
58
- sentinels.sort((a, b) => a.mtimeMs - b.mtimeMs);
59
- return sentinels;
60
- }
61
- function isHeavyGateActive() {
62
- for (const file of gatePidFiles()) {
63
- try {
64
- const pid = parsePid(readFileSync(file, "utf8"));
65
- if (pid !== null && pidAlive(pid)) return true;
66
- } catch {
67
- }
68
- }
69
- return false;
70
- }
71
-
72
- export {
73
- gatesDir,
74
- listGateExitSentinels,
75
- isHeavyGateActive
76
- };