agent-conveyor 0.1.23 → 0.1.25

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
@@ -189,12 +189,21 @@ the no-tmux binding with `create-disposable-binding`, point the worker at
189
189
  that the loop is ready. Pair and worker-set skills run
190
190
  `conveyor-smoke-app-connections` before real task work by default. Required
191
191
  smoke first checks package/plugin/ledger/thread metadata, then starts a
192
- nonce-scoped `app-smoke` session and blocks real work until the manager and each
193
- worker have visible app-thread send receipts, fresh app heartbeats, durable
194
- received/accepted acknowledgements, and an `app-smoke status` result with
195
- `real_work_allowed=true`. The plain CLI records and evaluates receipts; the
196
- Codex app skill/operator layer must perform live `send_message_to_thread`
197
- delivery and record `sent`, `blocked`, or skipped/advisory evidence.
192
+ nonce-scoped `app-smoke` session and blocks real work until the worker has
193
+ accepted smoke and delivered the manager report, then the manager validates
194
+ that report. Both roles need visible app-thread send receipts, fresh app
195
+ heartbeats, durable received/accepted acknowledgements, and an
196
+ `app-smoke status` result with `real_work_allowed=true`. The plain CLI records
197
+ and evaluates receipts; the Codex app skill/operator layer must perform live
198
+ `send_message_to_thread` delivery and record `sent`, `blocked`, or
199
+ skipped/advisory evidence. If a smoke role blocks and later succeeds, the later
200
+ accepted receipt for the same smoke id/nonce becomes the current terminal role
201
+ state. After required smoke passes, pair and worker-set skills start
202
+ `app-autopilot` before real work so the just-proved sessions keep polling. A
203
+ setup is autonomous only when the emitted heartbeat automation specs have been
204
+ applied and recorded with `app-autopilot record-automation`, or explicitly
205
+ deferred as `manual-poll only`; smoke-passed by itself only proves connection
206
+ plumbing at that moment.
198
207
  When the manager is itself running in the Codex app and
199
208
  thread tools are available, the skill should first call `create_thread` for a
200
209
  fresh same-project worker, name it with `set_thread_title`, pass the returned
@@ -533,16 +542,19 @@ stay out of receipts.
533
542
  a matching `ready_to_send` action with `send_ready=true` and the same thread
534
543
  id; healthy roles must be recorded as `skipped`; missing-thread blockers and
535
544
  failed app-thread sends must be recorded as `blocked` with a reason.
536
- - `app-autopilot start|stop|status TASK [--dispatcher-id ID]
545
+ - `app-autopilot start|stop|status|record-automation TASK [--dispatcher-id ID]
537
546
  [--interval SECONDS] [--watch-iterations N] [--stale-after N]
538
- [--quiet-after N] [--json]` —
547
+ [--quiet-after N] [--role manager|worker --automation-id ID] [--json]` —
539
548
  Manage the pair-level app-native heartbeat policy for the active
540
549
  manager/worker binding. `start` and `stop` write telemetry receipts and emit
541
550
  the exact manager/worker Codex app heartbeat automation specs plus the
542
551
  bounded Dispatch watch command. A plain shell CLI cannot call Codex app
543
552
  thread tools, so create/pause those heartbeat automations from a Codex app
544
- operator session using the emitted specs; Conveyor remains the durable source
545
- of truth through Dispatch, inboxes, wake receipts, and app heartbeat status.
553
+ operator session using the emitted specs, then run `record-automation` once
554
+ per created role automation. Conveyor remains the durable source of truth
555
+ through Dispatch, inboxes, automation-applied receipts, wake receipts, and app
556
+ heartbeat status. `status` reports `plan.readiness`; do not call a loop
557
+ autonomous unless `plan.readiness.autonomous_ready=true`.
546
558
  `status` also reports `plan.quiescence`: when the loop is healthy, has no
547
559
  `next_actions`, and both roles have produced `--quiet-after` paired
548
560
  heartbeats since the last command or inbox-consumption receipt, it recommends
@@ -560,7 +572,9 @@ stay out of receipts.
560
572
  same blockers without blocking, and skip mode records an explicit bypass. The
561
573
  CLI does not call Codex app thread tools; use the
562
574
  `conveyor-smoke-app-connections` plugin skill from a Codex app operator
