codex-toys 0.140.12 → 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 (38) hide show
  1. package/README.md +8 -6
  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 +11 -7
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +17 -10
  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 +2 -1
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/index.js +28 -18
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/internal/feed/index.d.ts +26 -1
  16. package/dist/internal/feed/index.d.ts.map +1 -1
  17. package/dist/internal/feed/index.js +102 -1
  18. package/dist/internal/feed/index.js.map +1 -1
  19. package/dist/internal/package.json +5 -0
  20. package/dist/internal/workbench/fetch.js +1 -1
  21. package/dist/internal/workbench/fetch.js.map +1 -1
  22. package/dist/internal/workbench/workbench-runtime.d.ts +6 -53
  23. package/dist/internal/workbench/workbench-runtime.d.ts.map +1 -1
  24. package/dist/internal/workbench/workbench-runtime.js +65 -379
  25. package/dist/internal/workbench/workbench-runtime.js.map +1 -1
  26. package/dist/internal/workbench/workflow.d.ts.map +1 -1
  27. package/dist/internal/workbench/workflow.js +106 -11
  28. package/dist/internal/workbench/workflow.js.map +1 -1
  29. package/docs/pages/components/cli.md +1 -1
  30. package/docs/pages/guides/feed-to-workflow.md +14 -24
  31. package/docs/pages/guides/local-scheduled-workbench.md +50 -25
  32. package/docs/pages/guides/repository-autonomy.md +22 -14
  33. package/docs/pages/index.md +5 -5
  34. package/docs/pages/primitives/feed.md +16 -7
  35. package/docs/pages/primitives/workbench.md +15 -17
  36. package/docs/pages/primitives/workflow.md +6 -5
  37. package/docs/pages/reference/packages.md +3 -1
  38. package/package.json +1 -1
@@ -87,7 +87,9 @@ export async function loadWorkbenchConfig(context) {
87
87
  const workbench = isRecord(parsed.workbench) ? parsed.workbench : undefined;
88
88
  const surfacesInput = Array.isArray(workbench?.surfaces) ? workbench.surfaces : [];
89
89
  const tasksInput = Array.isArray(workbench?.tasks) ? workbench.tasks : [];
90
- 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
+ }
91
93
  const tasks = tasksInput.map(parseTask);
92
94
  const ids = new Set();
93
95
  for (const task of tasks) {
@@ -100,11 +102,10 @@ export async function loadWorkbenchConfig(context) {
100
102
  name: stringValue(workbench?.name, path.basename(context.repoRoot)),
101
103
  surfaces: surfacesInput.map(parseSurface),
102
104
  tasks,
103
- reactive: reactiveInput.map(parseReactiveRule),
104
105
  path: context.configPath,
105
106
  };
106
107
  }
