agent-conveyor 0.1.22 → 0.1.23

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
@@ -168,9 +168,9 @@ conveyor plugin-status
168
168
 
169
169
  The per-project default ledger for operator sessions is
170
170
  `.codex-workers/workerctl.db`. The initial included skills are
171
- `conveyor-app-wake-relay`, `conveyor-create-pair`,
172
- `conveyor-create-worker-set`, `conveyor-check-status`, and
173
- `conveyor-whats-next-nudger`.
171
+ `conveyor-app-wake-relay`, `conveyor-smoke-app-connections`,
172
+ `conveyor-create-pair`, `conveyor-create-worker-set`,
173
+ `conveyor-check-status`, and `conveyor-whats-next-nudger`.
174
174
 
175
175
  After install, the intended Codex app entry point is natural language. Open a
176
176
  new Codex app session in the target repo and say:
@@ -186,7 +186,16 @@ For multiple workers, start with `Use the conveyor-create-worker-set skill`.
186
186
  The installed plugin skill should call the `conveyor` CLI, choose names, create
187
187
  the no-tmux binding with `create-disposable-binding`, point the worker at
188
188
  `worker-inbox`, and use `loop-status` plus telemetry receipts before reporting
189
- that the loop is ready. When the manager is itself running in the Codex app and
189
+ that the loop is ready. Pair and worker-set skills run
190
+ `conveyor-smoke-app-connections` before real task work by default. Required
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.
198
+ When the manager is itself running in the Codex app and
190
199
  thread tools are available, the skill should first call `create_thread` for a
191
200
  fresh same-project worker, name it with `set_thread_title`, pass the returned
192
201
  thread identity through `--worker-codex-app-thread-id` and
@@ -539,6 +548,19 @@ stay out of receipts.
539
548
  heartbeats since the last command or inbox-consumption receipt, it recommends
540
549
  `stop_autopilot` so operators can quiesce blocked/no-progress loops instead
541
550
  of repeating idle pulses.
551
+ - `app-smoke preflight|start|record|status TASK [--mode required|advisory|skip]
552
+ [--scope pair|worker-set] [--smoke-id ID] [--nonce NONCE]
553
+ [--role manager|worker] [--status sent|skipped|received|accepted|blocked]
554
+ [--thread-id ID] [--notification-id N] [--worker-count N] [--from-stdin]
555
+ [--json]` —
556
+ Record and evaluate the durable side of Codex app connection smoke. Required
557
+ mode blocks real work until the active binding has bound app thread ids,
558
+ nonce-matching send receipts, fresh app heartbeats after smoke start, worker
559
+ `received`, and manager/worker `accepted` receipts. Advisory mode reports the
560
+ same blockers without blocking, and skip mode records an explicit bypass. The
561
+ CLI does not call Codex app thread tools; use the
562
+ `conveyor-smoke-app-connections` plugin skill from a Codex app operator
563
+ session for live `send_message_to_thread` delivery.
542
564
  - `app-worker-rotation-plan TASK --old-worker-thread-id ID [--require-handoff]
543
565
  [--reason TEXT] [--json]` — Prepare a Codex app fresh-worker rotation. The
544
566
  CLI verifies that `ID` exactly matches the active bound worker session before
