agent-conveyor 0.1.24 → 0.1.26

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
@@ -122,7 +122,10 @@ PATH="$tmp_prefix/bin:$PATH" conveyor --help
122
122
  PATH="$tmp_prefix/bin:$PATH" workerctl --help
123
123
  ```
124
124
 
125
- `conveyor doctor` reports local dependency health (tmux, codex, etc.).
125
+ `conveyor doctor --json` reports local dependency health (tmux, codex, etc.),
126
+ package/bin resolution, Codex home, installed operator plugin version, installed
127
+ operator skills, and an `operator_ready` summary for Codex app manager/worker
128
+ setup.
126
129
  `conveyor db-doctor` initializes and checks the SQLite control-plane
127
130
  database.
128
131
  On Node versions where `node:sqlite` is still marked experimental, SQLite
@@ -151,6 +154,13 @@ static landing page. From the repo, `npm run docs:landing` serves it at
151
154
  `http://127.0.0.1:8765/`.
152
155
  The GitHub Pages version lives at
153
156
  [`neonwatty.github.io/agent-conveyor`](https://neonwatty.github.io/agent-conveyor/).
157
+ Pages deploys from the protected `landing-page` branch through the
158
+ `Pages` GitHub Actions workflow; propose public landing-page edits against
159
+ that branch rather than relying on ordinary `main` package PRs to publish the
160
+ site.
161
+ Pull requests into `landing-page` run the `Landing Page PR` workflow, which
162
+ checks linting, unused exports/files, tests, build output, the landing-page
163
+ screenshot gate, and a diff-scoped max-lines guard for changed text files.
154
164
  Use `node scripts/check-landing-page.mjs` for a docs-only desktop/mobile
155
165
  screenshot gate; this does not run the full package release smoke.
156
166
 
@@ -163,7 +173,7 @@ and inspect the plugin:
163
173
  ```bash
164
174
  npm install -g agent-conveyor
165
175
  conveyor install-plugin
166
- conveyor plugin-status
176
+ conveyor doctor --json
167
177
  ```
168
178
 
169
179
  The per-project default ledger for operator sessions is
@@ -189,15 +199,19 @@ the no-tmux binding with `create-disposable-binding`, point the worker at
189
199
  that the loop is ready. Pair and worker-set skills run
190
200
  `conveyor-smoke-app-connections` before real task work by default. Required
191
201
  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
202
+ nonce-scoped `app-smoke` session and blocks real work until the worker has
203
+ accepted smoke and delivered the manager report, then the manager validates
204
+ that report. Both roles need visible app-thread send receipts, fresh app
205
+ heartbeats, durable received/accepted acknowledgements, and an
206
+ `app-smoke status` result with `real_work_allowed=true`. The plain CLI records
207
+ and evaluates receipts; the Codex app skill/operator layer must perform live
208
+ `send_message_to_thread` delivery and record `sent`, `blocked`, or
209
+ skipped/advisory evidence. If a smoke role blocks and later succeeds, the later
210
+ accepted receipt for the same smoke id/nonce becomes the current terminal role
211
+ state. After required smoke passes, pair and worker-set skills start
212
+ `app-autopilot` before real work so the just-proved sessions keep polling. A
213
+ setup is autonomous only when the emitted heartbeat automation specs have been
214
+ applied and recorded with `app-autopilot record-automation`, or explicitly
201
215
  deferred as `manual-poll only`; smoke-passed by itself only proves connection
202
216
  plumbing at that moment.
203
217
  When the manager is itself running in the Codex app and
@@ -538,16 +552,19 @@ stay out of receipts.
538
552
  a matching `ready_to_send` action with `send_ready=true` and the same thread
539
553
  id; healthy roles must be recorded as `skipped`; missing-thread blockers and
540
554
  failed app-thread sends must be recorded as `blocked` with a reason.
541
- - `app-autopilot start|stop|status TASK [--dispatcher-id ID]
555
+ - `app-autopilot start|stop|status|record-automation TASK [--dispatcher-id ID]
542
556
  [--interval SECONDS] [--watch-iterations N] [--stale-after N]
543
- [--quiet-after N] [--json]` —
557
+ [--quiet-after N] [--role manager|worker --automation-id ID] [--json]` —
544
558
  Manage the pair-level app-native heartbeat policy for the active
545
559
  manager/worker binding. `start` and `stop` write telemetry receipts and emit
546
560
  the exact manager/worker Codex app heartbeat automation specs plus the
547
561
  bounded Dispatch watch command. A plain shell CLI cannot call Codex app
548
562
  thread tools, so create/pause those heartbeat automations from a Codex app
549
- operator session using the emitted specs; Conveyor remains the durable source
550
- of truth through Dispatch, inboxes, wake receipts, and app heartbeat status.
563
+ operator session using the emitted specs, then run `record-automation` once
564
+ per created role automation. Conveyor remains the durable source of truth
565
+ through Dispatch, inboxes, automation-applied receipts, wake receipts, and app
566
+ heartbeat status. `status` reports `plan.readiness`; do not call a loop
567
+ autonomous unless `plan.readiness.autonomous_ready=true`.
551
568
  `status` also reports `plan.quiescence`: when the loop is healthy, has no
552
569
  `next_actions`, and both roles have produced `--quiet-after` paired
553
570
  heartbeats since the last command or inbox-consumption receipt, it recommends
@@ -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,
@@ -1128,7 +1130,7 @@ function parseRuntimeArgs(args, env) {
1128
1130
  index += 1;
1129
1131
  }
1130
1132
  else if (arg === "--codex-home") {
1131
- if (command !== "install-skills" && command !== "install-plugin" && command !== "plugin-status" && command !== "plugin-path") {
1133
+ if (command !== "install-skills" && command !== "install-plugin" && command !== "plugin-status" && command !== "plugin-path" && command !== "doctor") {
1132
1134
  return { command, enabled, error: "Unsupported TypeScript runtime option: --codex-home", explicit, flags, task };
1133
1135
  }
1134
1136
  const value = valueAfter(queue, index, arg);
@@ -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
  }
@@ -11892,9 +11952,20 @@ function runDoctorCommand(parsed, options) {
11892
11952
  const root = stateRoot({ cwd: targetCwd, env: options.env });
11893
11953
  const tmuxPath = commandPath("tmux", options);
11894
11954
  const codexPath = options.codexCommandResolver?.("codex") ?? commandPath("codex", options);
11955
+ const conveyorPath = commandPath("conveyor", options);
11956
+ const workerctlPath = commandPath("workerctl", options);
11957
+ const packageRoot = packageRootFromRuntimeModule();
11958
+ const packageVersion = packageVersionFromRoot(packageRoot);
11959
+ const plugin = agentConveyorPluginStatus(parsed, options);
11960
+ const pluginSkillsInstalled = plugin.skills.every((skill) => skill.installed);
11895
11961
  const checks = [
11896
11962
  { name: "tmux", ok: Boolean(tmuxPath), path: tmuxPath },
11897
11963
  { name: "codex", ok: Boolean(codexPath), path: codexPath },
11964
+ { name: "conveyor_on_path", ok: Boolean(conveyorPath), path: conveyorPath },
11965
+ { name: "workerctl_on_path", ok: Boolean(workerctlPath), path: workerctlPath },
11966
+ { name: "plugin_installed", ok: plugin.installed, installed_version: plugin.installed_version },
11967
+ { name: "plugin_version_matches", ok: plugin.version_matches, package_version: packageVersion, plugin_version: plugin.plugin_version },
11968
+ { name: "plugin_skills_installed", ok: pluginSkillsInstalled, missing: plugin.skills.filter((skill) => !skill.installed).map((skill) => skill.name) },
11898
11969
  ];
11899
11970
  if (tmuxPath) {
11900
11971
  const proc = runProcess(["tmux", "-V"], options);
@@ -11906,8 +11977,44 @@ function runDoctorCommand(parsed, options) {
11906
11977
  }
11907
11978
  checks.push({ name: "target_cwd_exists", ok: pathIsDirectory(targetCwd), path: targetCwd });
11908
11979
  checks.push({ name: "state_root_exists", ok: existsSync(root), path: root });
11909
- const ok = checks.every((check) => check.name === "state_root_exists" || check.ok === true);
11910
- return { ...jsonResult({ checks, ok, project_root: packageRootFromRuntimeModule(), workers: doctorWorkerSummaries(root) }), exitCode: ok ? 0 : 1 };
11980
+ const commandReady = Boolean(conveyorPath) && Boolean(workerctlPath) && Boolean(codexPath) && Boolean(tmuxPath);
11981
+ const operatorReady = commandReady && plugin.installed && plugin.version_matches && pluginSkillsInstalled;
11982
+ const ok = checks.every((check) => check.name === "state_root_exists"
11983
+ || check.name === "conveyor_on_path"
11984
+ || check.name === "workerctl_on_path"
11985
+ || check.name === "plugin_installed"
11986
+ || check.name === "plugin_version_matches"
11987
+ || check.name === "plugin_skills_installed"
11988
+ || check.ok === true);
11989
+ return {
11990
+ ...jsonResult({
11991
+ checks,
11992
+ codex_home: plugin.paths.codex_home,
11993
+ commands: {
11994
+ codex: { ok: Boolean(codexPath), path: codexPath },
11995
+ conveyor: { ok: Boolean(conveyorPath), path: conveyorPath },
11996
+ tmux: { ok: Boolean(tmuxPath), path: tmuxPath },
11997
+ workerctl: { ok: Boolean(workerctlPath), path: workerctlPath },
11998
+ },
11999
+ ok,
12000
+ operator_ready: operatorReady,
12001
+ package: {
12002
+ name: "agent-conveyor",
12003
+ root: packageRoot,
12004
+ version: packageVersion,
12005
+ },
12006
+ platform: process.platform,
12007
+ plugin,
12008
+ project_root: packageRoot,
12009
+ runtime: {
12010
+ node: process.version,
12011
+ state_root: root,
12012
+ target_cwd: targetCwd,
12013
+ },
12014
+ workers: doctorWorkerSummaries(root),
12015
+ }),
12016
+ exitCode: ok ? 0 : 1,
12017
+ };
11911
12018
  }
11912
12019
  function runDoctorSelfCommand(parsed, options) {
11913
12020
  if (parsed.task !== null) {
@@ -18966,6 +19073,7 @@ function telemetryEventsForRunSync(database, options) {
18966
19073
  }
18967
19074
  function appTaskDispatchSummarySync(database, options) {
18968
19075
  const taskDispatchEventTypes = [
19076
+ "app_autopilot_automation_applied",
18969
19077
  "app_autopilot_started",
18970
19078
  "app_autopilot_stopped",
18971
19079
  "app_heartbeat",