107
- export async function collectWorkbenchDoctorInfo(context, options = {}) {
108
+ export async function collectWorkbenchDoctorInfo(context) {
108
109
  let config;
109
110
  let configExists = true;
110
111
  try {
@@ -129,10 +130,6 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
129
130
  .toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
130
131
  const dispatchDueFlags = await Promise.all(dispatchRuns.map(async (intent) => await isDispatchIntentDue(context, intent, now)));
131
132
  const failingCount = countFailingTasks(config?.tasks ?? [], runs);
132
- const includeRunner = options.includeRunner === true || options.runnerProbe !== undefined;
133
- const runner = includeRunner
134
- ? await collectWorkbenchRunnerInfo(context, config?.tasks ?? [], dispatchRuns, options.runnerProbe ?? runSystemctlUser)
135
- : undefined;
136
133
  return {
137
134
  mode: context.mode,
138
135
  requestedMode: context.requestedMode,
@@ -149,7 +146,6 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
149
146
  globalMemorySummaryExists: await exists(path.join(context.globalCodexHome, "memories", "memory_summary.md")),
150
147
  workbenchMemorySummaryExists: await exists(path.join(context.workbenchCodexHome, "memories", "memory_summary.md")),
151
148
  taskCount: config?.tasks.length ?? 0,
152
- dueCount: dueTasks(config?.tasks ?? [], runs, new Date()).length,
153
149
  failingCount,
154
150
  dispatchCount: dispatchRuns.length,
155
151
  dispatchDueCount: dispatchDueFlags.filter(Boolean).length,
@@ -157,7 +153,6 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
157
153
  dispatchFailedCount: dispatchRuns.filter((intent) => intent.status === "failed").length,
158
154
  latestRun,
159
155
  latestDispatchRun,
160
- runner,
161
156
  surfaces: config?.surfaces ?? [],
162
157
  errors: workbenchDoctorErrors(context),
163
158
  };
@@ -174,7 +169,7 @@ export function formatWorkbenchDoctorInfo(info) {
174
169
  ["actions state", info.actionsStateRoot],
175
170
  ["global memories", `${info.globalMemoryRoot}${info.globalMemorySummaryExists ? " (summary)" : ""}`],
176
171
  ["workbench memories", `${info.workbenchMemoryRoot}${info.workbenchMemorySummaryExists ? " (summary)" : ""}`],
177
- ["tasks", `${info.taskCount} configured, ${info.dueCount} due, ${info.failingCount} failing`],
172
+ ["tasks", `${info.taskCount} configured, ${info.failingCount} failing`],
178
173
  ["latest run", info.latestRun ? `${info.latestRun.status} ${info.latestRun.taskId} ${info.latestRun.finishedAt}` : "none"],
179
174
  [
180
175
  "dispatch runs",
@@ -186,11 +181,7 @@ export function formatWorkbenchDoctorInfo(info) {
186
181
  ? `${info.latestDispatchRun.status} ${info.latestDispatchRun.id} ${info.latestDispatchRun.updatedAt}`
187
182
  : "none",
188
183
  ],
189
- ["runner", formatWorkbenchRunnerInfo(info.runner)],
190
184
  ];
191
- if (info.runner?.warning) {
192
- rows.push(["runner warning", info.runner.warning]);
193
- }
194
185
  for (const error of info.errors) {
195
186
  rows.push(["error", error]);
196
187
  }
@@ -214,33 +205,6 @@ export async function scaffoldActionsWorkbench(options = {}) {
214
205
  files.push(await appendGitignoreEntries(workbenchRoot, actionsGitignoreEntries(), retiredActionsGitignoreEntries()));
215
206
  return { workbenchRoot, files };
216
207
  }
217
- export async function tickWorkbench(context, options) {
218
- await ensureStateDirs(context);
219
- const config = await loadWorkbenchConfig(context);
220
- const previousRuns = await readRuns(context);
221
- const previousIntents = await listDispatchRunIntents(context);
222
- const now = new Date();
223
- const due = dueTasks(config.tasks, previousRuns, now, previousIntents);
224
- const runs = [];
225
- for (const task of due) {
226
- await createScheduledWorkbenchTaskIntent(context, task, now);
227
- }
228
- const executions = await runDueDispatchRuns(context, options);
229
- for (const execution of executions.executions) {
230
- const workbenchRun = record(execution.output).workbenchRun;
231
- if (isWorkbenchRunRecord(workbenchRun)) {
232
- runs.push(workbenchRun);
233
- }
234
- }
235
- const allRuns = [...previousRuns, ...runs];
236
- for (const rule of config.reactive.filter((item) => item.enabled)) {
237
- const targets = config.tasks.filter((task) => rule.task === "*" ? true : task.id === rule.task);
238
- if (targets.some((task) => consecutiveFailures(task.id, allRuns) >= rule.consecutiveFailuresGte)) {
239
- runs.push(await runReactiveRule(context, rule));
240
- }
241
- }
242
- return { mode: context.mode, due: due.map((task) => task.id), runs };
243
- }
244
208
  export async function runWorkbenchTaskById(context, taskId, options) {
245
209
  await ensureStateDirs(context);
246
210
  const config = await loadWorkbenchConfig(context);
@@ -678,13 +642,13 @@ export async function pruneDispatchRunHistory(context, options) {
678
642
  async function runWorkbenchTask(context, config, task, options) {
679
643
  const startedAt = new Date().toISOString();
680
644
  const runId = workbenchRunId(task.id, startedAt);
681
- const outputPath = path.join(context.stateRoot, "outputs", `${runId}.json`);
645
+ const outputPath = workbenchTaskOutputPath(context, task, runId);
682
646
  try {
683
647
  let result;
684
648
  if (!task.enabled) {
685
649
  result = { skipped: "disabled" };
686
650
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "skipped", outputPath);
687
- await persistRun(context, run, result);
651
+ await persistRun(context, task, run, result);
688
652
  return run;
689
653
  }
690
654
  if (task.kind === "workflow") {
@@ -697,18 +661,23 @@ async function runWorkbenchTask(context, config, task, options) {
697
661
  result = await runSkill(task, context);
698
662
  }
699
663
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "completed", outputPath);
700
- await persistRun(context, run, result);
664
+ await persistRun(context, task, run, result);
701
665
  return run;
702
666
  }
703
667
  catch (error) {
704
668
  const run = runRecord(context, runId, task.id, task.kind, startedAt, "failed", outputPath, errorMessage(error));
705
- await persistRun(context, run, { error: errorMessage(error) });
669
+ await persistRun(context, task, run, { error: errorMessage(error) });
706
670
  return run;
707
671
  }
708
672
  }
709
673
  function workbenchRunId(taskId, startedAt) {
710
674
  return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${taskId}`;
711
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
+ }
712
681
  async function runWorkflowTask(context, config, task, runId, startedAt, options) {
713
682
  const target = await resolveWorkflowTarget(task.workflow, {
714
683
  cwd: context.repoRoot,
@@ -767,7 +736,6 @@ async function executeDispatchRunTarget(context, intent, options) {
767
736
  name: path.basename(context.repoRoot),
768
737
  surfaces: [],
769
738
  tasks: [],
770
- reactive: [],
771
739
  path: context.configPath,
772
740
  }));
773
741
  const result = await runWorkflowDispatchTarget(context, config, { ...intent, target }, options);
@@ -900,28 +868,6 @@ function dispatchWorkflowEvent(config, intent, startedAt) {
900
868
  function exhaustiveTarget(value) {
901
869
  throw new Error(`Unsupported dispatch run target: ${JSON.stringify(value)}`);
902
870
  }
903
- async function runReactiveRule(context, rule) {
904
- const startedAt = new Date().toISOString();
905
- const runId = `${startedAt.replace(/[:.]/g, "-")}-${rule.id}`;
906
- const outputPath = path.join(context.stateRoot, "outputs", `${runId}.json`);
907
- try {
908
- const result = await runSkill({
909
- id: rule.id,
910
- enabled: rule.enabled,
911
- kind: "skill",
912
- skill: rule.skill,
913
- var: `repair failures for ${rule.task}`,
914
- }, context);
915
- const run = runRecord(context, runId, rule.id, "reactive", startedAt, "completed", outputPath);
916
- await persistRun(context, run, result);
917
- return run;
918
- }
919
- catch (error) {
920
- const run = runRecord(context, runId, rule.id, "reactive", startedAt, "failed", outputPath, errorMessage(error));
921
- await persistRun(context, run, { error: errorMessage(error) });
922
- return run;
923
- }
924
- }
925
871
  async function runSkill(task, context) {
926
872
  const skillPath = path.join(context.runtimeCodexHome, "skills", task.skill, "SKILL.md");
927
873
  if (!await exists(skillPath)) {
@@ -976,18 +922,6 @@ async function runGit(cwd, args) {
976
922
  }
977
923
  return { stdout, stderr };
978
924
  }
979
- async function runSystemctlUser(args) {
980
- const proc = spawn("systemctl", ["--user", ...args]);
981
- const [stdout, stderr, exitCode] = await Promise.all([
982
- collectText(proc.stdout),
983
- collectText(proc.stderr),
984
- exitCodeFor(proc),
985
- ]);
986
- if (exitCode !== 0) {
987
- throw new Error(`systemctl --user ${args.join(" ")} failed (${exitCode}): ${stderr || stdout}`);
988
- }
989
- return stdout;
990
- }
991
925
  function collectText(stream) {
992
926
  return new Promise((resolve, reject) => {
993
927
  let output = "";
@@ -1009,16 +943,20 @@ function exitCodeFor(child) {
1009
943
  child.once("exit", (code) => resolve(code));
1010
944
  });
1011
945
  }
1012
- async function persistRun(context, run, output) {
946
+ async function persistRun(context, task, run, output) {
1013
947
  await ensureStateDirs(context);
1014
948
  if (run.outputPath) {
1015
949
  await writeFile(run.outputPath, `${JSON.stringify(output, null, 2)}\n`);
1016
950
  }
1017
- 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`);
1018
955
  await writeHealth(context, run);
1019
956
  }
1020
957
  async function writeHealth(context, run) {
1021
- 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));
1022
960
  const health = {
1023
961
  updatedAt: new Date().toISOString(),
1024
962
  latestRun: run,
@@ -1030,54 +968,33 @@ async function writeHealth(context, run) {
1030
968
  await writeFile(path.join(context.stateRoot, "health", "summary.json"), `${JSON.stringify(health, null, 2)}\n`);
1031
969
  }
1032
970
  async function readRuns(context) {
1033
- const dir = path.join(context.stateRoot, "runs");
1034
- try {
1035
- const entries = await readdir(dir);
1036
- const runs = [];
1037
- for (const entry of entries) {
1038
- if (!entry.endsWith(".json")) {
1039
- continue;
1040
- }
1041
- try {
1042
- const runPath = path.join(dir, entry);
1043
- const parsed = parseJsonText(await readFile(runPath, "utf8"), runPath);
1044
- if (parsed && typeof parsed.taskId === "string") {
1045
- 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
+ }
1046
986
  }
987
+ catch { }
1047
988
  }
1048
- catch { }
1049
989
  }
1050
- return runs;
1051
- }
1052
- catch {
1053
- return [];
1054
- }
1055
- }
1056
- async function createScheduledWorkbenchTaskIntent(context, task, now) {
1057
- try {
1058
- return await createDispatchRunIntent(context, {
1059
- id: scheduledDispatchRunId(task.id, now),
1060
- runAt: now.toISOString(),
1061
- target: {
1062
- kind: "workbench-task",
1063
- taskId: task.id,
1064
- },
1065
- createdBy: "workbench-schedule",
1066
- reason: `Scheduled workbench task ${task.id}`,
1067
- source: {
1068
- kind: "workbench-task-schedule",
1069
- taskId: task.id,
1070
- schedule: task.schedule,
1071
- date: now.toISOString().slice(0, 10),
1072
- },
1073
- });
1074
- }
1075
- catch (error) {
1076
- if (isAlreadyExistsError(error)) {
1077
- return undefined;
990
+ catch {
991
+ continue;
1078
992
  }
1079
- throw error;
1080
993
  }
994
+ return uniqueWorkbenchRuns(runs);
995
+ }
996
+ function uniqueWorkbenchRuns(runs) {
997
+ return [...new Map(runs.map((run) => [run.id, run])).values()];
1081
998
  }
1082
999
  async function readDispatchRunIntent(context, intentId) {
1083
1000
  const intentPath = dispatchIntentPath(context, intentId);
@@ -1496,49 +1413,6 @@ function normalizeDispatchRunCollectCursor(value, fallbackCursor) {
1496
1413
  lastIntentId: optionalString(input.lastIntentId),
1497
1414
  });
1498
1415
  }
1499
- function dueTasks(tasks, runs, now, intents = []) {
1500
- return tasks.filter((task) => {
1501
- if (!task.enabled) {
1502
- return false;
1503
- }
1504
- if (!task.schedule) {
1505
- return false;
1506
- }
1507
- return isScheduleDue(task.schedule, now) &&
1508
- !hasRunForDate(task.id, runs, now) &&
1509
- !hasScheduledIntentForDate(task.id, intents, now);
1510
- });
1511
- }
1512
- function isScheduleDue(schedule, now) {
1513
- const parts = schedule.trim().split(/\s+/);
1514
- if (parts.length !== 5) {
1515
- throw new Error(`Invalid workbench task schedule: ${schedule}`);
1516
- }
1517
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
1518
- return cronPartMatches(minute, now.getUTCMinutes()) &&
1519
- cronPartMatches(hour, now.getUTCHours()) &&
1520
- cronPartMatches(dayOfMonth, now.getUTCDate()) &&
1521
- cronPartMatches(month, now.getUTCMonth() + 1) &&
1522
- cronPartMatches(dayOfWeek, now.getUTCDay());
1523
- }
1524
- function cronPartMatches(part, value) {
1525
- if (!part || part === "*") {
1526
- return true;
1527
- }
1528
- return part.split(",").some((item) => Number.parseInt(item, 10) === value);
1529
- }
1530
- function hasRunForDate(taskId, runs, now) {
1531
- const today = now.toISOString().slice(0, 10);
1532
- return runs.some((run) => run.taskId === taskId && run.startedAt.startsWith(today));
1533
- }
1534
- function hasScheduledIntentForDate(taskId, intents, now) {
1535
- const expected = scheduledDispatchRunId(taskId, now);
1536
- return intents.some((intent) => intent.id === expected ||
1537
- (intent.target.kind === "workbench-task" &&
1538
- intent.target.taskId === taskId &&
1539
- intent.source?.kind === "workbench-task-schedule" &&
1540
- intent.source.date === now.toISOString().slice(0, 10)));
1541
- }
1542
1416
  async function isDispatchIntentDue(context, intent, now) {
1543
1417
  if (intent.status === "pending") {
1544
1418
  return intent.runAt <= now.toISOString() &&
@@ -1608,182 +1482,6 @@ function workbenchDoctorErrors(context) {
1608
1482
  }
1609
1483
  return [];
1610
1484
  }
1611
- async function collectWorkbenchRunnerInfo(context, tasks, dispatchRuns, probe) {
1612
- const workbenchRoot = path.resolve(context.repoRoot);
1613
- const hasScheduledWork = tasks.some((task) => task.enabled && task.schedule);
1614
- const hasPendingDispatchWork = dispatchRuns.some((intent) => intent.status === "pending" || intent.status === "running");
1615
- const hasRunnableWork = hasScheduledWork || hasPendingDispatchWork;
1616
- const base = {
1617
- kind: "systemd-user",
1618
- workbenchRoot,
1619
- candidates: [],
1620
- };
1621
- if (context.mode !== "local") {
1622
- return {
1623
- ...base,
1624
- status: "unsupported",
1625
- warning: hasRunnableWork
1626
- ? "Runner visibility currently checks local systemd user timers only."
1627
- : undefined,
1628
- };
1629
- }
1630
- if (os.platform() !== "linux") {
1631
- return {
1632
- ...base,
1633
- status: "unsupported",
1634
- warning: hasRunnableWork
1635
- ? "No local systemd user timer check is available on this platform."
1636
- : undefined,
1637
- };
1638
- }
1639
- try {
1640
- const timerRows = parseSystemdTimerRows(await probe(["list-timers", "--all", "--no-legend", "--no-pager"]));
1641
- const candidates = [];
1642
- for (const row of timerRows) {
1643
- const serviceShow = parseSystemdShow(await probe([
1644
- "show",
1645
- row.service,
1646
- "--property=ExecStart",
1647
- "--property=ActiveState",
1648
- "--property=UnitFileState",
1649
- "--no-pager",
1650
- ]));
1651
- const command = normalizeSystemdCommand(serviceShow.ExecStart);
1652
- if (!command.includes("codex-toys")) {
1653
- continue;
1654
- }
1655
- const runsWorkbenchTick = /\bworkbench\s+tick\b/.test(command);
1656
- const runsDispatchOnly = /\bworkbench\s+dispatch\s+run-due\b/.test(command);
1657
- if (!runsWorkbenchTick && !runsDispatchOnly) {
1658
- continue;
1659
- }
1660
- const timerShow = parseSystemdShow(await probe([
1661
- "show",
1662
- row.timer,
1663
- "--property=ActiveState",
1664
- "--property=UnitFileState",
1665
- "--property=NextElapseUSecRealtime",
1666
- "--property=LastTriggerUSec",
1667
- "--no-pager",
1668
- ]));
1669
- const runnerWorkbenchRoot = extractWorkbenchRootFromCommand(command);
1670
- const matchesWorkbench = runnerWorkbenchRoot
1671
- ? path.resolve(runnerWorkbenchRoot) === workbenchRoot
1672
- : command.includes(workbenchRoot);
1673
- candidates.push(compactUndefined({
1674
- kind: "systemd-user",
1675
- timer: row.timer,
1676
- service: row.service,
1677
- command,
1678
- activeState: serviceShow.ActiveState,
1679
- unitFileState: serviceShow.UnitFileState,
1680
- timerActiveState: timerShow.ActiveState,
1681
- timerUnitFileState: timerShow.UnitFileState,
1682
- nextTrigger: timerShow.NextElapseUSecRealtime,
1683
- lastTrigger: timerShow.LastTriggerUSec,
1684
- workbenchRoot: runnerWorkbenchRoot,
1685
- runsWorkbenchTick,
1686
- runsDispatchOnly,
1687
- matchesWorkbench,
1688
- }));
1689
- }
1690
- const selected = candidates.find((candidate) => candidate.matchesWorkbench &&
1691
- candidate.runsWorkbenchTick &&
1692
- candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
1693
- candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
1694
- candidate.runsWorkbenchTick) ?? candidates.find((candidate) => candidate.matchesWorkbench);
1695
- if (!selected) {
1696
- return {
1697
- ...base,
1698
- status: "missing",
1699
- candidates,
1700
- warning: hasRunnableWork
1701
- ? "No matching local runner was found; due work needs a manual tick or another scheduler."
1702
- : undefined,
1703
- };
1704
- }
1705
- const status = selected.timerActiveState === "active" ? "active" : "inactive";
1706
- return {
1707
- ...base,
1708
- status,
1709
- selected,
1710
- candidates,
1711
- warning: selected.runsDispatchOnly
1712
- ? "The matching runner only runs dispatch work; scheduled tasks need workbench tick."
1713
- : status === "inactive" && hasRunnableWork
1714
- ? "The matching local runner is not active; due work needs a manual tick or another scheduler."
1715
- : undefined,
1716
- };
1717
- }
1718
- catch (error) {
1719
- return {
1720
- ...base,
1721
- status: "unknown",
1722
- error: error instanceof Error ? error.message : String(error),
1723
- warning: hasRunnableWork
1724
- ? "Could not inspect local runner status; due work may need a manual tick or another scheduler."
1725
- : undefined,
1726
- };
1727
- }
1728
- }
1729
- function parseSystemdTimerRows(output) {
1730
- const rows = [];
1731
- for (const line of output.split(/\r?\n/)) {
1732
- const trimmed = line.trim();
1733
- if (!trimmed) {
1734
- continue;
1735
- }
1736
- const fields = trimmed.split(/\s+/);
1737
- const timerIndex = fields.findIndex((field) => field.endsWith(".timer"));
1738
- if (timerIndex < 0) {
1739
- continue;
1740
- }
1741
- const timer = fields[timerIndex];
1742
- const service = fields[timerIndex + 1];
1743
- if (!timer || !service?.endsWith(".service")) {
1744
- continue;
1745
- }
1746
- rows.push({ timer, service });
1747
- }
1748
- return rows;
1749
- }
1750
- function parseSystemdShow(output) {
1751
- const result = {};
1752
- for (const line of output.split(/\r?\n/)) {
1753
- const index = line.indexOf("=");
1754
- if (index <= 0) {
1755
- continue;
1756
- }
1757
- result[line.slice(0, index)] = line.slice(index + 1);
1758
- }
1759
- return result;
1760
- }
1761
- function normalizeSystemdCommand(value) {
1762
- return (value ?? "")
1763
- .replace(/\\x([0-9a-fA-F]{2})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
1764
- .replace(/\s+/g, " ")
1765
- .trim();
1766
- }
1767
- function extractWorkbenchRootFromCommand(command) {
1768
- const match = command.match(/--workbench-root(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s;]+))/);
1769
- return match?.[1] ?? match?.[2] ?? match?.[3];
1770
- }
1771
- function formatWorkbenchRunnerInfo(runner) {
1772
- if (!runner) {
1773
- return "not checked";
1774
- }
1775
- if (runner.selected) {
1776
- const command = runner.selected.runsWorkbenchTick ? "workbench tick" : "workbench dispatch run-due";
1777
- return `${runner.status} ${runner.selected.timer} -> ${runner.selected.service} (${command})`;
1778
- }
1779
- if (runner.status === "unsupported") {
1780
- return "unsupported";
1781
- }
1782
- if (runner.status === "unknown") {
1783
- return `unknown${runner.error ? ` (${runner.error})` : ""}`;
1784
- }
1785
- return `${runner.status}${runner.candidates.length > 0 ? ` (${runner.candidates.length} codex-toys runner candidates)` : ""}`;
1786
- }
1787
1485
  function consecutiveFailures(taskId, runs) {
1788
1486
  let count = 0;
1789
1487
  for (const run of runs.filter((item) => item.taskId === taskId).sort((a, b) => b.startedAt.localeCompare(a.startedAt))) {
@@ -1808,7 +1506,7 @@ function runRecord(context, id, taskId, kind, startedAt, status, outputPath, err
1808
1506
  };
1809
1507
  }
1810
1508
  async function ensureStateDirs(context) {
1811
- for (const name of ["state", "runs", "outputs", "health"]) {
1509
+ for (const name of ["state", "runs", "outputs", "latest-runs", "latest-outputs", "health"]) {
1812
1510
  await mkdir(path.join(context.stateRoot, name), { recursive: true });
1813
1511
  }
1814
1512
  }
@@ -1869,8 +1567,11 @@ function dispatchRetryRunId(originalIntentId, createdAt) {
1869
1567
  function dispatchAttemptId(intentId, startedAt) {
1870
1568
  return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${safeFileSegment(intentId).slice(0, 48)}`;
1871
1569
  }
1872
- function scheduledDispatchRunId(taskId, now) {
1873
- 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");
1874
1575
  }
1875
1576
  function safeFileSegment(value) {
1876
1577
  const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1974,7 +1675,7 @@ function actionsWorkflowTemplate(provider, runnerImage) {
1974
1675
  else {
1975
1676
  lines.push(` - uses: ${setupNode}`, " with:", " node-version: 24", " - run: npm install -g vite-plus", " - run: vp dlx codex-toys actions prepare-auth");
1976
1677
  }
1977
- 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 tick --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", "");
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", "");
1978
1679
  return lines.join("\n");
1979
1680
  }
1980
1681
  function actionsGitignoreEntries() {
@@ -2017,12 +1718,12 @@ function parseTask(input) {
2017
1718
  const id = requiredTaskId(input.id);
2018
1719
  const enabled = input.enabled === undefined ? true : booleanValue(input.enabled, `workbench task ${id} enabled`);
2019
1720
  const kind = requiredString(input.kind, `workbench task ${id} kind`);
2020
- const schedule = optionalString(input.schedule);
2021
- if (schedule) {
2022
- 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`);
2023
1723
  }
1724
+ const history = workbenchTaskHistoryValue(input.history, `workbench task ${id} history`);
2024
1725
  if (kind === "skill") {
2025
- 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) };
2026
1727
  }
2027
1728
  if (kind === "workflow") {
2028
1729
  return {
@@ -2030,7 +1731,7 @@ function parseTask(input) {
2030
1731
  enabled,
2031
1732
  kind,
2032
1733
  workflow: requiredString(input.workflow, `workbench task ${id} workflow`),
2033
- schedule,
1734
+ history,
2034
1735
  event: isRecord(input.event) ? input.event : undefined,
2035
1736
  prompt: optionalString(input.prompt),
2036
1737
  cwd: optionalString(input.cwd),
@@ -2040,28 +1741,10 @@ function parseTask(input) {
2040
1741
  if (!Array.isArray(input.command) || !input.command.every((item) => typeof item === "string")) {
2041
1742
  throw new Error(`workbench task ${id} command must be an array of strings`);
2042
1743
  }
2043
- return { id, enabled, kind, command: input.command, schedule };
1744
+ return { id, enabled, kind, command: input.command, history };
2044
1745
  }
2045
1746
  throw new Error(`Invalid workbench task kind for ${id}: ${kind}`);
2046
1747
  }
2047
- function parseReactiveRule(input) {
2048
- if (!isRecord(input)) {
2049
- throw new Error("workbench.reactive entries must be tables");
2050
- }
2051
- const id = requiredTaskId(input.id);
2052
- const kind = requiredString(input.kind, `workbench reactive ${id} kind`);
2053
- if (kind !== "skill") {
2054
- throw new Error(`Invalid workbench reactive kind for ${id}: ${kind}`);
2055
- }
2056
- return {
2057
- id,
2058
- enabled: input.enabled === undefined ? true : booleanValue(input.enabled, `workbench reactive ${id} enabled`),
2059
- task: requiredString(input.task, `workbench reactive ${id} task`),
2060
- consecutiveFailuresGte: positiveInteger(input.consecutive_failures_gte, `workbench reactive ${id} consecutive_failures_gte`),
2061
- kind,
2062
- skill: requiredString(input.skill, `workbench reactive ${id} skill`),
2063
- };
2064
- }
2065
1748
  function requiredTaskId(value) {
2066
1749
  const id = requiredString(value, "workbench task id");
2067
1750
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(id)) {
@@ -2102,6 +1785,15 @@ function booleanValue(value, label) {
2102
1785
  }
2103
1786
  return value;
2104
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
+ }
2105
1797
  function stringRecord(value) {
2106
1798
  if (!isRecord(value)) {
2107
1799
  return undefined;
@@ -2187,12 +1879,6 @@ function workbenchMode(value) {
2187
1879
  }
2188
1880
  throw new Error(`Invalid workbench mode: ${String(value)}`);
2189
1881
  }
2190
- function positiveInteger(value, label) {
2191
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
2192
- throw new Error(`${label} must be a positive integer`);
2193
- }
2194
- return value;
2195
- }
2196
1882
  function isRecord(value) {
2197
1883
  return typeof value === "object" && value !== null && !Array.isArray(value);
2198
1884
  }