563
- session for live `send_message_to_thread` delivery.
575
+ session for live `send_message_to_thread` delivery. A smoke-passed operator
576
+ flow should immediately run `app-autopilot start` and handle the emitted
577
+ automation specs before sending real task prompts.
564
578
  - `app-worker-rotation-plan TASK --old-worker-thread-id ID [--require-handoff]
565
579
  [--reason TEXT] [--json]` — Prepare a Codex app fresh-worker rotation. The
566
580
  CLI verifies that `ID` exactly matches the active bound worker session before
@@ -845,7 +859,8 @@ stay out of receipts.
845
859
  either received, accepted, or blocked on it. `received` requires delivery;
846
860
  `accepted` and `blocked` require consumption. Use this after visible Codex app
847
861
  sessions print `CONVEYOR RECEIVED`; do not use direct app-thread text as the
848
- durable receipt.
862
+ durable receipt. `--from-stdin` expects a JSON object, for example
863
+ `printf '%s\n' '{"summary":"accepted","evidence":["consumed notification"],"blockers":[]}' | conveyor inbox-ack ... --from-stdin`.
849
864
 
850
865
  ### Actuation
851
866
 
@@ -444,13 +444,14 @@ function commandHelpText(program, command) {
444
444
  ` ${program} dispatch --once --type nudge_worker --path /tmp/work/workerctl.db`,
445
445
  ],
446
446
  "app-autopilot": [
447
- `usage: ${program} app-autopilot start|stop|status <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--quiet-after N] ${path} [--json]`,
447
+ `usage: ${program} app-autopilot start|stop|status|record-automation <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--quiet-after N] [--role manager|worker --automation-id ID] ${path} [--json]`,
448
448
  "",
449
449
  "Manage the app-native heartbeat policy for a bound manager/worker pair.",
450
450
  "The CLI records policy receipts and emits Codex app heartbeat automation specs; app-thread automation creation still happens through Codex app tools.",
451
451
  "",
452
452
  "Examples:",
453
453
  ` ${program} app-autopilot start dogfood --dispatcher-id dispatch-local --path /tmp/work/workerctl.db --json`,
454
+ ` ${program} app-autopilot record-automation dogfood --role worker --automation-id conveyor-dogfood-worker-heartbeat --path /tmp/work/workerctl.db --json`,
454
455
  ` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
455
456
  ` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
456
457
  ],
