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.
- package/README.md +8 -6
- package/dist/cli/actions.d.ts +1 -1
- package/dist/cli/actions.d.ts.map +1 -1
- package/dist/cli/actions.js +1 -0
- package/dist/cli/actions.js.map +1 -1
- package/dist/cli/args.d.ts +11 -7
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +17 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.js +28 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/internal/feed/index.d.ts +26 -1
- package/dist/internal/feed/index.d.ts.map +1 -1
- package/dist/internal/feed/index.js +102 -1
- package/dist/internal/feed/index.js.map +1 -1
- package/dist/internal/package.json +5 -0
- package/dist/internal/workbench/fetch.js +1 -1
- package/dist/internal/workbench/fetch.js.map +1 -1
- package/dist/internal/workbench/workbench-runtime.d.ts +6 -53
- package/dist/internal/workbench/workbench-runtime.d.ts.map +1 -1
- package/dist/internal/workbench/workbench-runtime.js +65 -379
- package/dist/internal/workbench/workbench-runtime.js.map +1 -1
- package/dist/internal/workbench/workflow.d.ts.map +1 -1
- package/dist/internal/workbench/workflow.js +106 -11
- package/dist/internal/workbench/workflow.js.map +1 -1
- package/docs/pages/components/cli.md +1 -1
- package/docs/pages/guides/feed-to-workflow.md +14 -24
- package/docs/pages/guides/local-scheduled-workbench.md +50 -25
- package/docs/pages/guides/repository-autonomy.md +22 -14
- package/docs/pages/index.md +5 -5
- package/docs/pages/primitives/feed.md +16 -7
- package/docs/pages/primitives/workbench.md +15 -17
- package/docs/pages/primitives/workflow.md +6 -5
- package/docs/pages/reference/packages.md +3 -1
- 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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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]
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
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
|
|
1873
|
-
return
|
|
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
|
|
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
|
-
|
|
2021
|
-
|
|
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`),
|
|
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
|
-
|
|
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,
|
|
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
|
}
|