codex-toys 0.140.11 → 0.140.13

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.
Files changed (55) hide show
  1. package/README.md +35 -15
  2. package/dist/cli/actions.d.ts +1 -1
  3. package/dist/cli/actions.d.ts.map +1 -1
  4. package/dist/cli/actions.js +1 -0
  5. package/dist/cli/actions.js.map +1 -1
  6. package/dist/cli/args.d.ts +25 -20
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +67 -33
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/cli/help.d.ts.map +1 -1
  11. package/dist/cli/help.js +18 -16
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/index.js +68 -57
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/toybox.js +3 -3
  16. package/dist/internal/feed/index.d.ts +26 -1
  17. package/dist/internal/feed/index.d.ts.map +1 -1
  18. package/dist/internal/feed/index.js +102 -1
  19. package/dist/internal/feed/index.js.map +1 -1
  20. package/dist/internal/package.json +5 -0
  21. package/dist/internal/workbench/{deferred-run-methods.d.ts → dispatch-run-methods.d.ts} +12 -12
  22. package/dist/internal/workbench/{deferred-run-methods.d.ts.map → dispatch-run-methods.d.ts.map} +1 -1
  23. package/dist/internal/workbench/{deferred-run-methods.js → dispatch-run-methods.js} +61 -61
  24. package/dist/internal/workbench/{deferred-run-methods.js.map → dispatch-run-methods.js.map} +1 -1
  25. package/dist/internal/workbench/fetch.js +1 -1
  26. package/dist/internal/workbench/fetch.js.map +1 -1
  27. package/dist/internal/workbench/index.d.ts +1 -1
  28. package/dist/internal/workbench/index.js +1 -1
  29. package/dist/internal/workbench/workbench-overview.d.ts +9 -9
  30. package/dist/internal/workbench/workbench-overview.js +12 -12
  31. package/dist/internal/workbench/workbench-runtime.d.ts +75 -120
  32. package/dist/internal/workbench/workbench-runtime.d.ts.map +1 -1
  33. package/dist/internal/workbench/workbench-runtime.js +293 -621
  34. package/dist/internal/workbench/workbench-runtime.js.map +1 -1
  35. package/dist/internal/workbench/workflow.d.ts.map +1 -1
  36. package/dist/internal/workbench/workflow.js +106 -11
  37. package/dist/internal/workbench/workflow.js.map +1 -1
  38. package/docs/pages/{reference → components}/cli.md +13 -14
  39. package/docs/pages/{primitives → components}/toybox.md +4 -4
  40. package/docs/pages/guides/capability-kit-setup.md +135 -0
  41. package/docs/pages/guides/dashboard-over-toybox.md +145 -0
  42. package/docs/pages/guides/delegated-repo-work.md +119 -0
  43. package/docs/pages/guides/feed-to-workflow.md +138 -0
  44. package/docs/pages/guides/local-scheduled-workbench.md +149 -0
  45. package/docs/pages/guides/remote-codex-workbench.md +147 -0
  46. package/docs/pages/guides/repository-autonomy.md +163 -0
  47. package/docs/pages/index.md +35 -11
  48. package/docs/pages/primitives/{deferred-queues.md → dispatch-queues.md} +21 -21
  49. package/docs/pages/primitives/feed.md +18 -9
  50. package/docs/pages/primitives/workbench.md +26 -21
  51. package/docs/pages/primitives/workflow.md +6 -5
  52. package/docs/pages/reference/packages.md +14 -2
  53. package/package.json +1 -1
  54. /package/docs/pages/{primitives → components}/kits.md +0 -0
  55. /package/docs/pages/{primitives → components}/proxy.md +0 -0
@@ -6,6 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import { parse as parseToml } from "smol-toml";
7
7
  import { createWorkflowHost, resolveWorkflowTarget, runWorkflowScript, startWorkflowTurnWithRequest, waitWorkflowTurnWithRequest, } from "./workflow.js";
8
8
  import { parseJsonText } from "../bridge/json.js";