@@ -144,6 +144,9 @@ export function runTypescriptRuntimeCommand(options) {
144
144
  if (parsed.command === "app-autopilot") {
145
145
  return runAppAutopilotCommand(parsed, options);
146
146
  }
147
+ if (parsed.command === "app-smoke") {
148
+ return runAppSmokeCommand(parsed, options);
149
+ }
147
150
  if (parsed.command === "qa-plan") {
148
151
  return runQaPlanCommand(parsed);
149
152
  }
@@ -451,6 +454,18 @@ function commandHelpText(program, command) {
451
454
  ` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
452
455
  ` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
453
456
  ],
457
+ "app-smoke": [
458
+ `usage: ${program} app-smoke preflight|start|record|status <task> [--mode required|advisory|skip] [--scope pair|worker-set] [--smoke-id ID] [--nonce NONCE] [--role manager|worker] [--status sent|skipped|received|accepted|blocked] [--thread-id ID] [--notification-id N] [--worker-count N] ${path} [--from-stdin] [--json]`,
459
+ "",
460
+ "Manage a blocking Codex app connection smoke lifecycle for manager/worker bindings.",
461
+ "The CLI records durable smoke receipts; the Codex app/plugin layer still performs live app-thread sends.",
462
+ "",
463
+ "Examples:",
464
+ ` ${program} app-smoke preflight dogfood --mode required --path /tmp/work/workerctl.db --json`,
465
+ ` ${program} app-smoke start dogfood --mode required --path /tmp/work/workerctl.db --json`,
466
+ ` ${program} app-smoke record dogfood --smoke-id smoke-123 --nonce nonce --role worker --status accepted --thread-id thread-worker --from-stdin --path /tmp/work/workerctl.db --json`,
467
+ ` ${program} app-smoke status dogfood --path /tmp/work/workerctl.db --json`,
468
+ ],
454
469
  "app-worker-rotation-plan": [
455
470
  `usage: ${program} app-worker-rotation-plan <task> --old-worker-thread-id ID [--require-handoff] [--reason TEXT] ${path} [--json]`,
456
471
  "",
@@ -630,6 +645,10 @@ function parseRuntimeArgs(args, env) {
630
645
  subtype: null,
631
646
  summary: null,
632
647
  source: null,
648
+ smokeId: null,
649
+ smokeMode: null,
650
+ smokeNonce: null,
651
+ smokeScope: null,
633
652
  objective: null,
634
653
  promptSummary: null,
635
654
  proof: null,
@@ -735,6 +754,7 @@ function parseRuntimeArgs(args, env) {
735
754
  telemetryView: null,
736
755
  telemetryViewTask: null,
737
756
  workerName: null,
757
+ workerCount: null,
738
758
  search: null,
739
759
  severity: null,
740
760
  staleCycleSeconds: 3600.0,
@@ -1515,7 +1535,7 @@ function parseRuntimeArgs(args, env) {
1515
1535
  index += 1;
1516
1536
  }
1517
1537
  else if (arg === "--from-stdin") {
1518
- if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack" && command !== "inbox-ack") {
1538
+ if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack" && command !== "inbox-ack" && command !== "app-smoke") {
1519
1539
  return { command, enabled, error: "Unsupported TypeScript runtime option: --from-stdin", explicit, flags, task };
1520
1540
  }
1521
1541
  flags.fromStdin = true;
@@ -1649,7 +1669,8 @@ function parseRuntimeArgs(args, env) {
1649
1669
  && command !== "app-loop-status"
1650
1670
  && command !== "app-wakeup-plan"
1651
1671
  && command !== "app-wakeup-dispatch"
1652
- && command !== "app-autopilot") {
1672
+ && command !== "app-autopilot"
1673
+ && command !== "app-smoke") {
1653
1674
  return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
1654
1675
  }
1655
1676
  const value = valueAfter(queue, index, arg);
@@ -2047,7 +2068,7 @@ function parseRuntimeArgs(args, env) {
2047
2068
  index += 1;
2048
2069
  }
2049
2070
  else if (arg === "--thread-id") {
2050
- if (command !== "app-wakeup-record-delivery" && command !== "campaign") {
2071
+ if (command !== "app-wakeup-record-delivery" && command !== "campaign" && command !== "app-smoke") {
2051
2072
  return { command, enabled, error: "Unsupported TypeScript runtime option: --thread-id", explicit, flags, task };
2052
2073
  }
2053
2074
  const value = valueAfter(queue, index, arg);
@@ -2325,6 +2346,14 @@ function parseRuntimeArgs(args, env) {
2325
2346
  index += 1;
2326
2347
  continue;
2327
2348
  }
2349
+ if (command === "app-smoke") {
2350
+ if (value !== "required" && value !== "advisory" && value !== "skip") {
2351
+ return { command, enabled, error: `Unsupported app-smoke mode: ${value}`, explicit, flags, task };
2352
+ }
2353
+ flags.smokeMode = value;
2354
+ index += 1;
2355
+ continue;
2356
+ }
2328
2357
  if (!isTranscriptCaptureMode(value)) {
2329
2358
  return { command, enabled, error: `Unsupported transcript capture mode: ${value}`, explicit, flags, task };
2330
2359
  }
@@ -2386,7 +2415,7 @@ function parseRuntimeArgs(args, env) {
2386
2415
  flags.epilogueStatus = true;
2387
2416
  continue;
2388
2417
  }
2389
- if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign" && command !== "inbox-ack") {
2418
+ if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign" && command !== "inbox-ack" && command !== "app-smoke") {
2390
2419
  return { command, enabled, error: "Unsupported TypeScript runtime option: --status", explicit, flags, task };
2391
2420
  }
2392
2421
  const parsedValue = valueAfter(queue, index, arg);
@@ -2396,7 +2425,7 @@ function parseRuntimeArgs(args, env) {
2396
2425
  if (command === "criteria") {
2397
2426
  flags.statuses.push(parsedValue.value);
2398
2427
  }
2399
- else if (command === "inbox-ack") {
2428
+ else if (command === "inbox-ack" || command === "app-smoke") {
2400
2429
  flags.statusState = parsedValue.value;
2401
2430
  }
2402
2431
  else {
@@ -2405,7 +2434,7 @@ function parseRuntimeArgs(args, env) {
2405
2434
  index += 1;
2406
2435
  }
2407
2436
  else if (arg === "--notification-id") {
2408
- if (command !== "inbox-ack") {
2437
+ if (command !== "inbox-ack" && command !== "app-smoke") {
2409
2438
  return { command, enabled, error: "Unsupported TypeScript runtime option: --notification-id", explicit, flags, task };
2410
2439
  }
2411
2440
  const parsedValue = valueAfter(queue, index, arg);
@@ -2419,6 +2448,43 @@ function parseRuntimeArgs(args, env) {
2419
2448
  flags.notificationId = value;
2420
2449
  index += 1;
2421
2450
  }
2451
+ else if (arg === "--smoke-id" || arg === "--nonce" || arg === "--scope") {
2452
+ if (command !== "app-smoke") {
2453
+ return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
2454
+ }
2455
+ const parsedValue = valueAfter(queue, index, arg);
2456
+ if (parsedValue.error) {
2457
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2458
+ }
2459
+ if (arg === "--smoke-id") {
2460
+ flags.smokeId = parsedValue.value;
2461
+ }
2462
+ else if (arg === "--nonce") {
2463
+ flags.smokeNonce = parsedValue.value;
2464
+ }
2465
+ else {
2466
+ if (parsedValue.value !== "pair" && parsedValue.value !== "worker-set") {
2467
+ return { command, enabled, error: `Unsupported app-smoke scope: ${parsedValue.value}`, explicit, flags, task };
2468
+ }
2469
+ flags.smokeScope = parsedValue.value;
2470
+ }
2471
+ index += 1;
2472
+ }
2473
+ else if (arg === "--worker-count") {
2474
+ if (command !== "app-smoke") {
2475
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --worker-count", explicit, flags, task };
2476
+ }
2477
+ const parsedValue = valueAfter(queue, index, arg);
2478
+ if (parsedValue.error) {
2479
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2480
+ }
2481
+ const value = Number(parsedValue.value);
2482
+ if (!Number.isInteger(value) || value <= 0) {
2483
+ return { command, enabled, error: "--worker-count must be a positive integer.", explicit, flags, task };
2484
+ }
2485
+ flags.workerCount = value;
2486
+ index += 1;
2487
+ }
2422
2488
  else if (arg === "--type") {
2423
2489
  const value = valueAfter(queue, index, arg);
2424
2490
  if (value.error) {
@@ -2572,7 +2638,8 @@ function parseRuntimeArgs(args, env) {
2572
2638
  && command !== "loop-evidence"
2573
2639
  && command !== "continuation"
2574
2640
  && command !== "continuation-reviewer"
2575
- && command !== "epilogue") {
2641
+ && command !== "epilogue"
2642
+ && command !== "app-smoke") {
2576
2643
  return { command, enabled, error: "Unsupported TypeScript runtime option: --correlation-id", explicit, flags, task };
2577
2644
  }
2578
2645
  const value = valueAfter(queue, index, arg);
@@ -3068,6 +3135,12 @@ function parseRuntimeArgs(args, env) {
3068
3135
  }
3069
3136
  flags.action = arg;
3070
3137
  }
3138
+ else if (command === "app-smoke" && flags.action === null) {
3139
+ if (!["preflight", "record", "start", "status"].includes(arg)) {
3140
+ return { command, enabled, error: `Unsupported app-smoke action: ${arg}`, explicit, flags, task };
3141
+ }
3142
+ flags.action = arg;
3143
+ }
3071
3144
  else if (command === "campaign" && flags.action === null) {
3072
3145
  if (!CAMPAIGN_ACTIONS.has(arg)) {
3073
3146
  return { command, enabled, error: unsupportedCampaignActionMessage(arg), explicit, flags, task };
@@ -4358,6 +4431,407 @@ function runAppAutopilotCommand(parsed, options) {
4358
4431
  database.close();
4359
4432
  }
4360
4433
  }
4434
+ function runAppSmokeCommand(parsed, options) {
4435
+ const action = parsed.flags.action;
4436
+ if (action !== "preflight" && action !== "record" && action !== "start" && action !== "status") {
4437
+ return errorResult("app-smoke requires an action: preflight, start, record, or status");
4438
+ }
4439
+ const taskName = requireTask(parsed);
4440
+ const database = openRuntimeDatabase(parsed, options);
4441
+ try {
4442
+ const timestamp = nowIsoSeconds(options);
4443
+ const dbPath = runtimeDbPath(parsed, options);
4444
+ const task = taskRowForDiagnostics(database, taskName);
4445
+ if (action === "preflight") {
4446
+ const output = appSmokePreflightSync(database, {
4447
+ dbPath,
4448
+ mode: parseAppSmokeMode(parsed.flags.smokeMode),
4449
+ scope: parseAppSmokeScope(parsed.flags.smokeScope),
4450
+ task,
4451
+ workerCount: parsed.flags.workerCount ?? 1,
4452
+ });
4453
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(output) };
4454
+ }
4455
+ if (action === "start") {
4456
+ const mode = parseAppSmokeMode(parsed.flags.smokeMode);
4457
+ const scope = parseAppSmokeScope(parsed.flags.smokeScope);
4458
+ const binding = activeBindingForTaskSync(database, task.name);
4459
+ const smokeId = parsed.flags.smokeId ?? `smoke-${randomUUID()}`;
4460
+ const nonce = parsed.flags.smokeNonce ?? `nonce-${randomUUID()}`;
4461
+ const eventId = emitTelemetrySync(database, {
4462
+ actor: "operator",
4463
+ attributes: {
4464
+ binding_id: binding.binding_id,
4465
+ mode,
4466
+ nonce,
4467
+ scope,
4468
+ smoke_id: smokeId,
4469
+ worker_count: parsed.flags.workerCount ?? 1,
4470
+ },
4471
+ correlation: {
4472
+ command: "app-smoke",
4473
+ smoke_id: smokeId,
4474
+ },
4475
+ eventType: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
4476
+ severity: mode === "skip" ? "warning" : "info",
4477
+ summary: mode === "skip" ? `App smoke skipped for ${task.name}.` : `App smoke started for ${task.name}.`,
4478
+ taskId: task.id,
4479
+ timestamp,
4480
+ });
4481
+ const status = appSmokeStatusSync(database, {
4482
+ dbPath,
4483
+ now: timestamp,
4484
+ smokeId,
4485
+ staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
4486
+ task,
4487
+ });
4488
+ const output = {
4489
+ receipt: {
4490
+ event_id: eventId,
4491
+ event_type: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
4492
+ recorded_at: timestamp,
4493
+ },
4494
+ smoke: {
4495
+ id: smokeId,
4496
+ mode,
4497
+ nonce,
4498
+ scope,
4499
+ worker_count: parsed.flags.workerCount ?? 1,
4500
+ },
4501
+ status,
4502
+ task: { id: task.id, name: task.name },
4503
+ };
4504
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
4505
+ }
4506
+ if (action === "record") {
4507
+ const smokeId = requiredStringFlag(parsed.flags.smokeId, "--smoke-id");
4508
+ const nonce = requiredStringFlag(parsed.flags.smokeNonce, "--nonce");
4509
+ const role = parseAppSmokeRole(parsed.flags.role);
4510
+ const status = parseAppSmokeRecordStatus(parsed.flags.statusState);
4511
+ if (role instanceof Error) {
4512
+ return errorResult(role.message);
4513
+ }
4514
+ if (status instanceof Error) {
4515
+ return errorResult(status.message);
4516
+ }
4517
+ const session = latestAppSmokeSessionSync(database, { smokeId, taskId: task.id });
4518
+ if (session.nonce !== nonce) {
4519
+ throw new Error(`Smoke nonce mismatch for ${smokeId}; expected ${session.nonce}.`);
4520
+ }
4521
+ validateAppSmokeRecordThread(database, {
4522
+ role,
4523
+ status,
4524
+ taskId: task.id,
4525
+ taskName: task.name,
4526
+ threadId: parsed.flags.threadId,
4527
+ });
4528
+ const payload = parsed.flags.fromStdin ? parseStdinJsonObject(options.stdin) : {};
4529
+ const eventId = emitTelemetrySync(database, {
4530
+ actor: role,
4531
+ attributes: {
4532
+ notification_id: parsed.flags.notificationId,
4533
+ nonce,
4534
+ payload,
4535
+ payload_keys: Object.keys(payload).sort(),
4536
+ role,
4537
+ smoke_id: smokeId,
4538
+ status,
4539
+ thread_id: parsed.flags.threadId,
4540
+ },
4541
+ correlation: {
4542
+ command: "app-smoke",
4543
+ correlation_id: parsed.flags.correlationId,
4544
+ nonce,
4545
+ role,
4546
+ smoke_id: smokeId,
4547
+ },
4548
+ eventType: "app_smoke_receipt_recorded",
4549
+ severity: status === "blocked" ? "warning" : "info",
4550
+ summary: `App smoke ${status} recorded for ${role} on ${task.name}.`,
4551
+ taskId: task.id,
4552
+ timestamp,
4553
+ });
4554
+ const output = {
4555
+ receipt: {
4556
+ event_id: eventId,
4557
+ event_type: "app_smoke_receipt_recorded",
4558
+ recorded_at: timestamp,
4559
+ },
4560
+ smoke: {
4561
+ id: smokeId,
4562
+ nonce,
4563
+ },
4564
+ task: { id: task.id, name: task.name },
4565
+ };
4566
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: `App smoke ${status} recorded for ${role}.\n` };
4567
+ }
4568
+ const status = appSmokeStatusSync(database, {
4569
+ dbPath,
4570
+ now: timestamp,
4571
+ smokeId: parsed.flags.smokeId,
4572
+ staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
4573
+ task,
4574
+ });
4575
+ return parsed.flags.json ? jsonResult(status) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
4576
+ }
4577
+ finally {
4578
+ database.close();
4579
+ }
4580
+ }
4581
+ function appSmokePreflightSync(database, options) {
4582
+ let binding = null;
4583
+ let bindingError = null;
4584
+ try {
4585
+ binding = activeBindingForTaskSync(database, options.task.name);
4586
+ }
4587
+ catch (error) {
4588
+ bindingError = error instanceof Error ? error.message : String(error);
4589
+ }
4590
+ const bound = binding ? appSmokeBoundSessionsSync(database, options.task.id) : null;
4591
+ const checks = [
4592
+ { ok: true, name: "ledger_writable", path: options.dbPath },
4593
+ { ok: true, name: "package_cli_available", command: "app-smoke" },
4594
+ {
4595
+ detail: bindingError,
4596
+ ok: binding !== null,
4597
+ name: "active_binding",
4598
+ },
4599
+ {
4600
+ ok: Boolean(bound?.manager.thread_id),
4601
+ name: "manager_thread_metadata",
4602
+ thread_id: bound?.manager.thread_id ?? null,
4603
+ },
4604
+ {
4605
+ ok: Boolean(bound?.worker.thread_id),
4606
+ name: "worker_thread_metadata",
4607
+ thread_id: bound?.worker.thread_id ?? null,
4608
+ },
4609
+ {
4610
+ ok: false,
4611
+ name: "codex_app_thread_tools",
4612
+ note: "Plain CLI cannot call Codex app thread tools; the plugin skill must perform live sends.",
4613
+ required_in_plugin: true,
4614
+ },
4615
+ ];
4616
+ const blockers = options.mode === "required"
4617
+ ? checks.filter((check) => !check.ok && check.name !== "codex_app_thread_tools").map((check) => `${check.name}${check.detail ? `: ${check.detail}` : ""}`)
4618
+ : [];
4619
+ return {
4620
+ blockers,
4621
+ checks,
4622
+ mode: options.mode,
4623
+ ok: blockers.length === 0,
4624
+ real_work_allowed: options.mode !== "required" || blockers.length === 0,
4625
+ scope: options.scope,
4626
+ task: { id: options.task.id, name: options.task.name },
4627
+ worker_count: options.workerCount,
4628
+ };
4629
+ }
4630
+ function appSmokeStatusSync(database, options) {
4631
+ const smoke = latestAppSmokeSessionSync(database, { smokeId: options.smokeId, taskId: options.task.id });
4632
+ const bound = appSmokeBoundSessionsSync(database, options.task.id);
4633
+ const receipts = appSmokeReceiptsSync(database, { smokeId: smoke.smoke_id, taskId: options.task.id });
4634
+ const manager = appSmokeRoleStatus("manager", bound.manager, receipts, smoke, options);
4635
+ const worker = appSmokeRoleStatus("worker", bound.worker, receipts, smoke, options);
4636
+ const blockers = smoke.mode === "skip" ? [] : [
4637
+ ...appSmokeScopeBlockers(smoke),
4638
+ ...appSmokeRoleBlockers("manager", manager),
4639
+ ...appSmokeRoleBlockers("worker", worker),
4640
+ ];
4641
+ const ok = blockers.length === 0;
4642
+ return {
4643
+ blockers,
4644
+ ok,
4645
+ real_work_allowed: smoke.mode === "skip" || smoke.mode === "advisory" || ok,
4646
+ roles: {
4647
+ manager,
4648
+ worker,
4649
+ },
4650
+ smoke: {
4651
+ id: smoke.smoke_id,
4652
+ mode: smoke.mode,
4653
+ nonce: smoke.nonce,
4654
+ recorded_at: smoke.recorded_at,
4655
+ scope: smoke.scope,
4656
+ worker_count: smoke.worker_count,
4657
+ },
4658
+ task: { id: options.task.id, name: options.task.name },
4659
+ };
4660
+ }
4661
+ function appSmokeRoleStatus(role, session, receipts, smoke, options) {
4662
+ const roleReceipts = receipts.filter((receipt) => receipt.role === role && receipt.nonce === smoke.nonce);
4663
+ const sent = roleReceipts.some((receipt) => receipt.status === "sent");
4664
+ 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");
4667
+ const heartbeatFresh = session.last_heartbeat_at !== null
4668
+ && session.last_heartbeat_at >= smoke.recorded_at
4669
+ && secondsBetweenIso(session.last_heartbeat_at, options.now) <= options.staleAfterSeconds;
4670
+ return {
4671
+ accepted,
4672
+ blocked: Boolean(blockedReceipt),
4673
+ blocker: typeof blockedReceipt?.payload.summary === "string" ? blockedReceipt.payload.summary : null,
4674
+ heartbeat_fresh: heartbeatFresh,
4675
+ last_heartbeat_at: session.last_heartbeat_at,
4676
+ received,
4677
+ sent,
4678
+ session_id: session.session_id,
4679
+ session_name: session.session_name,
4680
+ thread_id: session.thread_id,
4681
+ };
4682
+ }
4683
+ function appSmokeRoleBlockers(role, status) {
4684
+ const blockers = [];
4685
+ if (!status.thread_id) {
4686
+ blockers.push(`${role} has no Codex app thread id`);
4687
+ }
4688
+ if (!status.sent) {
4689
+ blockers.push(`${role} smoke prompt has not been recorded as sent`);
4690
+ }
4691
+ if (!status.heartbeat_fresh) {
4692
+ blockers.push(`${role} heartbeat is not fresh after smoke start`);
4693
+ }
4694
+ if (role === "worker" && !status.received) {
4695
+ blockers.push("worker has not recorded smoke received");
4696
+ }
4697
+ if (!status.accepted) {
4698
+ blockers.push(`${role} has not accepted smoke`);
4699
+ }
4700
+ if (status.blocked) {
4701
+ blockers.push(`${role} smoke blocked${status.blocker ? `: ${status.blocker}` : ""}`);
4702
+ }
4703
+ return blockers;
4704
+ }
4705
+ function appSmokeScopeBlockers(smoke) {
4706
+ if (smoke.scope === "worker-set" && smoke.worker_count > 1) {
4707
+ return [`worker-set smoke for one task proves 1 of ${smoke.worker_count} workers; run per-worker smoke and aggregate in the plugin skill`];
4708
+ }
4709
+ return [];
4710
+ }
4711
+ function latestAppSmokeSessionSync(database, options) {
4712
+ const params = [options.taskId];
4713
+ const smokeClause = options.smokeId ? "and json_extract(attributes_json, '$.smoke_id') = ?" : "";
4714
+ if (options.smokeId) {
4715
+ params.push(options.smokeId);
4716
+ }
4717
+ const row = database.prepare(`
4718
+ select id, timestamp, event_type, attributes_json
4719
+ from telemetry_events
4720
+ where task_id = ?
4721
+ and event_type in ('app_smoke_started', 'app_smoke_skipped')
4722
+ ${smokeClause}
4723
+ order by timestamp desc, id desc
4724
+ limit 1
4725
+ `).get(...params);
4726
+ if (!row) {
4727
+ throw new Error(options.smokeId ? `Unknown app smoke session: ${options.smokeId}` : "No app smoke session has been started for this task.");
4728
+ }
4729
+ const attributes = JSON.parse(row.attributes_json);
4730
+ return {
4731
+ event_id: row.id,
4732
+ mode: attributes.mode,
4733
+ nonce: attributes.nonce,
4734
+ recorded_at: row.timestamp,
4735
+ scope: attributes.scope,
4736
+ smoke_id: attributes.smoke_id,
4737
+ worker_count: attributes.worker_count,
4738
+ };
4739
+ }
4740
+ function appSmokeReceiptsSync(database, options) {
4741
+ const rows = database.prepare(`
4742
+ select attributes_json
4743
+ from telemetry_events
4744
+ where task_id = ?
4745
+ and event_type = 'app_smoke_receipt_recorded'
4746
+ and json_extract(attributes_json, '$.smoke_id') = ?
4747
+ order by timestamp, id
4748
+ `).all(options.taskId, options.smokeId);
4749
+ return rows.map((row) => JSON.parse(row.attributes_json));
4750
+ }
4751
+ function appSmokeBoundSessionsSync(database, taskId) {
4752
+ const row = database.prepare(`
4753
+ select
4754
+ ms.id as manager_session_id,
4755
+ ms.name as manager_session_name,
4756
+ ms.codex_app_thread_id as manager_thread_id,
4757
+ ms.codex_app_thread_title as manager_thread_title,
4758
+ ms.last_heartbeat_at as manager_last_heartbeat_at,
4759
+ ws.id as worker_session_id,
4760
+ ws.name as worker_session_name,
4761
+ ws.codex_app_thread_id as worker_thread_id,
4762
+ ws.codex_app_thread_title as worker_thread_title,
4763
+ ws.last_heartbeat_at as worker_last_heartbeat_at
4764
+ from bindings b
4765
+ join sessions ms on ms.id = b.manager_session_id
4766
+ join sessions ws on ws.id = b.worker_session_id
4767
+ where b.task_id = ?
4768
+ and b.state in ('active', 'ending')
4769
+ order by b.created_at desc
4770
+ limit 1
4771
+ `).get(taskId);
4772
+ if (!row) {
4773
+ throw new Error("No active app smoke binding for task.");
4774
+ }
4775
+ return {
4776
+ manager: {
4777
+ last_heartbeat_at: row.manager_last_heartbeat_at,
4778
+ session_id: row.manager_session_id,
4779
+ session_name: row.manager_session_name,
4780
+ thread_id: row.manager_thread_id,
4781
+ thread_title: row.manager_thread_title,
4782
+ },
4783
+ worker: {
4784
+ last_heartbeat_at: row.worker_last_heartbeat_at,
4785
+ session_id: row.worker_session_id,
4786
+ session_name: row.worker_session_name,
4787
+ thread_id: row.worker_thread_id,
4788
+ thread_title: row.worker_thread_title,
4789
+ },
4790
+ };
4791
+ }
4792
+ function validateAppSmokeRecordThread(database, options) {
4793
+ const bound = appSmokeBoundSessionsSync(database, options.taskId);
4794
+ const expected = options.role === "manager" ? bound.manager.thread_id : bound.worker.thread_id;
4795
+ if (options.status === "sent" && !options.threadId) {
4796
+ throw new Error("app-smoke record --status sent requires --thread-id.");
4797
+ }
4798
+ if (options.threadId && expected !== options.threadId) {
4799
+ throw new Error(`Thread id mismatch for ${options.role}; active binding for ${options.taskName} targets ${expected ?? "(missing)"}.`);
4800
+ }
4801
+ }
4802
+ function parseAppSmokeMode(value) {
4803
+ return value === "advisory" || value === "skip" ? value : "required";
4804
+ }
4805
+ function parseAppSmokeScope(value) {
4806
+ return value === "worker-set" ? "worker-set" : "pair";
4807
+ }
4808
+ function parseAppSmokeRole(value) {
4809
+ if (value === "manager" || value === "worker") {
4810
+ return value;
4811
+ }
4812
+ return new Error("app-smoke record requires --role manager|worker");
4813
+ }
4814
+ function parseAppSmokeRecordStatus(value) {
4815
+ if (value === "accepted" || value === "blocked" || value === "received" || value === "sent" || value === "skipped") {
4816
+ return value;
4817
+ }
4818
+ return new Error("app-smoke record requires --status sent|skipped|received|accepted|blocked");
4819
+ }
4820
+ function secondsBetweenIso(left, right) {
4821
+ return Math.max(0, (Date.parse(right) - Date.parse(left)) / 1000);
4822
+ }
4823
+ function renderAppSmokeStatusText(status) {
4824
+ const smoke = isPlainRecord(status.smoke) ? status.smoke : {};
4825
+ const blockers = Array.isArray(status.blockers) ? status.blockers.map(String) : [];
4826
+ const lines = [
4827
+ `App smoke ${String(smoke.id ?? "(preflight)")}: ${status.ok ? "ok" : "blocked"}`,
4828
+ `Real work allowed: ${String(status.real_work_allowed ?? false)}`,
4829
+ ];
4830
+ for (const blocker of blockers) {
4831
+ lines.push(`Blocker: ${blocker}`);
4832
+ }
4833
+ return `${lines.join("\n")}\n`;
4834
+ }
4361
4835
  function parseAppWakeupDeliveryStatus(value) {
4362
4836
  if (value === "sent" || value === "skipped" || value === "blocked") {
4363
4837
  return value;
@@ -6738,6 +7212,7 @@ function runInstallSkillsCommand(parsed, options) {
6738
7212
  const AGENT_CONVEYOR_PLUGIN_NAME = "agent-conveyor";
6739
7213
  const AGENT_CONVEYOR_PLUGIN_SKILLS = [
6740
7214
  "conveyor-app-wake-relay",
7215
+ "conveyor-smoke-app-connections",
6741
7216
  "conveyor-create-pair",
6742
7217
  "conveyor-create-worker-set",
6743
7218
  "conveyor-check-status",
@@ -15878,6 +16353,7 @@ function isDefaultRuntimeCommand(command) {
15878
16353
  || command === "app-worker-rotation-plan"
15879
16354
  || command === "app-worker-rotation-record"
15880
16355
  || command === "app-autopilot"
16356
+ || command === "app-smoke"
15881
16357
  || command === "loop-templates"
15882
16358
  || command === "loop-triggers"
15883
16359
  || command === "ralph-loop-presets"