agent-conveyor 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,6 +131,10 @@ successful JSON. Treat that warning as expected Node runtime noise when the
131
131
  command exits 0 and the JSON result reports `"ok": true`.
132
132
  Before publishing `agent-conveyor` to npm, use
133
133
  [`docs/package-release.md`](docs/package-release.md).
134
+ The preferred publish path is the manual GitHub Actions `publish.yml` workflow
135
+ with npm Trusted Publishing enabled for the `npm-production` environment. Use
136
+ `publish=false` for artifact review and `publish=true` only for an approved
137
+ release version that is not already on npm.
134
138
 
135
139
  For common manager setups, start with
136
140
  [`docs/manager-recipes.md`](docs/manager-recipes.md). It maps natural-language
@@ -144,6 +148,10 @@ For a package-facing overview of these modes, open
144
148
  [`docs/landing-page.html`](docs/landing-page.html) locally or host it as a
145
149
  static landing page. From the repo, `npm run docs:landing` serves it at
146
150
  `http://127.0.0.1:8765/`.
151
+ The GitHub Pages version lives at
152
+ [`neonwatty.github.io/agent-conveyor`](https://neonwatty.github.io/agent-conveyor/).
153
+ Use `node scripts/check-landing-page.mjs` for a docs-only desktop/mobile
154
+ screenshot gate; this does not run the full package release smoke.
147
155
 
148
156
  After install, the intended Codex app entry point is natural language. Open a
149
157
  new Codex app session in the target repo and say:
@@ -158,7 +166,15 @@ Require adversarial proof before another worker iteration.
158
166
  The installed skill should call the `conveyor` CLI, choose names, create the
159
167
  no-tmux binding with `create-disposable-binding`, point the worker at
160
168
  `worker-inbox`, and use `loop-status` plus telemetry receipts before reporting
161
- that the loop is ready.
169
+ that the loop is ready. When the manager is itself running in the Codex app and
170
+ thread tools are available, the skill should first call `create_thread` for a
171
+ fresh same-project worker, name it with `set_thread_title`, pass the returned
172
+ thread identity through `--worker-codex-app-thread-id` and
173
+ `--worker-codex-app-thread-title`, and use `send_message_to_thread` only to
174
+ deliver the generated `worker_handoff` bootstrap prompt. The raw terminal
175
+ `conveyor` CLI does not create Codex app threads by itself; if app thread tools
176
+ are unavailable, open a separate Codex app worker manually and paste the
177
+ `worker_handoff` prompt.
162
178
 
163
179
  Dispatch is core infrastructure for supervised worker/manager pairs. The
164
180
  `pair` workflow starts a detached Dispatch watch process by default so worker
@@ -309,16 +325,21 @@ tmux attach -t codex-live-test
309
325
  Use `--accept-trust` only for directories you intentionally trust; it retries
310
326
  Enter during startup discovery so fresh workspaces do not stall before
311
327
  registration.
312
- - `register-worker --name N [--pid P | --codex-session PATH] [--cwd D] [--tmux-session S]` —
328
+ - `register-worker --name N [--pid P | --codex-session PATH] [--cwd D] [--tmux-session S] [--codex-app-thread-id ID] [--codex-app-thread-title TITLE]` —
313
329
  Register an already-running Codex session as a worker. Rollout JSONL is
314
330
  auto-discovered from the pid via `lsof` unless `--codex-session` is given.
331
+ The optional Codex app thread flags are metadata supplied by the Codex app
332
+ skill/tool layer; they help humans identify the app thread but do not change
333
+ rollout ingest or Dispatch delivery.
315
334
  - `register-manager --name N ...` — Same arguments; tmux is not required.
316
335
  Both registration commands print a `communication` object. When
317
336
  `--tmux-session` is present, `communication.session_kind='tmux'`,
318
337
  `receive_style='push'`, and `delivery_mode='push'`; without tmux but with a
319
338
  Codex rollout identity, `session_kind='codex_app'`, `receive_style='pull'`,
320
339
  and `delivery_mode='pull_required'`, with the role-specific inbox polling
321
- command template.
340
+ command template. The generated command may include a local
341
+ `PATH=.../bin:$PATH conveyor` prefix; preserve that prefix when sending the
342
+ command to a Codex app thread.
322
343
  - `deregister <name>` — Mark a session gone. Refuses if the session is bound
323
344
  to an active task.
324
345
  - `sessions [--role worker|manager] [--state active|gone|all] [--include-legacy]
@@ -338,7 +359,7 @@ tmux attach -t codex-live-test
338
359
  registration, so managers can detect whether a worker or manager is
339
360
  tmux-push capable or must poll its mailbox.
340
361
  - `tasks [--create NAME --goal G --summary S]` — List or create tasks.
341
- - `create-disposable-binding TASK [--worker NAME] [--manager NAME] [--template TEMPLATE | --required-before-continue TYPE] [--adversarial]` —
362
+ - `create-disposable-binding TASK [--worker NAME] [--manager NAME] [--template TEMPLATE | --required-before-continue TYPE] [--adversarial] [--worker-codex-app-thread-id ID] [--worker-codex-app-thread-title TITLE] [--manager-codex-app-thread-id ID] [--manager-codex-app-thread-title TITLE]` —
342
363
  Create a no-tmux manager/worker binding for real Ralph-loop slices. The
343
364
  helper creates the task when missing, marks it managed, writes valid Codex
344
365
  rollout JSONL files, registers worker and manager sessions with
@@ -346,7 +367,18 @@ tmux attach -t codex-live-test
346
367
  custom Ralph-loop policy run, and prints replay commands for Dispatch,
347
368
  `loop-status`, per-session `communication` metadata, plus a `worker_handoff`
348
369
  prompt that tells Codex app workers to keep polling their worker inbox
349
- through the bounded loop.
370
+ through the bounded loop using the exact generated command. For pull-required
371
+ Codex app sessions, the JSON output also includes
372
+ `heartbeat_recommendations` with role-specific poll prompts; Dispatch can
373
+ deliver into those inboxes, but a heartbeat or operator wake-up is still
374
+ required to make an idle app thread poll autonomously. Those recommendations
375
+ include a `teardown_policy`: an idle poll is only a quiet interval, not a
376
+ reason to delete or pause heartbeat automation; heartbeat teardown belongs to
377
+ the manager/operator after terminal closeout or explicit operator instruction.
378
+ The optional
379
+ Codex app thread metadata is normally supplied after a Codex app manager has
380
+ used `create_thread` and `set_thread_title`; terminal-only users can omit it
381
+ and still use the manual no-tmux handoff.
350
382
  - `discover [QUERY] [--all] [--limit N]` / `search [QUERY]` — Search tasks,
351
383
  registered sessions, active bindings, and recent telemetry in one JSON result.
352
384
  Use this for conversational setup when a manager or Codex session needs to
@@ -407,8 +439,8 @@ tmux attach -t codex-live-test
407
439
  or manager inspection; `manager_config` is not a valid criteria source.
408
440
  To add a criterion and satisfy that same row after verification:
409
441
  ```bash
410
- criterion_id=$(conveyor criteria my-task --add --criterion "Targeted prompt tests pass" --source worker_proposed --status proposed | python3 -c 'import json,sys; print(json.load(sys.stdin)["affected_criterion"]["id"])')
411
- conveyor criteria my-task --satisfy "$criterion_id" --evidence-json '{"command":"python3 -m unittest tests.test_workerctl.ManagerBootstrapPromptTests -v","status":"pass"}'
442
+ criterion_id=$(conveyor criteria my-task --add --criterion "Targeted prompt tests pass" --source worker_proposed --status proposed | node -e 'const fs = require("fs"); console.log(JSON.parse(fs.readFileSync(0, "utf8")).affected_criterion.id)')
443
+ conveyor criteria my-task --satisfy "$criterion_id" --evidence-json '{"command":"npm test -- --runInBand","status":"pass"}'
412
444
  ```
413
445
  For mutation responses, treat `affected_criterion` as the authoritative
414
446
  receipt for the row changed by that command. When a manager applies multiple
@@ -713,7 +745,7 @@ tmux attach -t codex-live-test
713
745
  - `loop-status TASK --run RUN [--json]` — Summarize a Ralph-loop run for manager
714
746
  review: policy template, iteration bounds, command states, routed
715
747
  notifications, worker inbox backlog, evidence types, consumed-inbox
716
- telemetry, failure counts, and a recommendation.
748
+ and iteration-advanced telemetry, failure counts, and a recommendation.
717
749
 
718
750
  For real vertical slices, start with the Ralph loop operator guide in
719
751
  `docs/qa/ralph-loop-operator-guide.md`. It explains the controlled
@@ -722,7 +754,12 @@ required evidence, adversarial proof, `loop-status`, and telemetry review pass
722
754
  bar.
723
755
  Use `create-disposable-binding` when the manager and worker are Codex app or
724
756
  other no-tmux sessions and you want the same Dispatch rails without manual
725
- task/session/bind setup.
757
+ task/session/bind setup. In a Codex app manager session, prefer a fresh
758
+ same-project `create_thread` worker plus `set_thread_title` before creating the
759
+ binding, then pass the worker thread id/title into Conveyor. Use `fork_thread`
760
+ only when the user explicitly asks to fork or resume this conversation. If app
761
+ thread tools are unavailable, create the binding anyway and paste the returned
762
+ `worker_handoff` prompt into a manually opened worker session.
726
763
  - `enqueue-continue-iteration TASK --loop-run RUN --requested-iteration N` —
727
764
  Queue a manager-requested next loop pass for Dispatch. The command refuses
728
765
  same/current iteration requests before they become pending queue rows, while
@@ -980,16 +1017,27 @@ Current dispatch state:
980
1017
  in `routed_notifications`, and threaded with `correlation_id`.
981
1018
  - The session inbox is the same `routed_notifications` stream addressed by
982
1019
  `target_session_id`: tmux push is optional transport. Codex app-based sessions
983
- should long-poll with `manager-inbox --consume-next --wait --json` or
984
- `worker-inbox --consume-next --wait --json`. For disposable Ralph loops, use
985
- the generated `worker_handoff` prompt so the worker keeps polling until no
986
- inbox item remains or the loop reaches `max_iterations`.
1020
+ should long-poll with the returned `communication.poll_command`. For
1021
+ disposable Ralph loops, use the generated `worker_handoff` prompt so the
1022
+ worker keeps polling until no inbox item remains or the loop reaches
1023
+ `max_iterations`. For no-tmux Codex app sessions, treat
1024
+ `communication.requires_polling=true` as requiring a heartbeat/wake layer:
1025
+ a delivered pull inbox item does not by itself wake an idle app thread. Do
1026
+ not delete or pause heartbeats because an inbox poll is idle. A terminal
1027
+ manager decision should be followed by `finish-task --require-criteria-audit`
1028
+ or by an explicit blocker explaining why the task/binding still appears
1029
+ active.
987
1030
  - `register-worker`, `register-manager`, `sessions`, `discover`, and
988
1031
  `create-disposable-binding --json` expose a `communication` block per
989
1032
  session. Treat `session_kind='tmux'` plus `receive_style='push'` as direct
990
1033
  tmux-delivery capable; treat `session_kind='codex_app'` plus
991
1034
  `receive_style='pull'` as mailbox polling required for that worker or
992
1035
  manager.
1036
+ - App-assisted setup may also expose `codex_app_thread_id` and
1037
+ `codex_app_thread_title` for sessions created or identified by Codex app
1038
+ thread tools. Treat those fields as human/app navigation metadata; the
1039
+ durable communication record is still `routed_notifications` plus inbox
1040
+ consumption telemetry.
993
1041
  - Template-backed `continue_iteration` deliveries include `loop_policy` in the
994
1042
  inbox payload, with template name, current/max iteration, cleanup policy,
995
1043
  required evidence, artifact requirements, and recommended tools. Codex
@@ -1001,6 +1049,10 @@ Current dispatch state:
1001
1049
  - Consuming a mailbox item records `dispatch_inbox_consumed` telemetry with the
1002
1050
  notification id, signal type, delivery mode, target session role, and poll
1003
1051
  count, so manager/worker dispatcher handoffs are visible in audit evidence.
1052
+ When the item is `continue_iteration`, Conveyor also advances the run
1053
+ metadata's durable `current_iteration` to the requested iteration and records
1054
+ `ralph_loop_iteration_advanced` telemetry keyed to the run, notification,
1055
+ command, and consuming session.
1004
1056
  - If `doctor-self --json` reports `workerctl_on_path=false` inside a Codex app
1005
1057
  session, run `conveyor ...` from the repository root or install the
1006
1058
  local wrapper with `scripts/install-local --write`. Its `inside_tmux` check
@@ -1112,9 +1164,8 @@ scripts/rc-check --with-live-smoke-repeat
1112
1164
  Underlying deterministic checks:
1113
1165
 
1114
1166
  ```bash
1115
- python3 -m unittest discover -s tests -v
1116
- scripts/check-resource-warnings
1117
- python3 -m py_compile scripts/workerctl scripts/check-resource-warnings workerctl/*.py
1167
+ scripts/check-resource-warnings -- npm test -- --runInBand
1168
+ npm run build
1118
1169
  npm run migration:audit:final
1119
1170
  scripts/package-smoke
1120
1171
  scripts/release-check
@@ -1123,10 +1174,9 @@ scripts/release-check
1123
1174
  For local parallel experiments, prefer:
1124
1175
 
1125
1176
  ```bash
1126
- scripts/run-unittests-isolated
1177
+ npm test
1127
1178
  ```
1128
1179
 
1129
- This gives the process a temporary `WORKERCTL_STATE_ROOT` and a test namespace.
1130
1180
  The standard CI job remains serial.
1131
1181
 
1132
1182
  GitHub Actions runs `scripts/rc-check --skip-live-smoke-repeat` and
@@ -1134,7 +1184,7 @@ GitHub Actions runs `scripts/rc-check --skip-live-smoke-repeat` and
1134
1184
  remains local/manual because hosted runners may not have `codex`.
1135
1185
  The ResourceWarning gate intentionally fails on any `ResourceWarning` text in
1136
1186
  test output so finalization-time resource warnings cannot be hidden by a zero
1137
- `unittest` exit status.
1187
+ test exit status.
1138
1188
 
1139
1189
  Live local smoke gate:
1140
1190
 
@@ -425,6 +425,10 @@ function parseRuntimeArgs(args, env) {
425
425
  candidate: null,
426
426
  check: null,
427
427
  classifyPrompt: null,
428
+ workerCodexAppThreadId: null,
429
+ workerCodexAppThreadTitle: null,
430
+ managerCodexAppThreadId: null,
431
+ managerCodexAppThreadTitle: null,
428
432
  codexSession: null,
429
433
  create: null,
430
434
  createRun: null,
@@ -1947,6 +1951,31 @@ function parseRuntimeArgs(args, env) {
1947
1951
  flags.manager = value.value;
1948
1952
  index += 1;
1949
1953
  }
1954
+ else if (arg === "--worker-codex-app-thread-id"
1955
+ || arg === "--worker-codex-app-thread-title"
1956
+ || arg === "--manager-codex-app-thread-id"
1957
+ || arg === "--manager-codex-app-thread-title") {
1958
+ if (command !== "create-disposable-binding") {
1959
+ return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
1960
+ }
1961
+ const value = valueAfter(queue, index, arg);
1962
+ if (value.error) {
1963
+ return { command, enabled, error: value.error, explicit, flags, task };
1964
+ }
1965
+ if (arg === "--worker-codex-app-thread-id") {
1966
+ flags.workerCodexAppThreadId = value.value;
1967
+ }
1968
+ else if (arg === "--worker-codex-app-thread-title") {
1969
+ flags.workerCodexAppThreadTitle = value.value;
1970
+ }
1971
+ else if (arg === "--manager-codex-app-thread-id") {
1972
+ flags.managerCodexAppThreadId = value.value;
1973
+ }
1974
+ else {
1975
+ flags.managerCodexAppThreadTitle = value.value;
1976
+ }
1977
+ index += 1;
1978
+ }
1950
1979
  else if (arg === "--template") {
1951
1980
  if (command !== "create-disposable-binding" && command !== "loop-templates") {
1952
1981
  return { command, enabled, error: "Unsupported TypeScript runtime option: --template", explicit, flags, task };
@@ -3968,10 +3997,10 @@ function qaRunBuildClearLoop(context) {
3968
3997
  const artifactDir = qaArtifactDir(context, "build-clear-loop", slug, run.id);
3969
3998
  const buildReceipt = join(artifactDir, "build-passed.json");
3970
3999
  mkdirSync(dirname(buildReceipt), { recursive: true });
3971
- writeFileSync(buildReceipt, `${JSON.stringify(sortJson({ command: "scripts/run-unittests-isolated -k build_clear_loop", result: "pass", status: "build_passed" }), null, 2)}\n`);
4000
+ writeFileSync(buildReceipt, `${JSON.stringify(sortJson({ command: "npm test -- --runInBand", result: "pass", status: "build_passed" }), null, 2)}\n`);
3972
4001
  qaRecordLoopEvidence(context, task, run.id, "build_passed", "qa-run-build-clear-build-passed", {
3973
4002
  artifactPath: buildReceipt,
3974
- metadata: { command: "scripts/run-unittests-isolated -k build_clear_loop", result: "Focused build/test command passed before retry." },
4003
+ metadata: { command: "npm test -- --runInBand", result: "Focused build/test command passed before retry." },
3975
4004
  });
3976
4005
  enqueueQaContinue(context, task, run.id, "qa-run-build-clear-build-only", "Run after build evidence only.");
3977
4006
  const buildOnlyDispatch = qaDispatchContinueOnce(context, "qa-run-build-clear-build-only");
@@ -4884,19 +4913,14 @@ function dispatchWatchCommand(workerctlPath, dispatcherId, dbPath) {
4884
4913
  }
4885
4914
  function installableSkillSources() {
4886
4915
  const root = packageRootFromRuntimeModule();
4887
- const candidates = [
4888
- join(root, "skills"),
4889
- join(root, "workerctl", "assets", "skills"),
4890
- ];
4891
- for (const candidate of candidates) {
4892
- const skills = ["manage-codex-workers", "codex-review"]
4893
- .map((name) => ({ name, source: join(candidate, name) }))
4894
- .filter((skill) => existsSync(join(skill.source, "SKILL.md")));
4895
- if (skills.length === 2) {
4896
- return skills;
4897
- }
4916
+ const candidate = join(root, "skills");
4917
+ const skills = ["manage-codex-workers", "codex-review"]
4918
+ .map((name) => ({ name, source: join(candidate, name) }))
4919
+ .filter((skill) => existsSync(join(skill.source, "SKILL.md")));
4920
+ if (skills.length === 2) {
4921
+ return skills;
4898
4922
  }
4899
- throw new Error("Bundled Agent Conveyor skills not found in skills/ or workerctl/assets/skills.");
4923
+ throw new Error("Bundled Agent Conveyor skills not found in skills/.");
4900
4924
  }
4901
4925
  function runBindCommand(parsed, options) {
4902
4926
  const unsupported = unsupportedBindOptions(parsed);
@@ -4997,6 +5021,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
4997
5021
  const workerRollout = writeDisposableRollout(sessionDir, workerName, cwd);
4998
5022
  const managerRollout = writeDisposableRollout(sessionDir, managerName, cwd);
4999
5023
  const worker = registerSessionSync(database, {
5024
+ codexAppThreadId: parsed.flags.workerCodexAppThreadId,
5025
+ codexAppThreadTitle: parsed.flags.workerCodexAppThreadTitle,
5000
5026
  codexSessionPath: workerRollout.path,
5001
5027
  cwd,
5002
5028
  name: workerName,
@@ -5005,6 +5031,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
5005
5031
  tmuxSession: null,
5006
5032
  });
5007
5033
  const manager = registerSessionSync(database, {
5034
+ codexAppThreadId: parsed.flags.managerCodexAppThreadId,
5035
+ codexAppThreadTitle: parsed.flags.managerCodexAppThreadTitle,
5008
5036
  codexSessionPath: managerRollout.path,
5009
5037
  cwd,
5010
5038
  name: managerName,
@@ -5043,6 +5071,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
5043
5071
  db_path: dbPath,
5044
5072
  manager: {
5045
5073
  communication: disposableSessionCommunication("manager", task.name, dbPath),
5074
+ codex_app_thread_id: manager.codex_app_thread_id,
5075
+ codex_app_thread_title: manager.codex_app_thread_title,
5046
5076
  id: manager.session_id,
5047
5077
  name: managerName,
5048
5078
  rollout_path: managerRollout.path,
@@ -5068,11 +5098,14 @@ function runCreateDisposableBindingCommand(parsed, options) {
5068
5098
  },
5069
5099
  worker: {
5070
5100
  communication: disposableSessionCommunication("worker", task.name, dbPath),
5101
+ codex_app_thread_id: worker.codex_app_thread_id,
5102
+ codex_app_thread_title: worker.codex_app_thread_title,
5071
5103
  id: worker.session_id,
5072
5104
  name: workerName,
5073
5105
  rollout_path: workerRollout.path,
5074
5106
  tmux_session: null,
5075
5107
  },
5108
+ heartbeat_recommendations: disposableHeartbeatRecommendations(task.name, dbPath),
5076
5109
  worker_handoff: disposableWorkerHandoff(task.name, run?.name ?? null, dbPath),
5077
5110
  };
5078
5111
  if (parsed.flags.json) {
@@ -16581,6 +16614,9 @@ function disposableWorkerHandoff(taskName, runName, dbPath) {
16581
16614
  `You are the worker for task ${taskName}${loopClause}.`,
16582
16615
  "Keep polling your Conveyor worker inbox until there are no items left or the loop reaches max_iterations. Consume the next item now, treat each consumed item as the manager's next instruction, complete the requested work, and report changed files, exact commands run, evidence, and any residual risk.",
16583
16616
  "",
16617
+ "Because this is a pull-required Codex app/no-tmux session, autonomous operation requires a heartbeat/wake layer that repeats this worker inbox poll while the thread is idle. If no heartbeat automation is available, report the loop as manual-poll only.",
16618
+ "Do not delete, pause, or disable heartbeat automation just because an inbox poll is idle; the manager or operator owns terminal loop teardown.",
16619
+ "",
16584
16620
  `Run: ${sessionPollCommand("worker", taskName, dbPath)}`,
16585
16621
  ].join("\n");
16586
16622
  }
@@ -16595,14 +16631,72 @@ function renderDisposableBindingText(result) {
16595
16631
  }
16596
16632
  lines.push("Replay commands:");
16597
16633
  lines.push(...result.replay_commands.map((command) => ` ${command}`));
16634
+ if (result.heartbeat_recommendations) {
16635
+ lines.push("Heartbeat recommendations:");
16636
+ lines.push(` interval: every ${result.heartbeat_recommendations.interval_minutes} minutes`);
16637
+ lines.push(` manager: ${result.heartbeat_recommendations.manager.poll_command}`);
16638
+ lines.push(` worker: ${result.heartbeat_recommendations.worker.poll_command}`);
16639
+ lines.push(` teardown: ${result.heartbeat_recommendations.teardown_policy.idle_poll}`);
16640
+ lines.push(` closeout: ${result.heartbeat_recommendations.teardown_policy.terminal_closeout_command}`);
16641
+ }
16598
16642
  lines.push("Worker handoff:");
16599
16643
  lines.push(result.worker_handoff);
16600
16644
  return `${lines.join("\n")}\n`;
16601
16645
  }
16646
+ function disposableHeartbeatRecommendations(taskName, dbPath) {
16647
+ const terminalCloseoutCommand = `${conveyorPollInvocation()} finish-task ${shellQuote(taskName)} --reason ${shellQuote("Verified terminal closeout")} --require-criteria-audit --path ${shellQuote(dbPath)}`;
16648
+ return {
16649
+ applies_when: {
16650
+ can_receive_push: false,
16651
+ delivery_mode: "pull_required",
16652
+ receive_style: "pull",
16653
+ session_kind: "codex_app",
16654
+ },
16655
+ interval_minutes: 2,
16656
+ note: "Dispatch can deliver pull-required inbox items, but Codex app/no-tmux sessions still need a heartbeat or operator wake-up to poll while idle.",
16657
+ teardown_policy: {
16658
+ idle_poll: "Never delete, pause, or disable a manager or worker heartbeat because an inbox poll returned no item; that is only a quiet poll interval.",
16659
+ owner: "manager_or_operator",
16660
+ terminal_closeout: "Only the manager or operator should tear down heartbeats, and only after a terminal manager decision plus verified task closeout, or after explicit operator instruction.",
16661
+ terminal_closeout_command: terminalCloseoutCommand,
16662
+ worker_rule: "The worker must not own loop teardown and must not remove heartbeat automation based on idle polling.",
16663
+ },
16664
+ manager: {
16665
+ kind: "thread_heartbeat",
16666
+ poll_command: sessionPollCommand("manager", taskName, dbPath),
16667
+ prompt: [
16668
+ "Use the manage-codex-workers skill.",
16669
+ `Poll the manager inbox for task ${taskName}.`,
16670
+ `Run: ${sessionPollCommand("manager", taskName, dbPath)}`,
16671
+ "If an item is consumed, execute only that manager instruction, verify worker claims before recording conclusions, update Conveyor state as appropriate, and produce exactly one next worker task.",
16672
+ "If no item is consumed, stop after a one-line idle receipt.",
16673
+ "Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
16674
+ `If all accepted criteria are satisfied, deferred, or rejected and there is no next worker task, record the terminal manager decision, run or report the result of: ${terminalCloseoutCommand}`,
16675
+ "After verified task closeout, explicitly report heartbeat teardown status; if the task remains managed/active, report that as a control-plane blocker instead of calling the loop complete.",
16676
+ ].join("\n"),
16677
+ },
16678
+ worker: {
16679
+ kind: "thread_heartbeat",
16680
+ poll_command: sessionPollCommand("worker", taskName, dbPath),
16681
+ prompt: [
16682
+ "Use the manage-codex-workers skill.",
16683
+ `Poll the worker inbox for task ${taskName}.`,
16684
+ `Run: ${sessionPollCommand("worker", taskName, dbPath)}`,
16685
+ "If an item is consumed, execute only that single worker instruction and return exact commands, compact evidence, blockers/residual risk, and exactly one next recommended worker task.",
16686
+ "If no item is consumed, stop after a one-line idle receipt.",
16687
+ "Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
16688
+ ].join("\n"),
16689
+ },
16690
+ };
16691
+ }
16602
16692
  function sessionPollCommand(role, taskName, dbPath) {
16603
16693
  const inbox = role === "worker" ? "worker-inbox" : "manager-inbox";
16604
16694
  const task = taskName ? shellQuote(taskName) : "<task>";
16605
- return `conveyor ${inbox} ${task} --consume-next --wait --timeout 60 --path ${shellQuote(dbPath)} --json`;
16695
+ return `${conveyorPollInvocation()} ${inbox} ${task} --consume-next --wait --timeout 60 --path ${shellQuote(dbPath)} --json`;
16696
+ }
16697
+ function conveyorPollInvocation() {
16698
+ const binDir = join(packageRootFromRuntimeModule(), "bin");
16699
+ return pathIsExecutable(join(binDir, "conveyor")) ? `PATH=${shellQuote(binDir)}:$PATH conveyor` : "conveyor";
16606
16700
  }
16607
16701
  function resolveCodexStartupOptions(options) {
16608
16702
  const defaults = {