9
+ export const defaultActionsRunnerImage = "ghcr.io/peezy-tech/codex-toys-actions:latest";
9
10
  export async function discoverWorkbenchRoot(start = process.cwd()) {
10
11
  let current = path.resolve(start);
11
12
  let firstDotCodexRoot;
@@ -86,7 +87,9 @@ export async function loadWorkbenchConfig(context) {
86
87
  const workbench = isRecord(parsed.workbench) ? parsed.workbench : undefined;
87
88
  const surfacesInput = Array.isArray(workbench?.surfaces) ? workbench.surfaces : [];
88
89
  const tasksInput = Array.isArray(workbench?.tasks) ? workbench.tasks : [];
89
- const reactiveInput = Array.isArray(workbench?.reactive) ? workbench.reactive : [];
90
+ if (workbench?.reactive !== undefined) {
91
+ throw new Error("workbench.reactive has been removed; run explicit tasks or dispatch queues from systemd or Actions schedules");
92
+ }
90
93
  const tasks = tasksInput.map(parseTask);
91
94
  const ids = new Set();
92
95
  for (const task of tasks) {
@@ -99,11 +102,10 @@ export async function loadWorkbenchConfig(context) {
99
102
  name: stringValue(workbench?.name, path.basename(context.repoRoot)),
100
103
  surfaces: surfacesInput.map(parseSurface),
101
104
  tasks,
102
- reactive: reactiveInput.map(parseReactiveRule),
103
105
  path: context.configPath,
104
106
  };
105
107
  }
106
- export async function collectWorkbenchDoctorInfo(context, options = {}) {
108
+ export async function collectWorkbenchDoctorInfo(context) {
107
109
  let config;
108
110
  let configExists = true;
109
111
  try {
@@ -122,16 +124,12 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
122
124
  }
123
125
  const runs = await readRuns(context);
124
126
  const latestRun = runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
125
- const deferredRuns = await listDeferredRunIntents(context);
127
+ const dispatchRuns = await listDispatchRunIntents(context);
126
128
  const now = new Date();
127
- const latestDeferredRun = deferredRuns
129
+ const latestDispatchRun = dispatchRuns
128
130
  .toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
129
- const deferredDueFlags = await Promise.all(deferredRuns.map(async (intent) => await isDeferredIntentDue(context, intent, now)));
131
+ const dispatchDueFlags = await Promise.all(dispatchRuns.map(async (intent) => await isDispatchIntentDue(context, intent, now)));
130
132
  const failingCount = countFailingTasks(config?.tasks ?? [], runs);
131
- const includeRunner = options.includeRunner === true || options.runnerProbe !== undefined;
132
- const runner = includeRunner
133
- ? await collectWorkbenchRunnerInfo(context, config?.tasks ?? [], deferredRuns, options.runnerProbe ?? runSystemctlUser)
134
- : undefined;
135
133
  return {
136
134
  mode: context.mode,
137
135
  requestedMode: context.requestedMode,
@@ -148,15 +146,13 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
148
146
  globalMemorySummaryExists: await exists(path.join(context.globalCodexHome, "memories", "memory_summary.md")),
149
147
  workbenchMemorySummaryExists: await exists(path.join(context.workbenchCodexHome, "memories", "memory_summary.md")),
150
148
  taskCount: config?.tasks.length ?? 0,
151
- dueCount: dueTasks(config?.tasks ?? [], runs, new Date()).length,
152
149
  failingCount,
153
- deferredCount: deferredRuns.length,
154
- deferredDueCount: deferredDueFlags.filter(Boolean).length,
155
- deferredRunningCount: deferredRuns.filter((intent) => intent.status === "running").length,
156
- deferredFailedCount: deferredRuns.filter((intent) => intent.status === "failed").length,
150
+ dispatchCount: dispatchRuns.length,
151
+ dispatchDueCount: dispatchDueFlags.filter(Boolean).length,
152
+ dispatchRunningCount: dispatchRuns.filter((intent) => intent.status === "running").length,
153
+ dispatchFailedCount: dispatchRuns.filter((intent) => intent.status === "failed").length,
157
154
  latestRun,
158
- latestDeferredRun,
159
- runner,
155
+ latestDispatchRun,
160
156
  surfaces: config?.surfaces ?? [],
161
157
  errors: workbenchDoctorErrors(context),
162
158
  };
@@ -173,23 +169,19 @@ export function formatWorkbenchDoctorInfo(info) {
173
169
  ["actions state", info.actionsStateRoot],
174
170
  ["global memories", `${info.globalMemoryRoot}${info.globalMemorySummaryExists ? " (summary)" : ""}`],
175
171
  ["workbench memories", `${info.workbenchMemoryRoot}${info.workbenchMemorySummaryExists ? " (summary)" : ""}`],
176
- ["tasks", `${info.taskCount} configured, ${info.dueCount} due, ${info.failingCount} failing`],
172
+ ["tasks", `${info.taskCount} configured, ${info.failingCount} failing`],
177
173
  ["latest run", info.latestRun ? `${info.latestRun.status} ${info.latestRun.taskId} ${info.latestRun.finishedAt}` : "none"],
178
174
  [
179
- "deferred runs",
180
- `${info.deferredCount} total, ${info.deferredDueCount} due, ${info.deferredRunningCount} running, ${info.deferredFailedCount} failed`,
175
+ "dispatch runs",
176
+ `${info.dispatchCount} total, ${info.dispatchDueCount} due, ${info.dispatchRunningCount} running, ${info.dispatchFailedCount} failed`,
181
177
  ],
182
178
  [
183
- "latest deferred",
184
- info.latestDeferredRun
185
- ? `${info.latestDeferredRun.status} ${info.latestDeferredRun.id} ${info.latestDeferredRun.updatedAt}`
179
+ "latest dispatch",
180
+ info.latestDispatchRun
181
+ ? `${info.latestDispatchRun.status} ${info.latestDispatchRun.id} ${info.latestDispatchRun.updatedAt}`
186
182
  : "none",
187
183
  ],
188
- ["runner", formatWorkbenchRunnerInfo(info.runner)],
189
184
  ];
190
- if (info.runner?.warning) {
191
- rows.push(["runner warning", info.runner.warning]);
192
- }
193
185
  for (const error of info.errors) {
194
186
  rows.push(["error", error]);
195
187
  }
@@ -198,47 +190,21 @@ export function formatWorkbenchDoctorInfo(info) {
198
190
  export async function scaffoldActionsWorkbench(options = {}) {
199
191
  const workbenchRoot = path.resolve(options.workbenchRoot ?? await discoverWorkbenchRoot());
200
192
  const files = [];
193
+ const runnerImage = options.runnerImage === undefined ? defaultActionsRunnerImage : options.runnerImage;
201
194
  const write = async (relativePath, content) => {
202
195
  files.push(await writeScaffoldFile(workbenchRoot, relativePath, content, options.overwrite === true));
203
196
  };
204
197
  await write(".codex/workbench.toml", workbenchTomlTemplate(workbenchRoot));
205
198
  await write(".codex/config.toml", codexConfigTemplate());
206
199
  if (options.forgejo) {
207
- await write(".forgejo/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("forgejo"));
200
+ await write(".forgejo/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("forgejo", runnerImage));
208
201
  }
209
202
  if (options.github || !options.forgejo) {
210
- await write(".github/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("github"));
203
+ await write(".github/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("github", runnerImage));
211
204
  }
212
205
  files.push(await appendGitignoreEntries(workbenchRoot, actionsGitignoreEntries(), retiredActionsGitignoreEntries()));
213
206
  return { workbenchRoot, files };
214
207
  }
215
- export async function tickWorkbench(context, options) {
216
- await ensureStateDirs(context);
217
- const config = await loadWorkbenchConfig(context);
218
- const previousRuns = await readRuns(context);
219
- const previousIntents = await listDeferredRunIntents(context);
220
- const now = new Date();
221
- const due = dueTasks(config.tasks, previousRuns, now, previousIntents);
222
- const runs = [];
223
- for (const task of due) {
224
- await createScheduledWorkbenchTaskIntent(context, task, now);
225
- }
226
- const executions = await runDueDeferredRuns(context, options);
227
- for (const execution of executions.executions) {
228
- const workbenchRun = record(execution.output).workbenchRun;
229
- if (isWorkbenchRunRecord(workbenchRun)) {
230
- runs.push(workbenchRun);
231
- }
232
- }
233
- const allRuns = [...previousRuns, ...runs];
234
- for (const rule of config.reactive.filter((item) => item.enabled)) {
235
- const targets = config.tasks.filter((task) => rule.task === "*" ? true : task.id === rule.task);
236
- if (targets.some((task) => consecutiveFailures(task.id, allRuns) >= rule.consecutiveFailuresGte)) {
237
- runs.push(await runReactiveRule(context, rule));
238
- }
239
- }
240
- return { mode: context.mode, due: due.map((task) => task.id), runs };
241
- }
242
208
  export async function runWorkbenchTaskById(context, taskId, options) {
243
209
  await ensureStateDirs(context);
244
210
  const config = await loadWorkbenchConfig(context);
@@ -269,15 +235,18 @@ export async function commitActionsWorkbenchState(context, options = {}) {
269
235
  await runGit(context.repoRoot, ["add", "-A", "-f", "--", sessionsPath]);
270
236
  }
271
237
  const staged = await runGit(context.repoRoot, ["diff", "--cached", "--name-only", "--", ...relativePaths]);
272
- if (!staged.stdout.trim()) {
238
+ const stagedPaths = staged.stdout.trim().split(/\r?\n/).filter(Boolean);
239
+ if (stagedPaths.length === 0) {
273
240
  return { attempted: true, committed: false, paths: context.actionsCommitPaths };
274
241
  }
242
+ await runGit(context.repoRoot, ["config", "user.name", "codex-toys-actions"]);
243
+ await runGit(context.repoRoot, ["config", "user.email", "codex-toys-actions@users.noreply.github.com"]);
275
244
  const commit = await runGit(context.repoRoot, [
276
245
  "commit",
277
246
  "-m",
278
247
  options.message ?? "Update Codex workbench state",
279
248
  "--",
280
- ...relativePaths,
249
+ ...stagedPaths,
281
250
  ]);
282
251
  return {
283
252
  attempted: true,
@@ -286,13 +255,13 @@ export async function commitActionsWorkbenchState(context, options = {}) {
286
255
  output: commit.stdout || commit.stderr,
287
256
  };
288
257
  }
289
- export async function createDeferredRunIntent(context, params) {
290
- await ensureDeferredRunDirs(context);
291
- const input = parseDeferredRunCreateParams(params);
258
+ export async function createDispatchRunIntent(context, params) {
259
+ await ensureDispatchRunDirs(context);
260
+ const input = parseDispatchRunCreateParams(params);
292
261
  const now = new Date().toISOString();
293
262
  const runAt = input.runAt ?? now;
294
263
  const intent = compactUndefined({
295
- id: input.id ?? deferredRunId(now),
264
+ id: input.id ?? dispatchRunId(now),
296
265
  status: "pending",
297
266
  mode: context.mode,
298
267
  runAt,
@@ -305,11 +274,11 @@ export async function createDeferredRunIntent(context, params) {
305
274
  dependsOn: input.dependsOn,
306
275
  attemptIds: [],
307
276
  });
308
- await writeNewJsonFile(deferredIntentPath(context, intent.id), intent);
277
+ await writeNewJsonFile(dispatchIntentPath(context, intent.id), intent);
309
278
  return intent;
310
279
  }
311
- export async function listDeferredRunIntents(context, options = {}) {
312
- const dir = deferredIntentDir(context);
280
+ export async function listDispatchRunIntents(context, options = {}) {
281
+ const dir = dispatchIntentDir(context);
313
282
  try {
314
283
  const entries = await readdir(dir);
315
284
  const intents = [];
@@ -318,7 +287,7 @@ export async function listDeferredRunIntents(context, options = {}) {
318
287
  continue;
319
288
  }
320
289
  try {
321
- const intent = normalizeDeferredRunIntent(parseJsonText(await readFile(path.join(dir, entry), "utf8"), path.join(dir, entry)));
290
+ const intent = normalizeDispatchRunIntent(parseJsonText(await readFile(path.join(dir, entry), "utf8"), path.join(dir, entry)));
322
291
  if (!options.status || intent.status === options.status) {
323
292
  intents.push(intent);
324
293
  }
@@ -332,11 +301,11 @@ export async function listDeferredRunIntents(context, options = {}) {
332
301
  return [];
333
302
  }
334
303
  }
335
- export async function readDeferredRun(context, intentId, options = {}) {
336
- const intent = await readDeferredRunIntent(context, intentId);
337
- const attempts = await readDeferredRunAttempts(context, intent.attemptIds);
304
+ export async function readDispatchRun(context, intentId, options = {}) {
305
+ const intent = await readDispatchRunIntent(context, intentId);
306
+ const attempts = await readDispatchRunAttempts(context, intent.attemptIds);
338
307
  const outputs = options.includeOutput
339
- ? await readDeferredRunAttemptOutputs(attempts)
308
+ ? await readDispatchRunAttemptOutputs(attempts)
340
309
  : undefined;
341
310
  return compactUndefined({ intent, attempts, outputs });
342
311
  }
@@ -344,12 +313,12 @@ export async function enqueuePromptQueueIntent(context, params) {
344
313
  const input = parsePromptQueueEnqueueParams(params);
345
314
  const after = input.afterIntentId
346
315
  ? {
347
- kind: "deferred-run",
316
+ kind: "dispatch-run",
348
317
  intentId: input.afterIntentId,
349
318
  status: input.afterStatus,
350
319
  }
351
320
  : undefined;
352
- return await createDeferredRunIntent(context, {
321
+ return await createDispatchRunIntent(context, {
353
322
  id: input.id,
354
323
  runAt: input.runAt,
355
324
  target: compactUndefined({
@@ -373,7 +342,7 @@ export async function enqueuePromptQueueIntent(context, params) {
373
342
  });
374
343
  }
375
344
  export async function listPromptQueueIntents(context, options = {}) {
376
- const intents = await listDeferredRunIntents(context, {
345
+ const intents = await listDispatchRunIntents(context, {
377
346
  status: options.status,
378
347
  });
379
348
  return intents
@@ -393,10 +362,10 @@ function comparePromptQueueIntents(left, right) {
393
362
  left.id.localeCompare(right.id);
394
363
  }
395
364
  function dependsOnIntent(intent, dependencyId) {
396
- return (intent.dependsOn ?? []).some((dependency) => dependency.kind === "deferred-run" && dependency.intentId === dependencyId);
365
+ return (intent.dependsOn ?? []).some((dependency) => dependency.kind === "dispatch-run" && dependency.intentId === dependencyId);
397
366
  }
398
367
  export async function collectPromptQueueRuns(context, options = {}) {
399
- return await collectDeferredRuns(context, {
368
+ return await collectDispatchRuns(context, {
400
369
  cursor: options.cursor,
401
370
  defaultCursor: "prompt-queue",
402
371
  now: options.now,
@@ -404,7 +373,7 @@ export async function collectPromptQueueRuns(context, options = {}) {
404
373
  });
405
374
  }
406
375
  export async function runDuePromptQueueIntents(context, options) {
407
- return await runDueDeferredRuns(context, {
376
+ return await runDueDispatchRuns(context, {
408
377
  ...options,
409
378
  filter: (intent) => isPromptQueueIntent(intent, options.queue),
410
379
  });
@@ -413,12 +382,12 @@ export async function enqueueLocalHandoffIntent(context, params) {
413
382
  const input = parseLocalHandoffEnqueueParams(params);
414
383
  const after = input.afterIntentId
415
384
  ? {
416
- kind: "deferred-run",
385
+ kind: "dispatch-run",
417
386
  intentId: input.afterIntentId,
418
387
  status: input.afterStatus,
419
388
  }
420
389
  : undefined;
421
- return await createDeferredRunIntent(context, {
390
+ return await createDispatchRunIntent(context, {
422
391
  id: input.id,
423
392
  runAt: input.runAt,
424
393
  target: compactUndefined({
@@ -442,7 +411,7 @@ export async function enqueueLocalHandoffIntent(context, params) {
442
411
  });
443
412
  }
444
413
  export async function listLocalHandoffIntents(context, options = {}) {
445
- const intents = await listDeferredRunIntents(context, {
414
+ const intents = await listDispatchRunIntents(context, {
446
415
  status: options.status,
447
416
  });
448
417
  return intents
@@ -450,7 +419,7 @@ export async function listLocalHandoffIntents(context, options = {}) {
450
419
  .slice(0, clampLimit(options.limit, 500));
451
420
  }
452
421
  export async function collectLocalHandoffRuns(context, options = {}) {
453
- return await collectDeferredRuns(context, {
422
+ return await collectDispatchRuns(context, {
454
423
  cursor: options.cursor,
455
424
  defaultCursor: "local-handoff",
456
425
  now: options.now,
@@ -459,7 +428,7 @@ export async function collectLocalHandoffRuns(context, options = {}) {
459
428
  }
460
429
  export async function drainLocalHandoffQueue(context, options) {
461
430
  const action = options.action ?? "run";
462
- const result = await runDueDeferredRuns(context, {
431
+ const result = await runDueDispatchRuns(context, {
463
432
  ...options,
464
433
  includeLocalHandoffs: true,
465
434
  localHandoffMaterialize: action === "materialize"
@@ -478,22 +447,22 @@ export async function drainLocalHandoffQueue(context, options) {
478
447
  executions: result.executions,
479
448
  };
480
449
  }
481
- export async function collectDeferredRuns(context, options = {}) {
482
- await ensureDeferredRunDirs(context);
483
- const cursor = deferredCollectCursorName(options.cursor, options.defaultCursor);
484
- const previousCursor = await readDeferredRunCollectCursor(context, cursor);
450
+ export async function collectDispatchRuns(context, options = {}) {
451
+ await ensureDispatchRunDirs(context);
452
+ const cursor = dispatchCollectCursorName(options.cursor, options.defaultCursor);
453
+ const previousCursor = await readDispatchRunCollectCursor(context, cursor);
485
454
  const collectedAt = (options.now ?? new Date()).toISOString();
486
455
  const filteredIntents = [];
487
- for (const intent of await listDeferredRunIntents(context)) {
456
+ for (const intent of await listDispatchRunIntents(context)) {
488
457
  if (!options.filter || await options.filter(intent)) {
489
458
  filteredIntents.push(intent);
490
459
  }
491
460
  }
492
461
  const terminalIntents = filteredIntents
493
- .filter((intent) => isTerminalDeferredRunStatus(intent.status))
462
+ .filter((intent) => isTerminalDispatchRunStatus(intent.status))
494
463
  .toSorted((left, right) => left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id))
495
- .filter((intent) => isAfterDeferredRunCollectCursor(intent, previousCursor));
496
- const intents = await Promise.all(terminalIntents.map(async (intent) => await readDeferredRun(context, intent.id, { includeOutput: true })));
464
+ .filter((intent) => isAfterDispatchRunCollectCursor(intent, previousCursor));
465
+ const intents = await Promise.all(terminalIntents.map(async (intent) => await readDispatchRun(context, intent.id, { includeOutput: true })));
497
466
  const last = terminalIntents.at(-1);
498
467
  const cursorState = compactUndefined({
499
468
  cursor,
@@ -501,7 +470,7 @@ export async function collectDeferredRuns(context, options = {}) {
501
470
  lastUpdatedAt: last?.updatedAt ?? previousCursor?.lastUpdatedAt,
502
471
  lastIntentId: last?.id ?? previousCursor?.lastIntentId,
503
472
  });
504
- await writeJsonFileAtomic(deferredCollectCursorPath(context, cursor), cursorState);
473
+ await writeJsonFileAtomic(dispatchCollectCursorPath(context, cursor), cursorState);
505
474
  return compactUndefined({
506
475
  mode: context.mode,
507
476
  cursor,
@@ -511,10 +480,10 @@ export async function collectDeferredRuns(context, options = {}) {
511
480
  intents,
512
481
  });
513
482
  }
514
- export async function cancelDeferredRunIntent(context, intentId) {
515
- const intent = await readDeferredRunIntent(context, intentId);
483
+ export async function cancelDispatchRunIntent(context, intentId) {
484
+ const intent = await readDispatchRunIntent(context, intentId);
516
485
  if (intent.status !== "pending") {
517
- throw new Error(`Only pending deferred runs can be canceled: ${intentId}`);
486
+ throw new Error(`Only pending dispatch runs can be canceled: ${intentId}`);
518
487
  }
519
488
  const now = new Date().toISOString();
520
489
  const canceled = {
@@ -523,19 +492,19 @@ export async function cancelDeferredRunIntent(context, intentId) {
523
492
  updatedAt: now,
524
493
  canceledAt: now,
525
494
  };
526
- await writeJsonFileAtomic(deferredIntentPath(context, intentId), canceled);
495
+ await writeJsonFileAtomic(dispatchIntentPath(context, intentId), canceled);
527
496
  return canceled;
528
497
  }
529
- export async function retryDeferredRunIntent(context, intentId, params = {}, options = {}) {
530
- await ensureDeferredRunDirs(context);
531
- const originalIntent = await readDeferredRunIntent(context, intentId);
532
- if (!isTerminalDeferredRunStatus(originalIntent.status)) {
533
- throw new Error(`Only terminal deferred runs can be retried: ${intentId} is ${originalIntent.status}`);
498
+ export async function retryDispatchRunIntent(context, intentId, params = {}, options = {}) {
499
+ await ensureDispatchRunDirs(context);
500
+ const originalIntent = await readDispatchRunIntent(context, intentId);
501
+ if (!isTerminalDispatchRunStatus(originalIntent.status)) {
502
+ throw new Error(`Only terminal dispatch runs can be retried: ${intentId} is ${originalIntent.status}`);
534
503
  }
535
- const input = parseDeferredRunRetryParams(params);
504
+ const input = parseDispatchRunRetryParams(params);
536
505
  const now = (options.now ?? new Date()).toISOString();
537
- const retrySource = retryDeferredRunSource(originalIntent, input.source);
538
- let id = input.id ?? deferredRetryRunId(originalIntent.id, now);
506
+ const retrySource = retryDispatchRunSource(originalIntent, input.source);
507
+ let id = input.id ?? dispatchRetryRunId(originalIntent.id, now);
539
508
  for (let attempt = 0; attempt < 10; attempt += 1) {
540
509
  const intent = compactUndefined({
541
510
  id,
@@ -545,32 +514,32 @@ export async function retryDeferredRunIntent(context, intentId, params = {}, opt
545
514
  target: originalIntent.target,
546
515
  createdAt: now,
547
516
  updatedAt: now,
548
- createdBy: input.createdBy ?? "workbench-deferred-retry",
549
- reason: input.reason ?? `Retry deferred run ${originalIntent.id}`,
517
+ createdBy: input.createdBy ?? "workbench-dispatch-retry",
518
+ reason: input.reason ?? `Retry dispatch run ${originalIntent.id}`,
550
519
  source: retrySource,
551
520
  dependsOn: originalIntent.dependsOn,
552
521
  attemptIds: [],
553
522
  });
554
523
  try {
555
- await writeNewJsonFile(deferredIntentPath(context, intent.id), intent);
524
+ await writeNewJsonFile(dispatchIntentPath(context, intent.id), intent);
556
525
  return { intent, originalIntent };
557
526
  }
558
527
  catch (error) {
559
528
  if (!isAlreadyExistsError(error)) {
560
529
  throw error;
561
530
  }
562
- id = deferredRetryRunId(originalIntent.id, now);
531
+ id = dispatchRetryRunId(originalIntent.id, now);
563
532
  }
564
533
  }
565
- throw new Error(`Could not allocate retry deferred run id for ${intentId}`);
534
+ throw new Error(`Could not allocate retry dispatch run id for ${intentId}`);
566
535
  }
567
- export async function runDueDeferredRuns(context, options) {
536
+ export async function runDueDispatchRuns(context, options) {
568
537
  await ensureStateDirs(context);
569
- await ensureDeferredRunDirs(context);
538
+ await ensureDispatchRunDirs(context);
570
539
  const now = options.now ?? new Date();
571
540
  const due = [];
572
- for (const intent of await listDeferredRunIntents(context)) {
573
- if (await isDeferredIntentDue(context, intent, now) &&
541
+ for (const intent of await listDispatchRunIntents(context)) {
542
+ if (await isDispatchIntentDue(context, intent, now) &&
574
543
  (options.includeLocalHandoffs === true || !isLocalHandoffIntent(intent)) &&
575
544
  (!options.filter || await options.filter(intent))) {
576
545
  due.push(intent);
@@ -581,7 +550,7 @@ export async function runDueDeferredRuns(context, options) {
581
550
  }
582
551
  const executions = [];
583
552
  for (const intent of due) {
584
- const claim = await claimDeferredRunIntent(context, intent, {
553
+ const claim = await claimDispatchRunIntent(context, intent, {
585
554
  now,
586
555
  leaseMs: options.leaseMs ?? 30 * 60 * 1000,
587
556
  });
@@ -589,9 +558,9 @@ export async function runDueDeferredRuns(context, options) {
589
558
  continue;
590
559
  }
591
560
  try {
592
- const outputPath = path.join(deferredOutputDir(context), `${claim.attempt.id}.json`);
593
- await writeJsonFileAtomic(deferredAttemptPath(context, claim.attempt.id), claim.attempt);
594
- const result = await executeDeferredRunTarget(context, claim.intent, {
561
+ const outputPath = path.join(dispatchOutputDir(context), `${claim.attempt.id}.json`);
562
+ await writeJsonFileAtomic(dispatchAttemptPath(context, claim.attempt.id), claim.attempt);
563
+ const result = await executeDispatchRunTarget(context, claim.intent, {
595
564
  callToybox: options.callToybox,
596
565
  workflowCwd: options.workflowCwd,
597
566
  localHandoffMaterialize: options.localHandoffMaterialize,
@@ -614,8 +583,8 @@ export async function runDueDeferredRuns(context, options) {
614
583
  completedAt: result.status === "completed" ? finishedAt : undefined,
615
584
  error: result.error,
616
585
  });
617
- await writeJsonFileAtomic(deferredAttemptPath(context, attempt.id), attempt);
618
- await writeJsonFileAtomic(deferredIntentPath(context, completedIntent.id), completedIntent);
586
+ await writeJsonFileAtomic(dispatchAttemptPath(context, attempt.id), attempt);
587
+ await writeJsonFileAtomic(dispatchIntentPath(context, completedIntent.id), completedIntent);
619
588
  executions.push({
620
589
  intent: completedIntent,
621
590
  attempt,
@@ -623,24 +592,24 @@ export async function runDueDeferredRuns(context, options) {
623
592
  });
624
593
  }
625
594
  finally {
626
- await releaseDeferredRunClaim(context, claim.intent.id);
595
+ await releaseDispatchRunClaim(context, claim.intent.id);
627
596
  }
628
597
  }
629
598
  return { mode: context.mode, executions };
630
599
  }
631
- export async function pruneDeferredRunHistory(context, options) {
600
+ export async function pruneDispatchRunHistory(context, options) {
632
601
  if (!Number.isInteger(options.olderThanDays) || options.olderThanDays <= 0) {
633
602
  throw new Error("olderThanDays must be a positive integer");
634
603
  }
635
604
  const now = options.now ?? new Date();
636
605
  const cutoff = new Date(now.getTime() - options.olderThanDays * 24 * 60 * 60 * 1000).toISOString();
637
- const intents = await listDeferredRunIntents(context);
606
+ const intents = await listDispatchRunIntents(context);
638
607
  const pruned = [];
639
608
  for (const intent of intents) {
640
- if (!isTerminalDeferredRunStatus(intent.status) || intent.updatedAt >= cutoff) {
609
+ if (!isTerminalDispatchRunStatus(intent.status) || intent.updatedAt >= cutoff) {
641
610
  continue;
642
611
  }
643
- const attempts = await readDeferredRunAttempts(context, intent.attemptIds);
612
+ const attempts = await readDispatchRunAttempts(context, intent.attemptIds);
644
613
  const outputPaths = attempts.flatMap((attempt) => attempt.outputPath ? [attempt.outputPath] : []);
645
614
  pruned.push({
646
615
  id: intent.id,
@@ -652,14 +621,14 @@ export async function pruneDeferredRunHistory(context, options) {
652
621
  if (options.dryRun === true) {
653
622
  continue;
654
623
  }
655
- await releaseDeferredRunClaim(context, intent.id);
624
+ await releaseDispatchRunClaim(context, intent.id);
656
625
  for (const outputPath of outputPaths) {
657
626
  await rm(outputPath, { force: true });
658
627
  }
659
628
  for (const attempt of attempts) {
660
- await rm(deferredAttemptPath(context, attempt.id), { force: true });
629
+ await rm(dispatchAttemptPath(context, attempt.id), { force: true });
661
630
  }
662
- await rm(deferredIntentPath(context, intent.id), { force: true });
631
+ await rm(dispatchIntentPath(context, intent.id), { force: true });
663
632
  }
664
633
  return {
665
634
  mode: context.mode,
@@ -673,13 +642,13 @@ export async function pruneDeferredRunHistory(context, options) {
673
642
  async function runWorkbenchTask(context, config, task, options) {
674
643
  const startedAt = new Date().toISOString();
675
644
  const runId = workbenchRunId(task.id, startedAt);
676
- const outputPath = path.join(context.stateRoot, "outputs", `${runId}.json`);
645
+ const outputPath = workbenchTaskOutputPath(context, task, runId);
677
646
  try {
678
647
  let result;
679
648
  if (!task.enabled) {
680
649
  result = { skipped: "disabled" };
681
650
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "skipped", outputPath);
682
- await persistRun(context, run, result);
651
+ await persistRun(context, task, run, result);
683
652
  return run;
684
653
  }
685
654
  if (task.kind === "workflow") {
@@ -692,18 +661,23 @@ async function runWorkbenchTask(context, config, task, options) {
692
661
  result = await runSkill(task, context);
693
662
  }
694
663
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "completed", outputPath);
695
- await persistRun(context, run, result);
664
+ await persistRun(context, task, run, result);
696
665
  return run;
697
666
  }
698
667
  catch (error) {
699
668
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "failed", outputPath, errorMessage(error));
700
- await persistRun(context, run, { error: errorMessage(error) });
669
+ await persistRun(context, task, run, { error: errorMessage(error) });
701
670
  return run;
702
671
  }
703
672
  }
704
673
  function workbenchRunId(taskId, startedAt) {
705
674
  return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${taskId}`;
706
675
  }
676
+ function workbenchTaskOutputPath(context, task, runId) {
677
+ return task.history === "latest"
678
+ ? path.join(latestOutputDir(context), `${safeFileSegment(task.id)}.json`)
679
+ : path.join(context.stateRoot, "outputs", `${runId}.json`);
680
+ }
707
681
  async function runWorkflowTask(context, config, task, runId, startedAt, options) {
708
682
  const target = await resolveWorkflowTarget(task.workflow, {
709
683
  cwd: context.repoRoot,
@@ -734,7 +708,7 @@ async function runWorkflowTask(context, config, task, runId, startedAt, options)
734
708
  });
735
709
  return scriptRun.result;
736
710
  }
737
- async function executeDeferredRunTarget(context, intent, options) {
711
+ async function executeDispatchRunTarget(context, intent, options) {
738
712
  try {
739
713
  const target = intent.target;
740
714
  if (options.localHandoffMaterialize && isLocalHandoffIntent(intent)) {
@@ -762,10 +736,9 @@ async function executeDeferredRunTarget(context, intent, options) {
762
736
  name: path.basename(context.repoRoot),
763
737
  surfaces: [],
764
738
  tasks: [],
765
- reactive: [],
766
739
  path: context.configPath,
767
740
  }));
768
- const result = await runWorkflowDeferredTarget(context, config, { ...intent, target }, options);
741
+ const result = await runWorkflowDispatchTarget(context, config, { ...intent, target }, options);
769
742
  return { status: "completed", output: result };
770
743
  }
771
744
  if (target.kind === "turn") {
@@ -825,12 +798,12 @@ async function materializeLocalHandoffIntent(context, intent, options) {
825
798
  queue,
826
799
  };
827
800
  }
828
- async function runWorkflowDeferredTarget(context, config, intent, options) {
801
+ async function runWorkflowDispatchTarget(context, config, intent, options) {
829
802
  const target = await resolveWorkflowTarget(intent.target.workflow, {
830
803
  cwd: context.repoRoot,
831
804
  });
832
805
  const startedAt = new Date().toISOString();
833
- const event = deferredWorkflowEvent(config, intent, startedAt);
806
+ const event = dispatchWorkflowEvent(config, intent, startedAt);
834
807
  const prompt = intent.target.prompt ?? target.prompt;
835
808
  const cwd = intent.target.cwd ?? options.workflowCwd ?? target.cwd ?? context.repoRoot;
836
809
  const scriptRun = await runWorkflowScript({
@@ -876,46 +849,24 @@ function workbenchWorkflowEvent(config, task, runId, startedAt) {
876
849
  },
877
850
  };
878
851
  }
879
- function deferredWorkflowEvent(config, intent, startedAt) {
852
+ function dispatchWorkflowEvent(config, intent, startedAt) {
880
853
  const event = intent.target.event ?? {};
881
854
  const payload = isRecord(event.payload) ? event.payload : {};
882
855
  return {
883
856
  ...event,
884
- id: stringValue(event.id, `deferred:${config.name}:${intent.id}`),
857
+ id: stringValue(event.id, `dispatch:${config.name}:${intent.id}`),
885
858
  type: stringValue(event.type, intent.target.workflow),
886
859
  source: stringValue(event.source, config.name),
887
860
  occurredAt: stringValue(event.occurredAt, intent.runAt),
888
861
  receivedAt: stringValue(event.receivedAt, startedAt),
889
862
  payload: {
890
- deferredRunId: intent.id,
863
+ dispatchRunId: intent.id,
891
864
  ...payload,
892
865
  },
893
866
  };
894
867
  }
895
868
  function exhaustiveTarget(value) {
896
- throw new Error(`Unsupported deferred run target: ${JSON.stringify(value)}`);
897
- }
898
- async function runReactiveRule(context, rule) {
899
- const startedAt = new Date().toISOString();
900
- const runId = `${startedAt.replace(/[:.]/g, "-")}-${rule.id}`;
901
- const outputPath = path.join(context.stateRoot, "outputs", `${runId}.json`);
902
- try {
903
- const result = await runSkill({
904
- id: rule.id,
905
- enabled: rule.enabled,
906
- kind: "skill",
907
- skill: rule.skill,
908
- var: `repair failures for ${rule.task}`,
909
- }, context);
910
- const run = runRecord(context, runId, rule.id, "reactive", startedAt, "completed", outputPath);
911
- await persistRun(context, run, result);
912
- return run;
913
- }
914
- catch (error) {
915
- const run = runRecord(context, runId, rule.id, "reactive", startedAt, "failed", outputPath, errorMessage(error));
916
- await persistRun(context, run, { error: errorMessage(error) });
917
- return run;
918
- }
869
+ throw new Error(`Unsupported dispatch run target: ${JSON.stringify(value)}`);
919
870
  }
920
871
  async function runSkill(task, context) {
921
872
  const skillPath = path.join(context.runtimeCodexHome, "skills", task.skill, "SKILL.md");
@@ -971,18 +922,6 @@ async function runGit(cwd, args) {
971
922
  }
972
923
  return { stdout, stderr };
973
924
  }
974
- async function runSystemctlUser(args) {
975
- const proc = spawn("systemctl", ["--user", ...args]);
976
- const [stdout, stderr, exitCode] = await Promise.all([
977
- collectText(proc.stdout),
978
- collectText(proc.stderr),
979
- exitCodeFor(proc),
980
- ]);
981
- if (exitCode !== 0) {
982
- throw new Error(`systemctl --user ${args.join(" ")} failed (${exitCode}): ${stderr || stdout}`);
983
- }
984
- return stdout;
985
- }
986
925
  function collectText(stream) {
987
926
  return new Promise((resolve, reject) => {
988
927
  let output = "";
@@ -1004,16 +943,20 @@ function exitCodeFor(child) {
1004
943
  child.once("exit", (code) => resolve(code));
1005
944
  });
1006
945
  }
1007
- async function persistRun(context, run, output) {
946
+ async function persistRun(context, task, run, output) {
1008
947
  await ensureStateDirs(context);
1009
948
  if (run.outputPath) {
1010
949
  await writeFile(run.outputPath, `${JSON.stringify(output, null, 2)}\n`);
1011
950
  }
1012
- await writeFile(path.join(context.stateRoot, "runs", `${run.id}.json`), `${JSON.stringify(run, null, 2)}\n`);
951
+ const runPath = task.history === "latest"
952
+ ? path.join(latestRunDir(context), `${safeFileSegment(task.id)}.json`)
953
+ : path.join(context.stateRoot, "runs", `${run.id}.json`);
954
+ await writeFile(runPath, `${JSON.stringify(run, null, 2)}\n`);
1013
955
  await writeHealth(context, run);
1014
956
  }
1015
957
  async function writeHealth(context, run) {
1016
- const runs = [...await readRuns(context), run].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
958
+ const runs = uniqueWorkbenchRuns([...await readRuns(context), run])
959
+ .sort((a, b) => a.startedAt.localeCompare(b.startedAt));
1017
960
  const health = {
1018
961
  updatedAt: new Date().toISOString(),
1019
962
  latestRun: run,
@@ -1025,79 +968,58 @@ async function writeHealth(context, run) {
1025
968
  await writeFile(path.join(context.stateRoot, "health", "summary.json"), `${JSON.stringify(health, null, 2)}\n`);
1026
969
  }
1027
970
  async function readRuns(context) {
1028
- const dir = path.join(context.stateRoot, "runs");
1029
- try {
1030
- const entries = await readdir(dir);
1031
- const runs = [];
1032
- for (const entry of entries) {
1033
- if (!entry.endsWith(".json")) {
1034
- continue;
1035
- }
1036
- try {
1037
- const runPath = path.join(dir, entry);
1038
- const parsed = parseJsonText(await readFile(runPath, "utf8"), runPath);
1039
- if (parsed && typeof parsed.taskId === "string") {
1040
- runs.push(parsed);
971
+ const dirs = [path.join(context.stateRoot, "runs"), latestRunDir(context)];
972
+ const runs = [];
973
+ for (const dir of dirs) {
974
+ try {
975
+ const entries = await readdir(dir);
976
+ for (const entry of entries) {
977
+ if (!entry.endsWith(".json")) {
978
+ continue;
979
+ }
980
+ try {
981
+ const runPath = path.join(dir, entry);
982
+ const parsed = parseJsonText(await readFile(runPath, "utf8"), runPath);
983
+ if (isWorkbenchRunRecord(parsed)) {
984
+ runs.push(parsed);
985
+ }
1041
986
  }
987
+ catch { }
1042
988
  }
1043
- catch { }
1044
989
  }
1045
- return runs;
1046
- }
1047
- catch {
1048
- return [];
1049
- }
1050
- }
1051
- async function createScheduledWorkbenchTaskIntent(context, task, now) {
1052
- try {
1053
- return await createDeferredRunIntent(context, {
1054
- id: scheduledDeferredRunId(task.id, now),
1055
- runAt: now.toISOString(),
1056
- target: {
1057
- kind: "workbench-task",
1058
- taskId: task.id,
1059
- },
1060
- createdBy: "workbench-schedule",
1061
- reason: `Scheduled workbench task ${task.id}`,
1062
- source: {
1063
- kind: "workbench-task-schedule",
1064
- taskId: task.id,
1065
- schedule: task.schedule,
1066
- date: now.toISOString().slice(0, 10),
1067
- },
1068
- });
1069
- }
1070
- catch (error) {
1071
- if (isAlreadyExistsError(error)) {
1072
- return undefined;
990
+ catch {
991
+ continue;
1073
992
  }
1074
- throw error;
1075
993
  }
994
+ return uniqueWorkbenchRuns(runs);
995
+ }
996
+ function uniqueWorkbenchRuns(runs) {
997
+ return [...new Map(runs.map((run) => [run.id, run])).values()];
1076
998
  }
1077
- async function readDeferredRunIntent(context, intentId) {
1078
- const intentPath = deferredIntentPath(context, intentId);
999
+ async function readDispatchRunIntent(context, intentId) {
1000
+ const intentPath = dispatchIntentPath(context, intentId);
1079
1001
  try {
1080
- return normalizeDeferredRunIntent(parseJsonText(await readFile(intentPath, "utf8"), intentPath));
1002
+ return normalizeDispatchRunIntent(parseJsonText(await readFile(intentPath, "utf8"), intentPath));
1081
1003
  }
1082
1004
  catch (error) {
1083
1005
  if (isNotFoundError(error)) {
1084
- throw new Error(`Unknown deferred run: ${intentId}`);
1006
+ throw new Error(`Unknown dispatch run: ${intentId}`);
1085
1007
  }
1086
1008
  throw error;
1087
1009
  }
1088
1010
  }
1089
- async function readDeferredRunAttempts(context, attemptIds) {
1011
+ async function readDispatchRunAttempts(context, attemptIds) {
1090
1012
  const attempts = [];
1091
1013
  for (const attemptId of attemptIds) {
1092
- const attemptPath = deferredAttemptPath(context, attemptId);
1014
+ const attemptPath = dispatchAttemptPath(context, attemptId);
1093
1015
  try {
1094
- attempts.push(normalizeDeferredRunAttempt(parseJsonText(await readFile(attemptPath, "utf8"), attemptPath)));
1016
+ attempts.push(normalizeDispatchRunAttempt(parseJsonText(await readFile(attemptPath, "utf8"), attemptPath)));
1095
1017
  }
1096
1018
  catch { }
1097
1019
  }
1098
1020
  return attempts.sort((left, right) => left.startedAt.localeCompare(right.startedAt));
1099
1021
  }
1100
- async function readDeferredRunAttemptOutputs(attempts) {
1022
+ async function readDispatchRunAttemptOutputs(attempts) {
1101
1023
  const outputs = [];
1102
1024
  for (const attempt of attempts) {
1103
1025
  if (!attempt.outputPath) {
@@ -1120,10 +1042,10 @@ async function readDeferredRunAttemptOutputs(attempts) {
1120
1042
  }
1121
1043
  return outputs;
1122
1044
  }
1123
- async function readDeferredRunCollectCursor(context, cursor) {
1124
- const file = deferredCollectCursorPath(context, cursor);
1045
+ async function readDispatchRunCollectCursor(context, cursor) {
1046
+ const file = dispatchCollectCursorPath(context, cursor);
1125
1047
  try {
1126
- return normalizeDeferredRunCollectCursor(parseJsonText(await readFile(file, "utf8"), file), cursor);
1048
+ return normalizeDispatchRunCollectCursor(parseJsonText(await readFile(file, "utf8"), file), cursor);
1127
1049
  }
1128
1050
  catch (error) {
1129
1051
  if (isNotFoundError(error)) {
@@ -1132,14 +1054,14 @@ async function readDeferredRunCollectCursor(context, cursor) {
1132
1054
  throw error;
1133
1055
  }
1134
1056
  }
1135
- async function claimDeferredRunIntent(context, intent, options) {
1136
- const current = await readDeferredRunIntent(context, intent.id);
1137
- if (!await isDeferredIntentDue(context, current, options.now)) {
1057
+ async function claimDispatchRunIntent(context, intent, options) {
1058
+ const current = await readDispatchRunIntent(context, intent.id);
1059
+ if (!await isDispatchIntentDue(context, current, options.now)) {
1138
1060
  return undefined;
1139
1061
  }
1140
- const claimPath = deferredClaimPath(context, current.id);
1062
+ const claimPath = dispatchClaimPath(context, current.id);
1141
1063
  const claimedAt = options.now.toISOString();
1142
- const attemptId = deferredAttemptId(current.id, claimedAt);
1064
+ const attemptId = dispatchAttemptId(current.id, claimedAt);
1143
1065
  const leaseExpiresAt = new Date(options.now.getTime() + options.leaseMs).toISOString();
1144
1066
  const executorId = `${process.pid}:${randomUUID()}`;
1145
1067
  const claim = { intentId: current.id, attemptId, claimedAt, leaseExpiresAt, executorId };
@@ -1185,11 +1107,11 @@ async function claimDeferredRunIntent(context, intent, options) {
1185
1107
  executorId,
1186
1108
  },
1187
1109
  };
1188
- await writeJsonFileAtomic(deferredIntentPath(context, current.id), running);
1110
+ await writeJsonFileAtomic(dispatchIntentPath(context, current.id), running);
1189
1111
  return { intent: running, attempt };
1190
1112
  }
1191
- async function releaseDeferredRunClaim(context, intentId) {
1192
- await rm(deferredClaimPath(context, intentId), { force: true });
1113
+ async function releaseDispatchRunClaim(context, intentId) {
1114
+ await rm(dispatchClaimPath(context, intentId), { force: true });
1193
1115
  }
1194
1116
  async function readClaimFile(file) {
1195
1117
  try {
@@ -1202,13 +1124,13 @@ async function readClaimFile(file) {
1202
1124
  return undefined;
1203
1125
  }
1204
1126
  }
1205
- function parseDeferredRunCreateParams(value) {
1127
+ function parseDispatchRunCreateParams(value) {
1206
1128
  const input = record(value);
1207
1129
  const runAt = optionalString(input.runAt);
1208
1130
  if (runAt && Number.isNaN(Date.parse(runAt))) {
1209
- throw new Error(`Deferred run runAt must be an ISO-compatible date: ${runAt}`);
1131
+ throw new Error(`Dispatch run runAt must be an ISO-compatible date: ${runAt}`);
1210
1132
  }
1211
- const target = parseDeferredRunTarget(input.target);
1133
+ const target = parseDispatchRunTarget(input.target);
1212
1134
  const source = recordOrUndefined(input.source);
1213
1135
  return compactUndefined({
1214
1136
  id: optionalString(input.id),
@@ -1217,7 +1139,7 @@ function parseDeferredRunCreateParams(value) {
1217
1139
  createdBy: optionalString(input.createdBy),
1218
1140
  reason: optionalString(input.reason),
1219
1141
  source,
1220
- dependsOn: parseDeferredRunDependencies(input.dependsOn),
1142
+ dependsOn: parseDispatchRunDependencies(input.dependsOn),
1221
1143
  });
1222
1144
  }
1223
1145
  function parsePromptQueueEnqueueParams(value) {
@@ -1226,7 +1148,7 @@ function parsePromptQueueEnqueueParams(value) {
1226
1148
  if (runAt && Number.isNaN(Date.parse(runAt))) {
1227
1149
  throw new Error(`Prompt queue runAt must be an ISO-compatible date: ${runAt}`);
1228
1150
  }
1229
- const afterStatus = deferredDependencyStatusValue(input.afterStatus, "prompt queue afterStatus");
1151
+ const afterStatus = dispatchDependencyStatusValue(input.afterStatus, "prompt queue afterStatus");
1230
1152
  return compactUndefined({
1231
1153
  id: optionalString(input.id),
1232
1154
  runAt,
@@ -1257,7 +1179,7 @@ function parseLocalHandoffEnqueueParams(value) {
1257
1179
  if (runAt && Number.isNaN(Date.parse(runAt))) {
1258
1180
  throw new Error(`Local handoff runAt must be an ISO-compatible date: ${runAt}`);
1259
1181
  }
1260
- const afterStatus = deferredDependencyStatusValue(input.afterStatus, "local handoff afterStatus");
1182
+ const afterStatus = dispatchDependencyStatusValue(input.afterStatus, "local handoff afterStatus");
1261
1183
  return compactUndefined({
1262
1184
  id: optionalString(input.id),
1263
1185
  runAt,
@@ -1286,30 +1208,30 @@ function parseLocalHandoffEnqueueParams(value) {
1286
1208
  source: recordOrUndefined(input.source),
1287
1209
  });
1288
1210
  }
1289
- function parseDeferredRunDependencies(value) {
1211
+ function parseDispatchRunDependencies(value) {
1290
1212
  if (!Array.isArray(value)) {
1291
1213
  return undefined;
1292
1214
  }
1293
- const dependencies = value.map(parseDeferredRunDependency);
1215
+ const dependencies = value.map(parseDispatchRunDependency);
1294
1216
  return dependencies.length > 0 ? dependencies : undefined;
1295
1217
  }
1296
- function parseDeferredRunDependency(value) {
1218
+ function parseDispatchRunDependency(value) {
1297
1219
  const input = record(value);
1298
- const kind = requiredString(input.kind, "deferred run dependency kind");
1299
- if (kind !== "deferred-run") {
1300
- throw new Error(`Invalid deferred run dependency kind: ${kind}`);
1220
+ const kind = requiredString(input.kind, "dispatch run dependency kind");
1221
+ if (kind !== "dispatch-run") {
1222
+ throw new Error(`Invalid dispatch run dependency kind: ${kind}`);
1301
1223
  }
1302
1224
  return compactUndefined({
1303
- kind: "deferred-run",
1304
- intentId: requiredString(input.intentId, "deferred run dependency intentId"),
1305
- status: deferredDependencyStatusValue(input.status, "deferred run dependency status"),
1225
+ kind: "dispatch-run",
1226
+ intentId: requiredString(input.intentId, "dispatch run dependency intentId"),
1227
+ status: dispatchDependencyStatusValue(input.status, "dispatch run dependency status"),
1306
1228
  });
1307
1229
  }
1308
- function parseDeferredRunRetryParams(value) {
1230
+ function parseDispatchRunRetryParams(value) {
1309
1231
  const input = record(value);
1310
1232
  const runAt = optionalString(input.runAt);
1311
1233
  if (runAt && Number.isNaN(Date.parse(runAt))) {
1312
- throw new Error(`Deferred run retry runAt must be an ISO-compatible date: ${runAt}`);
1234
+ throw new Error(`Dispatch run retry runAt must be an ISO-compatible date: ${runAt}`);
1313
1235
  }
1314
1236
  return compactUndefined({
1315
1237
  id: optionalString(input.id),
@@ -1319,13 +1241,13 @@ function parseDeferredRunRetryParams(value) {
1319
1241
  source: recordOrUndefined(input.source),
1320
1242
  });
1321
1243
  }
1322
- function retryDeferredRunSource(originalIntent, source) {
1244
+ function retryDispatchRunSource(originalIntent, source) {
1323
1245
  const originalSource = recordOrUndefined(originalIntent.source) ?? {};
1324
1246
  return compactUndefined({
1325
1247
  ...originalSource,
1326
- kind: optionalString(originalSource.kind) ?? "deferred-retry",
1248
+ kind: optionalString(originalSource.kind) ?? "dispatch-retry",
1327
1249
  retry: compactUndefined({
1328
- kind: "deferred-retry",
1250
+ kind: "dispatch-retry",
1329
1251
  originalIntentId: originalIntent.id,
1330
1252
  originalStatus: originalIntent.status,
1331
1253
  originalRunAt: originalIntent.runAt,
@@ -1396,30 +1318,30 @@ function isLocalHandoffIntent(intent, options = {}) {
1396
1318
  }
1397
1319
  return true;
1398
1320
  }
1399
- function parseDeferredRunTarget(value) {
1321
+ function parseDispatchRunTarget(value) {
1400
1322
  const target = record(value);
1401
- const kind = requiredString(target.kind, "deferred run target kind");
1323
+ const kind = requiredString(target.kind, "dispatch run target kind");
1402
1324
  if (kind === "workbench-task") {
1403
1325
  return {
1404
1326
  kind,
1405
- taskId: requiredString(target.taskId, "deferred run workbench-task taskId"),
1327
+ taskId: requiredString(target.taskId, "dispatch run workbench-task taskId"),
1406
1328
  };
1407
1329
  }
1408
1330
  if (kind === "workflow") {
1409
1331
  return compactUndefined({
1410
1332
  kind,
1411
- workflow: requiredString(target.workflow, "deferred run workflow target workflow"),
1333
+ workflow: requiredString(target.workflow, "dispatch run workflow target workflow"),
1412
1334
  event: recordOrUndefined(target.event),
1413
1335
  prompt: optionalString(target.prompt),
1414
1336
  cwd: optionalString(target.cwd),
1415
1337
  model: optionalString(target.model),
1416
- sandbox: sandboxValue(target.sandbox, "deferred run workflow target sandbox"),
1417
- approvalPolicy: approvalPolicyValue(target.approvalPolicy, "deferred run workflow target approvalPolicy"),
1338
+ sandbox: sandboxValue(target.sandbox, "dispatch run workflow target sandbox"),
1339
+ approvalPolicy: approvalPolicyValue(target.approvalPolicy, "dispatch run workflow target approvalPolicy"),
1418
1340
  permissions: optionalString(target.permissions),
1419
1341
  });
1420
1342
  }
1421
1343
  if (kind === "turn") {
1422
- const prompt = requiredString(target.prompt, "deferred run turn target prompt");
1344
+ const prompt = requiredString(target.prompt, "dispatch run turn target prompt");
1423
1345
  return compactUndefined({
1424
1346
  kind,
1425
1347
  prompt,
@@ -1427,39 +1349,39 @@ function parseDeferredRunTarget(value) {
1427
1349
  cwd: optionalString(target.cwd),
1428
1350
  model: optionalString(target.model),
1429
1351
  serviceTier: optionalString(target.serviceTier),
1430
- effort: reasoningEffortValue(target.effort, "deferred run turn target effort"),
1431
- sandbox: sandboxValue(target.sandbox, "deferred run turn target sandbox"),
1432
- approvalPolicy: approvalPolicyValue(target.approvalPolicy, "deferred run turn target approvalPolicy"),
1352
+ effort: reasoningEffortValue(target.effort, "dispatch run turn target effort"),
1353
+ sandbox: sandboxValue(target.sandbox, "dispatch run turn target sandbox"),
1354
+ approvalPolicy: approvalPolicyValue(target.approvalPolicy, "dispatch run turn target approvalPolicy"),
1433
1355
  permissions: optionalString(target.permissions),
1434
1356
  responsesapiClientMetadata: stringRecord(target.responsesapiClientMetadata),
1435
1357
  outputSchema: target.outputSchema,
1436
1358
  });
1437
1359
  }
1438
- throw new Error(`Invalid deferred run target kind: ${kind}`);
1360
+ throw new Error(`Invalid dispatch run target kind: ${kind}`);
1439
1361
  }
1440
- function normalizeDeferredRunIntent(value) {
1362
+ function normalizeDispatchRunIntent(value) {
1441
1363
  const input = record(value);
1442
1364
  return {
1443
- id: requiredString(input.id, "deferred run id"),
1444
- status: deferredRunStatus(input.status),
1365
+ id: requiredString(input.id, "dispatch run id"),
1366
+ status: dispatchRunStatus(input.status),
1445
1367
  mode: workbenchMode(input.mode),
1446
- runAt: requiredString(input.runAt, "deferred run runAt"),
1447
- target: parseDeferredRunTarget(input.target),
1448
- createdAt: requiredString(input.createdAt, "deferred run createdAt"),
1449
- updatedAt: requiredString(input.updatedAt, "deferred run updatedAt"),
1368
+ runAt: requiredString(input.runAt, "dispatch run runAt"),
1369
+ target: parseDispatchRunTarget(input.target),
1370
+ createdAt: requiredString(input.createdAt, "dispatch run createdAt"),
1371
+ updatedAt: requiredString(input.updatedAt, "dispatch run updatedAt"),
1450
1372
  createdBy: optionalString(input.createdBy),
1451
1373
  reason: optionalString(input.reason),
1452
1374
  source: recordOrUndefined(input.source),
1453
- dependsOn: parseDeferredRunDependencies(input.dependsOn),
1375
+ dependsOn: parseDispatchRunDependencies(input.dependsOn),
1454
1376
  attemptIds: Array.isArray(input.attemptIds)
1455
1377
  ? input.attemptIds.filter((entry) => typeof entry === "string")
1456
1378
  : [],
1457
1379
  lease: isRecord(input.lease)
1458
1380
  ? {
1459
- attemptId: requiredString(input.lease.attemptId, "deferred run lease attemptId"),
1460
- claimedAt: requiredString(input.lease.claimedAt, "deferred run lease claimedAt"),
1461
- expiresAt: requiredString(input.lease.expiresAt, "deferred run lease expiresAt"),
1462
- executorId: requiredString(input.lease.executorId, "deferred run lease executorId"),
1381
+ attemptId: requiredString(input.lease.attemptId, "dispatch run lease attemptId"),
1382
+ claimedAt: requiredString(input.lease.claimedAt, "dispatch run lease claimedAt"),
1383
+ expiresAt: requiredString(input.lease.expiresAt, "dispatch run lease expiresAt"),
1384
+ executorId: requiredString(input.lease.executorId, "dispatch run lease executorId"),
1463
1385
  }
1464
1386
  : undefined,
1465
1387
  completedAt: optionalString(input.completedAt),
@@ -1467,101 +1389,58 @@ function normalizeDeferredRunIntent(value) {
1467
1389
  error: optionalString(input.error),
1468
1390
  };
1469
1391
  }
1470
- function normalizeDeferredRunAttempt(value) {
1392
+ function normalizeDispatchRunAttempt(value) {
1471
1393
  const input = record(value);
1472
1394
  return {
1473
- id: requiredString(input.id, "deferred run attempt id"),
1474
- intentId: requiredString(input.intentId, "deferred run attempt intentId"),
1475
- status: deferredAttemptStatus(input.status),
1395
+ id: requiredString(input.id, "dispatch run attempt id"),
1396
+ intentId: requiredString(input.intentId, "dispatch run attempt intentId"),
1397
+ status: dispatchAttemptStatus(input.status),
1476
1398
  mode: workbenchMode(input.mode),
1477
- startedAt: requiredString(input.startedAt, "deferred run attempt startedAt"),
1399
+ startedAt: requiredString(input.startedAt, "dispatch run attempt startedAt"),
1478
1400
  finishedAt: optionalString(input.finishedAt),
1479
- executorId: requiredString(input.executorId, "deferred run attempt executorId"),
1480
- leaseExpiresAt: requiredString(input.leaseExpiresAt, "deferred run attempt leaseExpiresAt"),
1401
+ executorId: requiredString(input.executorId, "dispatch run attempt executorId"),
1402
+ leaseExpiresAt: requiredString(input.leaseExpiresAt, "dispatch run attempt leaseExpiresAt"),
1481
1403
  outputPath: optionalString(input.outputPath),
1482
1404
  error: optionalString(input.error),
1483
1405
  };
1484
1406
  }
1485
- function normalizeDeferredRunCollectCursor(value, fallbackCursor) {
1407
+ function normalizeDispatchRunCollectCursor(value, fallbackCursor) {
1486
1408
  const input = record(value);
1487
1409
  return compactUndefined({
1488
1410
  cursor: optionalString(input.cursor) ?? fallbackCursor,
1489
- updatedAt: requiredString(input.updatedAt, "deferred run collect cursor updatedAt"),
1411
+ updatedAt: requiredString(input.updatedAt, "dispatch run collect cursor updatedAt"),
1490
1412
  lastUpdatedAt: optionalString(input.lastUpdatedAt),
1491
1413
  lastIntentId: optionalString(input.lastIntentId),
1492
1414
  });
1493
1415
  }
1494
- function dueTasks(tasks, runs, now, intents = []) {
1495
- return tasks.filter((task) => {
1496
- if (!task.enabled) {
1497
- return false;
1498
- }
1499
- if (!task.schedule) {
1500
- return false;
1501
- }
1502
- return isScheduleDue(task.schedule, now) &&
1503
- !hasRunForDate(task.id, runs, now) &&
1504
- !hasScheduledIntentForDate(task.id, intents, now);
1505
- });
1506
- }
1507
- function isScheduleDue(schedule, now) {
1508
- const parts = schedule.trim().split(/\s+/);
1509
- if (parts.length !== 5) {
1510
- throw new Error(`Invalid workbench task schedule: ${schedule}`);
1511
- }
1512
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
1513
- return cronPartMatches(minute, now.getUTCMinutes()) &&
1514
- cronPartMatches(hour, now.getUTCHours()) &&
1515
- cronPartMatches(dayOfMonth, now.getUTCDate()) &&
1516
- cronPartMatches(month, now.getUTCMonth() + 1) &&
1517
- cronPartMatches(dayOfWeek, now.getUTCDay());
1518
- }
1519
- function cronPartMatches(part, value) {
1520
- if (!part || part === "*") {
1521
- return true;
1522
- }
1523
- return part.split(",").some((item) => Number.parseInt(item, 10) === value);
1524
- }
1525
- function hasRunForDate(taskId, runs, now) {
1526
- const today = now.toISOString().slice(0, 10);
1527
- return runs.some((run) => run.taskId === taskId && run.startedAt.startsWith(today));
1528
- }
1529
- function hasScheduledIntentForDate(taskId, intents, now) {
1530
- const expected = scheduledDeferredRunId(taskId, now);
1531
- return intents.some((intent) => intent.id === expected ||
1532
- (intent.target.kind === "workbench-task" &&
1533
- intent.target.taskId === taskId &&
1534
- intent.source?.kind === "workbench-task-schedule" &&
1535
- intent.source.date === now.toISOString().slice(0, 10)));
1536
- }
1537
- async function isDeferredIntentDue(context, intent, now) {
1416
+ async function isDispatchIntentDue(context, intent, now) {
1538
1417
  if (intent.status === "pending") {
1539
1418
  return intent.runAt <= now.toISOString() &&
1540
- await areDeferredRunDependenciesSatisfied(context, intent.dependsOn);
1419
+ await areDispatchRunDependenciesSatisfied(context, intent.dependsOn);
1541
1420
  }
1542
1421
  if (intent.status === "running" && intent.lease?.expiresAt) {
1543
1422
  return intent.lease.expiresAt <= now.toISOString();
1544
1423
  }
1545
1424
  return false;
1546
1425
  }
1547
- async function areDeferredRunDependenciesSatisfied(context, dependencies) {
1426
+ async function areDispatchRunDependenciesSatisfied(context, dependencies) {
1548
1427
  if (!dependencies || dependencies.length === 0) {
1549
1428
  return true;
1550
1429
  }
1551
1430
  for (const dependency of dependencies) {
1552
- if (dependency.kind !== "deferred-run") {
1431
+ if (dependency.kind !== "dispatch-run") {
1553
1432
  return false;
1554
1433
  }
1555
1434
  let intent;
1556
1435
  try {
1557
- intent = await readDeferredRunIntent(context, dependency.intentId);
1436
+ intent = await readDispatchRunIntent(context, dependency.intentId);
1558
1437
  }
1559
1438
  catch {
1560
1439
  return false;
1561
1440
  }
1562
1441
  const status = dependency.status ?? "completed";
1563
1442
  if (status === "terminal") {
1564
- if (!isTerminalDeferredRunStatus(intent.status)) {
1443
+ if (!isTerminalDispatchRunStatus(intent.status)) {
1565
1444
  return false;
1566
1445
  }
1567
1446
  continue;
@@ -1572,10 +1451,10 @@ async function areDeferredRunDependenciesSatisfied(context, dependencies) {
1572
1451
  }
1573
1452
  return true;
1574
1453
  }
1575
- function isTerminalDeferredRunStatus(status) {
1454
+ function isTerminalDispatchRunStatus(status) {
1576
1455
  return status === "completed" || status === "failed" || status === "canceled";
1577
1456
  }
1578
- function isAfterDeferredRunCollectCursor(intent, cursor) {
1457
+ function isAfterDispatchRunCollectCursor(intent, cursor) {
1579
1458
  if (!cursor?.lastUpdatedAt) {
1580
1459
  return true;
1581
1460
  }
@@ -1603,182 +1482,6 @@ function workbenchDoctorErrors(context) {
1603
1482
  }
1604
1483
  return [];
1605
1484
  }
1606
- async function collectWorkbenchRunnerInfo(context, tasks, deferredRuns, probe) {
1607
- const workbenchRoot = path.resolve(context.repoRoot);
1608
- const hasScheduledWork = tasks.some((task) => task.enabled && task.schedule);
1609
- const hasPendingDeferredWork = deferredRuns.some((intent) => intent.status === "pending" || intent.status === "running");
1610
- const hasRunnableWork = hasScheduledWork || hasPendingDeferredWork;
1611
- const base = {
1612
- kind: "systemd-user",
1613
- workbenchRoot,
1614
- candidates: [],
1615
- };
1616
- if (context.mode !== "local") {
1617
- return {
1618
- ...base,
1619
- status: "unsupported",
1620
- warning: hasRunnableWork
1621
- ? "Runner visibility currently checks local systemd user timers only."
1622
- : undefined,
1623
- };
1624
- }
1625
- if (os.platform() !== "linux") {
1626
- return {
1627
- ...base,
1628
- status: "unsupported",
1629
- warning: hasRunnableWork
1630
- ? "No local systemd user timer check is available on this platform."
1631
- : undefined,
1632
- };
1633
- }
1634
- try {
1635
- const timerRows = parseSystemdTimerRows(await probe(["list-timers", "--all", "--no-legend", "--no-pager"]));
1636
- const candidates = [];
1637
- for (const row of timerRows) {
1638
- const serviceShow = parseSystemdShow(await probe([
1639
- "show",
1640
- row.service,
1641
- "--property=ExecStart",
1642
- "--property=ActiveState",
1643
- "--property=UnitFileState",
1644
- "--no-pager",
1645
- ]));
1646
- const command = normalizeSystemdCommand(serviceShow.ExecStart);
1647
- if (!command.includes("codex-toys")) {
1648
- continue;
1649
- }
1650
- const runsWorkbenchTick = /\bworkbench\s+tick\b/.test(command);
1651
- const runsDeferredOnly = /\bworkbench\s+deferred\s+run-due\b/.test(command);
1652
- if (!runsWorkbenchTick && !runsDeferredOnly) {
1653
- continue;
1654
- }
1655
- const timerShow = parseSystemdShow(await probe([
1656
- "show",
1657
- row.timer,
1658
- "--property=ActiveState",
1659
- "--property=UnitFileState",
1660
- "--property=NextElapseUSecRealtime",
1661
- "--property=LastTriggerUSec",
1662
- "--no-pager",
1663
- ]));
1664
- const runnerWorkbenchRoot = extractWorkbenchRootFromCommand(command);
1665
- const matchesWorkbench = runnerWorkbenchRoot
1666
- ? path.resolve(runnerWorkbenchRoot) === workbenchRoot
1667
- : command.includes(workbenchRoot);
1668
- candidates.push(compactUndefined({
1669
- kind: "systemd-user",
1670
- timer: row.timer,
1671
- service: row.service,
1672
- command,
1673
- activeState: serviceShow.ActiveState,
1674
- unitFileState: serviceShow.UnitFileState,
1675
- timerActiveState: timerShow.ActiveState,
1676
- timerUnitFileState: timerShow.UnitFileState,
1677
- nextTrigger: timerShow.NextElapseUSecRealtime,
1678
- lastTrigger: timerShow.LastTriggerUSec,
1679
- workbenchRoot: runnerWorkbenchRoot,
1680
- runsWorkbenchTick,
1681
- runsDeferredOnly,
1682
- matchesWorkbench,
1683
- }));
1684
- }
1685
- const selected = candidates.find((candidate) => candidate.matchesWorkbench &&
1686
- candidate.runsWorkbenchTick &&
1687
- candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
1688
- candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
1689
- candidate.runsWorkbenchTick) ?? candidates.find((candidate) => candidate.matchesWorkbench);
1690
- if (!selected) {
1691
- return {
1692
- ...base,
1693
- status: "missing",
1694
- candidates,
1695
- warning: hasRunnableWork
1696
- ? "No matching local runner was found; due work needs a manual tick or another scheduler."
1697
- : undefined,
1698
- };
1699
- }
1700
- const status = selected.timerActiveState === "active" ? "active" : "inactive";
1701
- return {
1702
- ...base,
1703
- status,
1704
- selected,
1705
- candidates,
1706
- warning: selected.runsDeferredOnly
1707
- ? "The matching runner only runs deferred work; scheduled tasks need workbench tick."
1708
- : status === "inactive" && hasRunnableWork
1709
- ? "The matching local runner is not active; due work needs a manual tick or another scheduler."
1710
- : undefined,
1711
- };
1712
- }
1713
- catch (error) {
1714
- return {
1715
- ...base,
1716
- status: "unknown",
1717
- error: error instanceof Error ? error.message : String(error),
1718
- warning: hasRunnableWork
1719
- ? "Could not inspect local runner status; due work may need a manual tick or another scheduler."
1720
- : undefined,
1721
- };
1722
- }
1723
- }
1724
- function parseSystemdTimerRows(output) {
1725
- const rows = [];
1726
- for (const line of output.split(/\r?\n/)) {
1727
- const trimmed = line.trim();
1728
- if (!trimmed) {
1729
- continue;
1730
- }
1731
- const fields = trimmed.split(/\s+/);
1732
- const timerIndex = fields.findIndex((field) => field.endsWith(".timer"));
1733
- if (timerIndex < 0) {
1734
- continue;
1735
- }
1736
- const timer = fields[timerIndex];
1737
- const service = fields[timerIndex + 1];
1738
- if (!timer || !service?.endsWith(".service")) {
1739
- continue;
1740
- }
1741
- rows.push({ timer, service });
1742
- }
1743
- return rows;
1744
- }
1745
- function parseSystemdShow(output) {
1746
- const result = {};
1747
- for (const line of output.split(/\r?\n/)) {
1748
- const index = line.indexOf("=");
1749
- if (index <= 0) {
1750
- continue;
1751
- }
1752
- result[line.slice(0, index)] = line.slice(index + 1);
1753
- }
1754
- return result;
1755
- }
1756
- function normalizeSystemdCommand(value) {
1757
- return (value ?? "")
1758
- .replace(/\\x([0-9a-fA-F]{2})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
1759
- .replace(/\s+/g, " ")
1760
- .trim();
1761
- }
1762
- function extractWorkbenchRootFromCommand(command) {
1763
- const match = command.match(/--workbench-root(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s;]+))/);
1764
- return match?.[1] ?? match?.[2] ?? match?.[3];
1765
- }
1766
- function formatWorkbenchRunnerInfo(runner) {
1767
- if (!runner) {
1768
- return "not checked";
1769
- }
1770
- if (runner.selected) {
1771
- const command = runner.selected.runsWorkbenchTick ? "workbench tick" : "workbench deferred run-due";
1772
- return `${runner.status} ${runner.selected.timer} -> ${runner.selected.service} (${command})`;
1773
- }
1774
- if (runner.status === "unsupported") {
1775
- return "unsupported";
1776
- }
1777
- if (runner.status === "unknown") {
1778
- return `unknown${runner.error ? ` (${runner.error})` : ""}`;
1779
- }
1780
- return `${runner.status}${runner.candidates.length > 0 ? ` (${runner.candidates.length} codex-toys runner candidates)` : ""}`;
1781
- }
1782
1485
  function consecutiveFailures(taskId, runs) {
1783
1486
  let count = 0;
1784
1487
  for (const run of runs.filter((item) => item.taskId === taskId).sort((a, b) => b.startedAt.localeCompare(a.startedAt))) {
@@ -1803,73 +1506,76 @@ function runRecord(context, id, taskId, kind, startedAt, status, outputPath, err
1803
1506
  };
1804
1507
  }
1805
1508
  async function ensureStateDirs(context) {
1806
- for (const name of ["state", "runs", "outputs", "health"]) {
1509
+ for (const name of ["state", "runs", "outputs", "latest-runs", "latest-outputs", "health"]) {
1807
1510
  await mkdir(path.join(context.stateRoot, name), { recursive: true });
1808
1511
  }
1809
1512
  }
1810
- async function ensureDeferredRunDirs(context) {
1513
+ async function ensureDispatchRunDirs(context) {
1811
1514
  for (const dir of [
1812
- deferredIntentDir(context),
1813
- deferredAttemptDir(context),
1814
- deferredOutputDir(context),
1815
- deferredClaimDir(context),
1816
- deferredCollectCursorDir(context),
1515
+ dispatchIntentDir(context),
1516
+ dispatchAttemptDir(context),
1517
+ dispatchOutputDir(context),
1518
+ dispatchClaimDir(context),
1519
+ dispatchCollectCursorDir(context),
1817
1520
  ]) {
1818
1521
  await mkdir(dir, { recursive: true });
1819
1522
  }
1820
1523
  }
1821
- function deferredRoot(context) {
1822
- return path.join(context.stateRoot, "deferred");
1524
+ function dispatchRoot(context) {
1525
+ return path.join(context.stateRoot, "dispatch");
1823
1526
  }
1824
- function deferredIntentDir(context) {
1825
- return path.join(deferredRoot(context), "intents");
1527
+ function dispatchIntentDir(context) {
1528
+ return path.join(dispatchRoot(context), "intents");
1826
1529
  }
1827
- function deferredAttemptDir(context) {
1828
- return path.join(deferredRoot(context), "attempts");
1530
+ function dispatchAttemptDir(context) {
1531
+ return path.join(dispatchRoot(context), "attempts");
1829
1532
  }
1830
- function deferredOutputDir(context) {
1831
- return path.join(deferredRoot(context), "outputs");
1533
+ function dispatchOutputDir(context) {
1534
+ return path.join(dispatchRoot(context), "outputs");
1832
1535
  }
1833
- function deferredClaimDir(context) {
1834
- return path.join(deferredRoot(context), "claims");
1536
+ function dispatchClaimDir(context) {
1537
+ return path.join(dispatchRoot(context), "claims");
1835
1538
  }
1836
- function deferredCollectCursorDir(context) {
1837
- return path.join(deferredRoot(context), "collect-cursors");
1539
+ function dispatchCollectCursorDir(context) {
1540
+ return path.join(dispatchRoot(context), "collect-cursors");
1838
1541
  }
1839
- function deferredIntentPath(context, intentId) {
1840
- return path.join(deferredIntentDir(context), `${safeFileSegment(intentId)}.json`);
1542
+ function dispatchIntentPath(context, intentId) {
1543
+ return path.join(dispatchIntentDir(context), `${safeFileSegment(intentId)}.json`);
1841
1544
  }
1842
- function deferredAttemptPath(context, attemptId) {
1843
- return path.join(deferredAttemptDir(context), `${safeFileSegment(attemptId)}.json`);
1545
+ function dispatchAttemptPath(context, attemptId) {
1546
+ return path.join(dispatchAttemptDir(context), `${safeFileSegment(attemptId)}.json`);
1844
1547
  }
1845
- function deferredClaimPath(context, intentId) {
1846
- return path.join(deferredClaimDir(context), `${safeFileSegment(intentId)}.json`);
1548
+ function dispatchClaimPath(context, intentId) {
1549
+ return path.join(dispatchClaimDir(context), `${safeFileSegment(intentId)}.json`);
1847
1550
  }
1848
- function deferredCollectCursorPath(context, cursor) {
1849
- return path.join(deferredCollectCursorDir(context), `${safeFileSegment(cursor)}.json`);
1551
+ function dispatchCollectCursorPath(context, cursor) {
1552
+ return path.join(dispatchCollectCursorDir(context), `${safeFileSegment(cursor)}.json`);
1850
1553
  }
1851
- function deferredCollectCursorName(value, defaultCursor = "default") {
1554
+ function dispatchCollectCursorName(value, defaultCursor = "default") {
1852
1555
  const cursor = value?.trim() || defaultCursor;
1853
1556
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(cursor)) {
1854
- throw new Error(`Invalid deferred collect cursor: ${cursor}`);
1557
+ throw new Error(`Invalid dispatch collect cursor: ${cursor}`);
1855
1558
  }
1856
1559
  return cursor;
1857
1560
  }
1858
- function deferredRunId(createdAt) {
1859
- return `deferred-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1561
+ function dispatchRunId(createdAt) {
1562
+ return `dispatch-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1860
1563
  }
1861
- function deferredRetryRunId(originalIntentId, createdAt) {
1564
+ function dispatchRetryRunId(originalIntentId, createdAt) {
1862
1565
  return `retry-${safeFileSegment(originalIntentId).slice(0, 40)}-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1863
1566
  }
1864
- function deferredAttemptId(intentId, startedAt) {
1567
+ function dispatchAttemptId(intentId, startedAt) {
1865
1568
  return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${safeFileSegment(intentId).slice(0, 48)}`;
1866
1569
  }
1867
- function scheduledDeferredRunId(taskId, now) {
1868
- return `scheduled-${safeFileSegment(taskId)}-${now.toISOString().slice(0, 10)}`;
1570
+ function latestRunDir(context) {
1571
+ return path.join(context.stateRoot, "latest-runs");
1572
+ }
1573
+ function latestOutputDir(context) {
1574
+ return path.join(context.stateRoot, "latest-outputs");
1869
1575
  }
1870
1576
  function safeFileSegment(value) {
1871
1577
  const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1872
- return safe.slice(0, 120) || "deferred-run";
1578
+ return safe.slice(0, 120) || "dispatch-run";
1873
1579
  }
1874
1580
  async function writeNewJsonFile(file, value) {
1875
1581
  await mkdir(path.dirname(file), { recursive: true });
@@ -1944,10 +1650,10 @@ function codexConfigTemplate() {
1944
1650
  "",
1945
1651
  ].join("\n");
1946
1652
  }
1947
- function actionsWorkflowTemplate(provider) {
1653
+ function actionsWorkflowTemplate(provider, runnerImage) {
1948
1654
  const checkout = provider === "github" ? "actions/checkout@v4" : "actions/checkout@v4";
1949
1655
  const setupNode = provider === "github" ? "actions/setup-node@v4" : "actions/setup-node@v4";
1950
- return [
1656
+ const lines = [
1951
1657
  "name: Codex Toys Actions",
1952
1658
  "",
1953
1659
  "on:",
@@ -1958,38 +1664,19 @@ function actionsWorkflowTemplate(provider) {
1958
1664
  "jobs:",
1959
1665
  " workbench:",
1960
1666
  " runs-on: ubuntu-latest",
1961
- " permissions:",
1962
- " contents: write",
1963
- " steps:",
1964
- ` - uses: ${checkout}`,
1965
- ` - uses: ${setupNode}`,
1966
- " with:",
1967
- " node-version: 24",
1968
- " - run: npm install -g vite-plus",
1969
- " - run: vp dlx codex-toys actions prepare-auth",
1970
- " env:",
1971
- " CODEX_AUTH_JSON_B64: ${{ secrets.CODEX_AUTH_JSON_B64 }}",
1972
- " CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}",
1973
- " OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}",
1974
- " - run: vp dlx codex-toys workbench tick --mode actions",
1975
- " - if: always()",
1976
- " run: vp dlx codex-toys actions cleanup",
1977
- " - if: always()",
1978
- " run: |",
1979
- " git add -- .codex/memories .codex/workbench/actions",
1980
- " if [ -d .codex/feed/actions ]; then",
1981
- " git add -- .codex/feed/actions",
1982
- " fi",
1983
- " if [ -d .codex/sessions ]; then",
1984
- " git add -A -f -- .codex/sessions",
1985
- " fi",
1986
- " git diff --cached --quiet && exit 0",
1987
- " git config user.name codex-toys-actions",
1988
- " git config user.email codex-toys-actions@users.noreply.github.com",
1989
- " git commit -m \"Update Codex workbench state\"",
1990
- " git push",
1991
- "",
1992
- ].join("\n");
1667
+ ];
1668
+ if (runnerImage) {
1669
+ lines.push(" container:", ` image: ${runnerImage}`);
1670
+ }
1671
+ lines.push(" permissions:", " contents: write", " steps:", ` - uses: ${checkout}`);
1672
+ if (runnerImage) {
1673
+ lines.push(" - run: git config --global --add safe.directory \"${GITHUB_WORKSPACE:-$PWD}\"", " - run: codex-toys actions prepare-auth");
1674
+ }
1675
+ else {
1676
+ lines.push(` - uses: ${setupNode}`, " with:", " node-version: 24", " - run: npm install -g vite-plus", " - run: vp dlx codex-toys actions prepare-auth");
1677
+ }
1678
+ lines.push(" env:", " CODEX_AUTH_JSON_B64: ${{ secrets.CODEX_AUTH_JSON_B64 }}", " CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}", " OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}", ` - run: ${runnerImage ? "codex-toys" : "vp dlx codex-toys"} workbench dispatch run-due --mode actions`, " - if: always()", ` run: ${runnerImage ? "codex-toys" : "vp dlx codex-toys"} actions cleanup`, " - if: always()", " run: |", " git add -- .codex/memories .codex/workbench/actions", " if [ -d .codex/feed/actions ]; then", " git add -- .codex/feed/actions", " fi", " if [ -d .codex/sessions ]; then", " git add -A -f -- .codex/sessions", " fi", " if git diff --cached --quiet; then", " upstream=\"$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)\"", " if [ -z \"${upstream}\" ] || [ \"$(git rev-list --count \"${upstream}..HEAD\")\" = \"0\" ]; then", " exit 0", " fi", " else", " git config user.name codex-toys-actions", " git config user.email codex-toys-actions@users.noreply.github.com", " git commit -m \"Update Codex workbench state\"", " fi", " git push", "");
1679
+ return lines.join("\n");
1993
1680
  }
1994
1681
  function actionsGitignoreEntries() {
1995
1682
  return [
@@ -2031,12 +1718,12 @@ function parseTask(input) {
2031
1718
  const id = requiredTaskId(input.id);
2032
1719
  const enabled = input.enabled === undefined ? true : booleanValue(input.enabled, `workbench task ${id} enabled`);
2033
1720
  const kind = requiredString(input.kind, `workbench task ${id} kind`);
2034
- const schedule = optionalString(input.schedule);
2035
- if (schedule) {
2036
- isScheduleDue(schedule, new Date());
1721
+ if (input.schedule !== undefined) {
1722
+ throw new Error(`workbench task ${id} schedule has been removed; run explicit codex-toys commands from systemd or Actions schedules`);
2037
1723
  }
1724
+ const history = workbenchTaskHistoryValue(input.history, `workbench task ${id} history`);
2038
1725
  if (kind === "skill") {
2039
- return { id, enabled, kind, skill: requiredString(input.skill, `workbench task ${id} skill`), schedule, var: optionalString(input.var) };
1726
+ return { id, enabled, kind, skill: requiredString(input.skill, `workbench task ${id} skill`), history, var: optionalString(input.var) };
2040
1727
  }
2041
1728
  if (kind === "workflow") {
2042
1729
  return {
@@ -2044,7 +1731,7 @@ function parseTask(input) {
2044
1731
  enabled,
2045
1732
  kind,
2046
1733
  workflow: requiredString(input.workflow, `workbench task ${id} workflow`),
2047
- schedule,
1734
+ history,
2048
1735
  event: isRecord(input.event) ? input.event : undefined,
2049
1736
  prompt: optionalString(input.prompt),
2050
1737
  cwd: optionalString(input.cwd),
@@ -2054,28 +1741,10 @@ function parseTask(input) {
2054
1741
  if (!Array.isArray(input.command) || !input.command.every((item) => typeof item === "string")) {
2055
1742
  throw new Error(`workbench task ${id} command must be an array of strings`);
2056
1743
  }
2057
- return { id, enabled, kind, command: input.command, schedule };
1744
+ return { id, enabled, kind, command: input.command, history };
2058
1745
  }
2059
1746
  throw new Error(`Invalid workbench task kind for ${id}: ${kind}`);
2060
1747
  }
2061
- function parseReactiveRule(input) {
2062
- if (!isRecord(input)) {
2063
- throw new Error("workbench.reactive entries must be tables");
2064
- }
2065
- const id = requiredTaskId(input.id);
2066
- const kind = requiredString(input.kind, `workbench reactive ${id} kind`);
2067
- if (kind !== "skill") {
2068
- throw new Error(`Invalid workbench reactive kind for ${id}: ${kind}`);
2069
- }
2070
- return {
2071
- id,
2072
- enabled: input.enabled === undefined ? true : booleanValue(input.enabled, `workbench reactive ${id} enabled`),
2073
- task: requiredString(input.task, `workbench reactive ${id} task`),
2074
- consecutiveFailuresGte: positiveInteger(input.consecutive_failures_gte, `workbench reactive ${id} consecutive_failures_gte`),
2075
- kind,
2076
- skill: requiredString(input.skill, `workbench reactive ${id} skill`),
2077
- };
2078
- }
2079
1748
  function requiredTaskId(value) {
2080
1749
  const id = requiredString(value, "workbench task id");
2081
1750
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(id)) {
@@ -2116,6 +1785,15 @@ function booleanValue(value, label) {
2116
1785
  }
2117
1786
  return value;
2118
1787
  }
1788
+ function workbenchTaskHistoryValue(value, label) {
1789
+ if (value === undefined) {
1790
+ return "full";
1791
+ }
1792
+ if (value === "full" || value === "latest") {
1793
+ return value;
1794
+ }
1795
+ throw new Error(`${label} must be full or latest`);
1796
+ }
2119
1797
  function stringRecord(value) {
2120
1798
  if (!isRecord(value)) {
2121
1799
  return undefined;
@@ -2167,7 +1845,7 @@ function reasoningEffortValue(value, label) {
2167
1845
  }
2168
1846
  return undefined;
2169
1847
  }
2170
- function deferredDependencyStatusValue(value, label) {
1848
+ function dispatchDependencyStatusValue(value, label) {
2171
1849
  if (value === "completed" ||
2172
1850
  value === "failed" ||
2173
1851
  value === "canceled" ||
@@ -2179,7 +1857,7 @@ function deferredDependencyStatusValue(value, label) {
2179
1857
  }
2180
1858
  return undefined;
2181
1859
  }
2182
- function deferredRunStatus(value) {
1860
+ function dispatchRunStatus(value) {
2183
1861
  if (value === "pending" ||
2184
1862
  value === "running" ||
2185
1863
  value === "completed" ||
@@ -2187,13 +1865,13 @@ function deferredRunStatus(value) {
2187
1865
  value === "canceled") {
2188
1866
  return value;
2189
1867
  }
2190
- throw new Error(`Invalid deferred run status: ${String(value)}`);
1868
+ throw new Error(`Invalid dispatch run status: ${String(value)}`);
2191
1869
  }
2192
- function deferredAttemptStatus(value) {
1870
+ function dispatchAttemptStatus(value) {
2193
1871
  if (value === "running" || value === "completed" || value === "failed") {
2194
1872
  return value;
2195
1873
  }
2196
- throw new Error(`Invalid deferred run attempt status: ${String(value)}`);
1874
+ throw new Error(`Invalid dispatch run attempt status: ${String(value)}`);
2197
1875
  }
2198
1876
  function workbenchMode(value) {
2199
1877
  if (value === "local" || value === "actions") {
@@ -2201,12 +1879,6 @@ function workbenchMode(value) {
2201
1879
  }
2202
1880
  throw new Error(`Invalid workbench mode: ${String(value)}`);
2203
1881
  }
2204
- function positiveInteger(value, label) {
2205
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
2206
- throw new Error(`${label} must be a positive integer`);
2207
- }
2208
- return value;
2209
- }
2210
1882
  function isRecord(value) {
2211
1883
  return typeof value === "object" && value !== null && !Array.isArray(value);
2212
1884
  }