@@ -544,6 +545,7 @@ function parseRuntimeArgs(args, env) {
544
545
  all: false,
545
546
  allowAdditionalReceipt: false,
546
547
  action: null,
548
+ automationId: null,
547
549
  artifactPath: null,
548
550
  assetType: null,
549
551
  assignment: null,
@@ -1450,6 +1452,17 @@ function parseRuntimeArgs(args, env) {
1450
1452
  flags.codexProfile = value.value;
1451
1453
  index += 1;
1452
1454
  }
1455
+ else if (arg === "--automation-id") {
1456
+ if (command !== "app-autopilot") {
1457
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --automation-id", explicit, flags, task };
1458
+ }
1459
+ const value = valueAfter(queue, index, arg);
1460
+ if (value.error) {
1461
+ return { command, enabled, error: value.error, explicit, flags, task };
1462
+ }
1463
+ flags.automationId = value.value;
1464
+ index += 1;
1465
+ }
1453
1466
  else if (arg === "--session-dir") {
1454
1467
  if (command !== "create-disposable-binding") {
1455
1468
  return { command, enabled, error: "Unsupported TypeScript runtime option: --session-dir", explicit, flags, task };
@@ -3130,7 +3143,7 @@ function parseRuntimeArgs(args, env) {
3130
3143
  flags.subtype = arg;
3131
3144
  }
3132
3145
  else if (command === "app-autopilot" && flags.action === null) {
3133
- if (!["start", "stop", "status"].includes(arg)) {
3146
+ if (!["record-automation", "start", "stop", "status"].includes(arg)) {
3134
3147
  return { command, enabled, error: `Unsupported app-autopilot action: ${arg}`, explicit, flags, task };
3135
3148
  }
3136
3149
  flags.action = arg;
@@ -4349,8 +4362,8 @@ function renderAppWorkerRotationPlanText(plan) {
4349
4362
  }
4350
4363
  function runAppAutopilotCommand(parsed, options) {
4351
4364
  const action = parsed.flags.action;
4352
- if (action !== "start" && action !== "stop" && action !== "status") {
4353
- return errorResult("app-autopilot requires an action: start, stop, or status");
4365
+ if (action !== "record-automation" && action !== "start" && action !== "stop" && action !== "status") {
4366
+ return errorResult("app-autopilot requires an action: start, stop, status, or record-automation");
4354
4367
  }
4355
4368
  const taskName = requireTask(parsed);
4356
4369
  const database = openRuntimeDatabase(parsed, options);
@@ -4358,7 +4371,39 @@ function runAppAutopilotCommand(parsed, options) {
4358
4371
  const timestamp = nowIsoSeconds(options);
4359
4372
  const dbPath = runtimeDbPath(parsed, options);
4360
4373
  const dispatcherId = parsed.flags.dispatcherId ?? "dispatch-local";
4374
+ const task = taskRowForDiagnostics(database, taskName);
4361
4375
  const desiredState = action === "start" ? "active" : action === "stop" ? "stopped" : null;
4376
+ let receipt = null;
4377
+ if (action === "record-automation") {
4378
+ const role = parseAppSmokeRole(parsed.flags.role);
4379
+ if (role instanceof Error) {
4380
+ return errorResult(role.message);
4381
+ }
4382
+ const automationId = requiredStringFlag(parsed.flags.automationId, "--automation-id");
4383
+ const eventId = emitTelemetrySync(database, {
4384
+ actor: "operator",
4385
+ attributes: {
4386
+ automation_id: automationId,
4387
+ role,
4388
+ },
4389
+ correlation: {
4390
+ action,
4391
+ command: "app-autopilot",
4392
+ dispatcher_id: dispatcherId,
4393
+ role,
4394
+ },
4395
+ eventType: "app_autopilot_automation_applied",
4396
+ severity: "info",
4397
+ summary: `App autopilot ${role} automation applied for ${task.name}.`,
4398
+ taskId: task.id,
4399
+ timestamp,
4400
+ });
4401
+ receipt = {
4402
+ event_id: eventId,
4403
+ event_type: "app_autopilot_automation_applied",
4404
+ recorded_at: timestamp,
4405
+ };
4406
+ }
4362
4407
  let plan = appAutopilotPlanSync(database, {
4363
4408
  dbPath,
4364
4409
  dispatchIntervalSeconds: parsed.flags.intervalSeconds,
@@ -4371,12 +4416,12 @@ function runAppAutopilotCommand(parsed, options) {
4371
4416
  taskName,
4372
4417
  watchIterations: parsed.flags.watchIterations ?? 1000000,
4373
4418
  });
4374
- let receipt = null;
4375
4419
  if (action === "start" || action === "stop") {
4376
4420
  const eventType = action === "start" ? "app_autopilot_started" : "app_autopilot_stopped";
4377
4421
  const eventId = emitTelemetrySync(database, {
4378
4422
  actor: "operator",
4379
4423
  attributes: {
4424
+ automation_state: plan.automation_state,
4380
4425
  automation_specs: plan.automation_specs.map((spec) => ({
4381
4426
  can_create: spec.can_create,
4382
4427
  interval_minutes: spec.interval_minutes,
@@ -4390,6 +4435,7 @@ function runAppAutopilotCommand(parsed, options) {
4390
4435
  dispatcher_id: dispatcherId,
4391
4436
  interval_minutes: plan.interval_minutes,
4392
4437
  quiescence: plan.quiescence,
4438
+ readiness: plan.readiness,
4393
4439
  summary: plan.summary,
4394
4440
  },
4395
4441
  correlation: {
@@ -4662,8 +4708,10 @@ function appSmokeRoleStatus(role, session, receipts, smoke, options) {
4662
4708
  const roleReceipts = receipts.filter((receipt) => receipt.role === role && receipt.nonce === smoke.nonce);
4663
4709
  const sent = roleReceipts.some((receipt) => receipt.status === "sent");
4664
4710
  const received = roleReceipts.some((receipt) => receipt.status === "received");
4665
- const accepted = roleReceipts.some((receipt) => receipt.status === "accepted");
4666
- const blockedReceipt = roleReceipts.find((receipt) => receipt.status === "blocked");
4711
+ const terminalReceipts = roleReceipts.filter((receipt) => receipt.status === "accepted" || receipt.status === "blocked");
4712
+ const terminalReceipt = terminalReceipts.at(-1);
4713
+ const accepted = terminalReceipt?.status === "accepted";
4714
+ const blockedReceipt = terminalReceipt?.status === "blocked" ? terminalReceipt : undefined;
4667
4715
  const heartbeatFresh = session.last_heartbeat_at !== null
4668
4716
  && session.last_heartbeat_at >= smoke.recorded_at
4669
4717
  && secondsBetweenIso(session.last_heartbeat_at, options.now) <= options.staleAfterSeconds;
@@ -4739,14 +4787,18 @@ function latestAppSmokeSessionSync(database, options) {
4739
4787
  }
4740
4788
  function appSmokeReceiptsSync(database, options) {
4741
4789
  const rows = database.prepare(`
4742
- select attributes_json
4790
+ select id, timestamp, attributes_json
4743
4791
  from telemetry_events
4744
4792
  where task_id = ?
4745
4793
  and event_type = 'app_smoke_receipt_recorded'
4746
4794
  and json_extract(attributes_json, '$.smoke_id') = ?
4747
4795
  order by timestamp, id
4748
4796
  `).all(options.taskId, options.smokeId);
4749
- return rows.map((row) => JSON.parse(row.attributes_json));
4797
+ return rows.map((row) => ({
4798
+ ...JSON.parse(row.attributes_json),
4799
+ event_id: row.id,
4800
+ recorded_at: row.timestamp,
4801
+ }));
4750
4802
  }
4751
4803
  function appSmokeBoundSessionsSync(database, taskId) {
4752
4804
  const row = database.prepare(`
@@ -4952,6 +5004,7 @@ function renderAppWakeupPlanText(plan) {
4952
5004
  function renderAppAutopilotText(result) {
4953
5005
  const lines = [
4954
5006
  `App autopilot ${result.action} for ${result.plan.task.name}: ${result.plan.desired_state}`,
5007
+ `Readiness: ${result.plan.readiness.state}${result.plan.readiness.autonomous_ready ? " (autonomous)" : " (setup required)"}`,
4955
5008
  `Loop status: ${result.plan.status.ok ? "ok" : "attention required"}`,
4956
5009
  `Dispatch: ${result.plan.dispatcher.state}${result.plan.dispatcher.required ? " required" : ""}`,
4957
5010
  `Dispatch command: ${result.plan.control.dispatcher_command}`,
@@ -4973,12 +5026,19 @@ function renderAppAutopilotText(result) {
4973
5026
  else {
4974
5027
  lines.push("Last policy: unconfigured");
4975
5028
  }
5029
+ for (const blocker of result.plan.readiness.blockers) {
5030
+ lines.push(`Readiness blocker: ${blocker}`);
5031
+ }
4976
5032
  for (const spec of result.plan.automation_specs) {
4977
5033
  lines.push(`${spec.role} automation: ${spec.can_create ? "ready" : "blocked"} ${spec.name}`, ` thread: ${spec.target_thread_title ?? "(untitled)"} ${spec.target_thread_id ?? "(missing)"}`, ` schedule: ${spec.rrule}`);
4978
5034
  if (spec.blocker) {
4979
5035
  lines.push(` blocker: ${spec.blocker}`);
4980
5036
  }
4981
5037
  }
5038
+ for (const role of result.plan.automation_state.applied_roles) {
5039
+ const receipt = result.plan.automation_state.receipts.find((item) => item.role === role);
5040
+ lines.push(`${role} automation applied: ${receipt?.automation_id ?? "(unknown)"}`);
5041
+ }
4982
5042
  lines.push(result.plan.control.note);
4983
5043
  return `${lines.join("\n")}\n`;
4984
5044
  }
@@ -18966,6 +19026,7 @@ function telemetryEventsForRunSync(database, options) {
18966
19026
  }
18967
19027
  function appTaskDispatchSummarySync(database, options) {
18968
19028
  const taskDispatchEventTypes = [
19029
+ "app_autopilot_automation_applied",
18969
19030
  "app_autopilot_started",
18970
19031
  "app_autopilot_stopped",
18971
19032
  "app_heartbeat",