@rallycry/conveyor-agent 11.0.18 → 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.
@@ -1416,6 +1416,7 @@ var ListProjectSessionGroupsRequestSchema = z5.object({
1416
1416
  projectId: z5.string()
1417
1417
  });
1418
1418
  var ListMyLiveSessionsAcrossProjectsRequestSchema = z5.object({});
1419
+ var ListSessionGroupsAcrossProjectsRequestSchema = z5.object({});
1419
1420
  var GetProjectAvailableTuisRequestSchema = z5.object({
1420
1421
  projectId: z5.string()
1421
1422
  });
@@ -2042,6 +2043,11 @@ var ANTHROPIC_CATALOG = [
2042
2043
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
2043
2044
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
2044
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
+ }
2045
2051
  var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
2046
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):
2047
2053
  - Use active voice. Say who does what ("The API rejects the request", not "the request is rejected").
@@ -3251,6 +3257,13 @@ var JsonlTailer = class {
3251
3257
  };
3252
3258
 
3253
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
+ }
3254
3267
  var CodexEventSource = class {
3255
3268
  constructor(emit, onSession, chat) {
3256
3269
  this.emit = emit;
@@ -3263,6 +3276,8 @@ var CodexEventSource = class {
3263
3276
  sessionId = null;
3264
3277
  completed = /* @__PURE__ */ new Set();
3265
3278
  transcript = null;
3279
+ /** Last reported reading per rate-limit window, keyed by `five_hour` / `seven_day`. */
3280
+ windows = /* @__PURE__ */ new Map();
3266
3281
  handleRecord(raw) {
3267
3282
  if (!raw || typeof raw !== "object") return;
3268
3283
  const e = raw;
@@ -3317,11 +3332,16 @@ var CodexEventSource = class {
3317
3332
  if (record.type !== "event_msg" || !record.payload || typeof record.payload !== "object")
3318
3333
  return;
3319
3334
  const event = record.payload;
3335
+ if (event.type === "token_count") {
3336
+ this.handleTokenCount(event);
3337
+ return;
3338
+ }
3320
3339
  if (event.type !== "task_complete" || !event.error || typeof event.error !== "object") return;
3321
3340
  const turn = event.turn_id;
3322
3341
  if (typeof turn !== "string" || this.completed.has(turn)) return;
3323
3342
  this.rememberTurn(turn);
3324
3343
  const error = event.error;
3344
+ if (isUsageCap(error)) this.emitUsageCap();
3325
3345
  this.emit({
3326
3346
  type: "result",
3327
3347
  subtype: "error",
@@ -3329,6 +3349,59 @@ var CodexEventSource = class {
3329
3349
  });
3330
3350
  this.chat({ kind: "turn_end" });
3331
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
+ }
3332
3405
  rememberTurn(turn) {
3333
3406
  this.completed.add(turn);
3334
3407
  const oldest = this.completed.values().next().value;
@@ -6471,13 +6544,16 @@ async function seedOpenCodeOauth(env) {
6471
6544
  }
6472
6545
 
6473
6546
  // src/harness/openai-model.ts
6474
- var DEFAULT_OPENAI_CODING_MODEL = "gpt-5.6-terra";
6547
+ var DEFAULT_OPENAI_CODING_MODEL = DEFAULT_CODEX_CODING_MODEL;
6475
6548
  function resolveOpenAiCodingModel(model) {
6476
6549
  if (!model || model.startsWith("claude-") || model.includes("/") && !model.startsWith("openai/")) {
6477
6550
  return DEFAULT_OPENAI_CODING_MODEL;
6478
6551
  }
6479
6552
  return model.replace(/^openai\//, "");
6480
6553
  }
6554
+ function resolveCodexReasoningEffort(...candidates) {
6555
+ return candidates.find(isCodexReasoningEffort);
6556
+ }
6481
6557
 
6482
6558
  // src/harness/opencode/credentials.ts
6483
6559
  var PROVIDER_KEY_ENV = {
@@ -6987,11 +7063,16 @@ var CodexTuiAdapter = class {
6987
7063
  sandbox_mode: "danger-full-access",
6988
7064
  developer_instructions: [
6989
7065
  input.options.appendSystemPrompt,
6990
- "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."
6991
7067
  ].filter(Boolean).join("\n\n"),
6992
7068
  mcp_servers: codexMcpConfig(input.mcpEntries ?? {}),
6993
7069
  projects: { [trustedCwd]: { trust_level: "trusted" } }
6994
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;
6995
7076
  if (input.pluginPath && input.eventsSinkPath) {
6996
7077
  const profile = basename(dirname3(input.pluginPath));
6997
7078
  const profilePath = join14(env.CODEX_HOME, `${profile}.config.toml`);
@@ -7009,7 +7090,9 @@ var CodexTuiAdapter = class {
7009
7090
  if (input.pluginPath) args.push("--profile", basename(dirname3(input.pluginPath)));
7010
7091
  for (const [key, value] of Object.entries(config))
7011
7092
  args.push("-c", `${key}=${tomlValue(value)}`);
7012
- 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
+ );
7013
7096
  args.push("--model", model);
7014
7097
  if (input.resume) args.push("resume", input.resume);
7015
7098
  if (input.initialPrompt) args.push("--", input.initialPrompt);
@@ -13243,17 +13326,21 @@ function handleRateLimitEvent(event, host) {
13243
13326
  const { rate_limit_info } = event;
13244
13327
  logger4.info("Rate limit event received", { rate_limit_info });
13245
13328
  const status = rate_limit_info.status;
13329
+ const resetsAt = epochSecondsToISO(rate_limit_info.resetsAt);
13246
13330
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
13247
13331
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
13248
13332
  host.connection.sendEvent({
13249
13333
  type: "rate_limit_update",
13250
13334
  rateLimitType: rate_limit_info.rateLimitType,
13251
13335
  utilization,
13252
- 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 } : {}
13253
13341
  });
13254
13342
  }
13255
13343
  if (status === "rejected") {
13256
- const resetsAt = epochSecondsToISO(rate_limit_info.resetsAt);
13257
13344
  const resetsAtDisplay = resetsAt ?? "unknown";
13258
13345
  const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAtDisplay})`;
13259
13346
  host.connection.sendEvent({ type: "error", message });
@@ -14163,6 +14250,7 @@ function buildQueryOptions(host, context) {
14163
14250
  effort: settings.effort,
14164
14251
  thinking: settings.thinking,
14165
14252
  betas: settings.betas,
14253
+ codex: settings.codex,
14166
14254
  abortController: host.abortController ?? void 0,
14167
14255
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
14168
14256
  enableFileCheckpointing: settings.enableFileCheckpointing,
@@ -14318,6 +14406,7 @@ async function* watchForParkedTui(inner, host, opts) {
14318
14406
  const timer = setTimeout(() => {
14319
14407
  parked = true;
14320
14408
  host.connection.emitStatus("waiting_for_input");
14409
+ host.callbacks.onSubmittedPtyParked?.();
14321
14410
  void host.callbacks.onStatusChange("waiting_for_input");
14322
14411
  }, PARKED_TUI_GRACE_MS);
14323
14412
  const silenceTimeoutMs = resolveTurnSilenceTimeoutMs();
@@ -15373,6 +15462,9 @@ async function runUsageProbe(deps = {}) {
15373
15462
  // src/execution/usage-sampler.ts
15374
15463
  var logger7 = createServiceLogger("usage-sampler");
15375
15464
  var NO_SAMPLES = { samples: [], unmeasurable: null };
15465
+ function usesNativeUsageReporting(env = process.env) {
15466
+ return env.CONVEYOR_TUI === "codex";
15467
+ }
15376
15468
  function isAttributable(identity, sessionToken) {
15377
15469
  if (!identity) return { ok: true };
15378
15470
  if (identity.hasRefreshToken && !identity.isConveyorOwned) {
@@ -16606,7 +16698,7 @@ var SessionRunner = class _SessionRunner {
16606
16698
  * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
16607
16699
  * pod has no OAuth token (e.g. API-key projects). */
16608
16700
  async sampleAndReportKeyUsage() {
16609
- if (this.stopped) return;
16701
+ if (this.stopped || usesNativeUsageReporting()) return;
16610
16702
  const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
16611
16703
  const { samples, unmeasurable } = await sampleKeyUsage(
16612
16704
  process.env.CLAUDE_CODE_OAUTH_TOKEN,
@@ -16754,16 +16846,8 @@ var SessionRunner = class _SessionRunner {
16754
16846
  isAuto: this.config.isAuto
16755
16847
  };
16756
16848
  const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
16757
- onStatusChange: (status) => {
16758
- if (status === "running") {
16759
- this.beginRunningEpisode();
16760
- if (this._state === "waiting_for_input") {
16761
- this._state = "running";
16762
- this.lifecycle.cancelIdleTimer();
16763
- }
16764
- }
16765
- return this.callbacks.onStatusChange(status);
16766
- },
16849
+ onStatusChange: (status) => this.handleQueryStatus(status),
16850
+ onSubmittedPtyParked: () => this.applySubmittedPtyParkedStatus(),
16767
16851
  onEvent: (event) => {
16768
16852
  if (!this.agentLiveReported) {
16769
16853
  this.agentLiveReported = true;
@@ -17016,6 +17100,32 @@ ${outcome.failures.join("\n")}
17016
17100
  await this.connection.emitStatus(status);
17017
17101
  await this.callbacks.onStatusChange(status);
17018
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
+ }
17019
17129
  /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
17020
17130
  * prefilled PTY callback already emitted `running` before it reaches here. */
17021
17131
  beginRunningEpisode() {
@@ -17121,6 +17231,7 @@ export {
17121
17231
  resolveSessionStart,
17122
17232
  parseUsageGauges,
17123
17233
  runUsageProbe,
17234
+ usesNativeUsageReporting,
17124
17235
  sampleKeyUsage,
17125
17236
  buildRateLimitEvents,
17126
17237
  buildUnmeasurableEvent,
package/dist/cli.js CHANGED
@@ -26,8 +26,9 @@ import {
26
26
  resolveTuiAdapter,
27
27
  resolveTuiKindFromEnv,
28
28
  runUsageProbe,
29
- sampleKeyUsage
30
- } from "./chunk-ZFHN7GO7.js";
29
+ sampleKeyUsage,
30
+ usesNativeUsageReporting
31
+ } from "./chunk-N5LEVAGA.js";
31
32
  import "./chunk-SR66HQKB.js";
32
33
  import {
33
34
  inheritedEnv,
@@ -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,
package/dist/index.d.ts CHANGED
@@ -1107,6 +1107,20 @@ declare class SessionRunner {
1107
1107
  * the tool starts work that outlives the turn. */
1108
1108
  private noteToolUseForBackgroundWork;
1109
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;
1110
1124
  /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
1111
1125
  * prefilled PTY callback already emitted `running` before it reaches here. */
1112
1126
  private beginRunningEpisode;
@@ -1165,6 +1179,10 @@ type UserQuestionSignal = {
1165
1179
  interface AgentRunnerCallbacks {
1166
1180
  onEvent: (event: Record<string, unknown>) => void | Promise<void>;
1167
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;
1168
1186
  /** Optional — fired on question-pending transitions (see UserQuestionSignal). */
1169
1187
  onUserQuestion?: (signal: UserQuestionSignal) => void;
1170
1188
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-ZFHN7GO7.js";
4
+ } from "./chunk-N5LEVAGA.js";
5
5
  import "./chunk-SR66HQKB.js";
6
6
  import "./chunk-W5INK3NE.js";
7
7
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.18",
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