agent-conveyor 0.1.22 → 0.1.24
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 +35 -5
- package/dist/cli/typescript-runtime.js +483 -7
- package/dist/cli/typescript-runtime.js.map +1 -1
- package/package.json +1 -1
- package/plugin/agent-conveyor/plugin.json +2 -1
- package/plugin/agent-conveyor/skills/conveyor-create-pair/SKILL.md +22 -1
- package/plugin/agent-conveyor/skills/conveyor-create-worker-set/SKILL.md +20 -2
- package/plugin/agent-conveyor/skills/conveyor-smoke-app-connections/SKILL.md +260 -0
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-
|
|
172
|
-
`conveyor-create-
|
|
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,21 @@ 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.
|
|
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. After
|
|
198
|
+
required smoke passes, pair and worker-set skills start `app-autopilot` before
|
|
199
|
+
real work so the just-proved sessions keep polling. A setup is autonomous only
|
|
200
|
+
when the emitted heartbeat automation specs have been applied, or explicitly
|
|
201
|
+
deferred as `manual-poll only`; smoke-passed by itself only proves connection
|
|
202
|
+
plumbing at that moment.
|
|
203
|
+
When the manager is itself running in the Codex app and
|
|
190
204
|
thread tools are available, the skill should first call `create_thread` for a
|
|
191
205
|
fresh same-project worker, name it with `set_thread_title`, pass the returned
|
|
192
206
|
thread identity through `--worker-codex-app-thread-id` and
|
|
@@ -539,6 +553,21 @@ stay out of receipts.
|
|
|
539
553
|
heartbeats since the last command or inbox-consumption receipt, it recommends
|
|
540
554
|
`stop_autopilot` so operators can quiesce blocked/no-progress loops instead
|
|
541
555
|
of repeating idle pulses.
|
|
556
|
+
- `app-smoke preflight|start|record|status TASK [--mode required|advisory|skip]
|
|
557
|
+
[--scope pair|worker-set] [--smoke-id ID] [--nonce NONCE]
|
|
558
|
+
[--role manager|worker] [--status sent|skipped|received|accepted|blocked]
|
|
559
|
+
[--thread-id ID] [--notification-id N] [--worker-count N] [--from-stdin]
|
|
560
|
+
[--json]` —
|
|
561
|
+
Record and evaluate the durable side of Codex app connection smoke. Required
|
|
562
|
+
mode blocks real work until the active binding has bound app thread ids,
|
|
563
|
+
nonce-matching send receipts, fresh app heartbeats after smoke start, worker
|
|
564
|
+
`received`, and manager/worker `accepted` receipts. Advisory mode reports the
|
|
565
|
+
same blockers without blocking, and skip mode records an explicit bypass. The
|
|
566
|
+
CLI does not call Codex app thread tools; use the
|
|
567
|
+
`conveyor-smoke-app-connections` plugin skill from a Codex app operator
|
|
568
|
+
session for live `send_message_to_thread` delivery. A smoke-passed operator
|
|
569
|
+
flow should immediately run `app-autopilot start` and handle the emitted
|
|
570
|
+
automation specs before sending real task prompts.
|
|
542
571
|
- `app-worker-rotation-plan TASK --old-worker-thread-id ID [--require-handoff]
|
|
543
572
|
[--reason TEXT] [--json]` — Prepare a Codex app fresh-worker rotation. The
|
|
544
573
|
CLI verifies that `ID` exactly matches the active bound worker session before
|
|
@@ -823,7 +852,8 @@ stay out of receipts.
|
|
|
823
852
|
either received, accepted, or blocked on it. `received` requires delivery;
|
|
824
853
|
`accepted` and `blocked` require consumption. Use this after visible Codex app
|
|
825
854
|
sessions print `CONVEYOR RECEIVED`; do not use direct app-thread text as the
|
|
826
|
-
durable receipt.
|
|
855
|
+
durable receipt. `--from-stdin` expects a JSON object, for example
|
|
856
|
+
`printf '%s\n' '{"summary":"accepted","evidence":["consumed notification"],"blockers":[]}' | conveyor inbox-ack ... --from-stdin`.
|
|
827
857
|
|
|
828
858
|
### Actuation
|
|
829
859
|
|
|
@@ -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"
|