@workbench-ai/workbench 0.0.75 → 0.0.76

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 (2) hide show
  1. package/dist/index.js +189 -119
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -914,10 +914,7 @@ async function handleInstall(parsed, io) {
914
914
  next: next,
915
915
  ...(parsed.flags["dry-run"] === true ? { dryRun: true } : {}),
916
916
  }, parsed, io, () => [
917
- parsed.flags["dry-run"] === true
918
- ? `Would install ${result.directoryName} to ${result.destination}: filesCopied=${result.filesCopied}`
919
- : `Installed ${result.directoryName}: ${result.result}`,
920
- ` machine\t${result.previous}\t${result.destination}`,
917
+ formatInstallOutcome(result, parsed.flags["dry-run"] === true),
921
918
  formatFanOut(fanout),
922
919
  ...(next ? [`next: ${next}`] : []),
923
920
  ].join("\n"));
@@ -1013,6 +1010,21 @@ function installNextCommand(fanout) {
1013
1010
  ? fanout.command
1014
1011
  : null;
1015
1012
  }
1013
+ function formatInstallOutcome(result, dryRun) {
1014
+ if (dryRun) {
1015
+ return `Would install ${result.directoryName} to ${result.destination} (${formatFileCount(result.filesCopied)}).`;
1016
+ }
1017
+ if (result.result === "unchanged") {
1018
+ return `Already installed ${result.directoryName} at ${result.destination} (unchanged).`;
1019
+ }
1020
+ const detail = result.previous === "overwritten"
1021
+ ? `overwrote existing copy, ${formatFileCount(result.filesCopied)}`
1022
+ : formatFileCount(result.filesCopied);
1023
+ return `Installed ${result.directoryName} to ${result.destination} (${detail}).`;
1024
+ }
1025
+ function formatFileCount(count) {
1026
+ return `${count} ${count === 1 ? "file" : "files"}`;
1027
+ }
1016
1028
  function fanOutToJson(fanout) {
1017
1029
  return {
1018
1030
  status: fanout.status,
@@ -1065,79 +1077,139 @@ function withTimeout(promise, timeoutMs) {
1065
1077
  async function startCloudExecution(command, parsed, io) {
1066
1078
  const root = dirFlag(parsed) ?? process.cwd();
1067
1079
  const showProgress = parsed.flags.json !== true;
1068
- const remote = await ensureCloudRemoteForExecution(root, parsed);
1069
- const source = parseWorkbenchInstallSource(remote.url);
1070
- if (!source) {
1071
- throw new WorkbenchCodedError("remote_invalid_url", `Workbench remote is not a Cloud skill URL: ${remote.url}`, {
1072
- remediation: "Run workbench publish to recreate the Workbench Cloud link.",
1073
- subject: { remote: remote.name, url: remote.url },
1074
- exitCode: 2,
1075
- });
1076
- }
1077
- const token = await workbenchCloudToken({ baseUrl: source.baseUrl });
1078
- if (!token) {
1079
- throw new WorkbenchCodedError("auth_required", `workbench ${command} --cloud requires Workbench Cloud auth.`, {
1080
- remediation: `Run workbench login --base-url ${source.baseUrl}.`,
1081
- exitCode: 1,
1080
+ const interrupt = createCloudInterruptController(command, io, showProgress);
1081
+ try {
1082
+ writeCloudProgress(io, `workbench cloud: preparing hosted ${command}.`, showProgress);
1083
+ const remote = await cloudPreScheduleStep(command, interrupt, ensureCloudRemoteForExecution(root, parsed));
1084
+ const source = parseWorkbenchInstallSource(remote.url);
1085
+ if (!source) {
1086
+ throw new WorkbenchCodedError("remote_invalid_url", `Workbench remote is not a Cloud skill URL: ${remote.url}`, {
1087
+ remediation: "Run workbench publish to recreate the Workbench Cloud link.",
1088
+ subject: { remote: remote.name, url: remote.url },
1089
+ exitCode: 2,
1090
+ });
1091
+ }
1092
+ const token = await workbenchCloudToken({ baseUrl: source.baseUrl });
1093
+ if (!token) {
1094
+ throw new WorkbenchCodedError("auth_required", `workbench ${command} --cloud requires Workbench Cloud auth.`, {
1095
+ remediation: `Run workbench login --base-url ${source.baseUrl}.`,
1096
+ exitCode: 1,
1097
+ });
1098
+ }
1099
+ const core = { dir: root, authToken: token };
1100
+ writeCloudProgress(io, "workbench cloud: preparing current source.", showProgress);
1101
+ const request = command === "eval"
1102
+ ? await cloudPreScheduleStep(command, interrupt, prepareWorkbenchCloudEvalRequest({
1103
+ ...core,
1104
+ skill: stringFlag(parsed, "skills"),
1105
+ agent: stringFlag(parsed, "agents"),
1106
+ samples: intFlag(parsed, "samples"),
1107
+ }))
1108
+ : await cloudPreScheduleStep(command, interrupt, prepareWorkbenchCloudImproveRequest({
1109
+ ...core,
1110
+ skill: stringFlag(parsed, "skills"),
1111
+ agent: stringFlag(parsed, "agents"),
1112
+ samples: intFlag(parsed, "samples"),
1113
+ budget: intFlag(parsed, "budget"),
1114
+ }));
1115
+ writeCloudProgress(io, "workbench cloud: syncing source to cloud.", showProgress);
1116
+ const syncBefore = await cloudPreScheduleStep(command, interrupt, syncWorkbenchRemote({ ...core, remote: remote.name }));
1117
+ writeCloudProgress(io, `workbench cloud: scheduling hosted ${command}.`, showProgress);
1118
+ const skillId = await cloudPreScheduleStep(command, interrupt, resolveCloudSkillId(source));
1119
+ const response = await cloudPreScheduleStep(command, interrupt, apiRequest(`/api/workbench/skills/${encodeURIComponent(skillId)}${command === "improve" ? "/improve" : "/runs"}`, { method: "POST", body: cloudExecutionRequestBody(command, request) }, source.baseUrl));
1120
+ const runs = response.runs ?? [];
1121
+ if (runs.length === 0) {
1122
+ throw new WorkbenchCodedError("cloud_run_missing", `Workbench Cloud did not return a run for ${command}.`, {
1123
+ retryable: true,
1124
+ remediation: "Run workbench log --runs.",
1125
+ subject: { remote: remote.name, skillId },
1126
+ exitCode: 1,
1127
+ });
1128
+ }
1129
+ const initialRunIds = runs.map((run) => run.id);
1130
+ interrupt.setRunIds(initialRunIds);
1131
+ const syncAfterSchedule = await syncWorkbenchRemote({ ...core, remote: remote.name });
1132
+ writeCloudProgress(io, `workbench cloud: scheduled hosted ${command} on ${remote.url} (${formatCloudRunStatuses(runs)}).`, showProgress);
1133
+ writeCloudProgress(io, `workbench cloud: waiting for terminal status; press Ctrl-C to detach and resume with workbench show ${displayRef(initialRunIds[0] ?? "run")}.`, showProgress);
1134
+ const completed = await waitForCloudRuns({
1135
+ command,
1136
+ core,
1137
+ interrupt,
1138
+ io,
1139
+ progress: showProgress,
1140
+ remote,
1141
+ runs,
1142
+ source,
1143
+ skillId,
1144
+ initialSync: syncAfterSchedule,
1082
1145
  });
1146
+ return {
1147
+ core,
1148
+ remote,
1149
+ skillId,
1150
+ initialRunIds,
1151
+ runs: completed.runs,
1152
+ ...(completed.detached ? { detached: true } : {}),
1153
+ startVersionId: request.versionId,
1154
+ source,
1155
+ sync: {
1156
+ before: { pushed: syncBefore.pushed, pulled: syncBefore.pulled, upToDate: syncBefore.upToDate },
1157
+ after: { pushed: completed.sync.pushed, pulled: completed.sync.pulled, upToDate: completed.sync.upToDate },
1158
+ },
1159
+ };
1083
1160
  }
1084
- const core = { dir: root, authToken: token };
1085
- const request = command === "eval"
1086
- ? await prepareWorkbenchCloudEvalRequest({
1087
- ...core,
1088
- skill: stringFlag(parsed, "skills"),
1089
- agent: stringFlag(parsed, "agents"),
1090
- samples: intFlag(parsed, "samples"),
1091
- })
1092
- : await prepareWorkbenchCloudImproveRequest({
1093
- ...core,
1094
- skill: stringFlag(parsed, "skills"),
1095
- agent: stringFlag(parsed, "agents"),
1096
- samples: intFlag(parsed, "samples"),
1097
- budget: intFlag(parsed, "budget"),
1098
- });
1099
- const syncBefore = await syncWorkbenchRemote({ ...core, remote: remote.name });
1100
- const skillId = await resolveCloudSkillId(source);
1101
- const response = await apiRequest(`/api/workbench/skills/${encodeURIComponent(skillId)}${command === "improve" ? "/improve" : "/runs"}`, { method: "POST", body: cloudExecutionRequestBody(command, request) }, source.baseUrl);
1102
- const runs = response.runs ?? [];
1103
- if (runs.length === 0) {
1104
- throw new WorkbenchCodedError("cloud_run_missing", `Workbench Cloud did not return a run for ${command}.`, {
1105
- retryable: true,
1106
- remediation: "Run workbench log --runs.",
1107
- subject: { remote: remote.name, skillId },
1108
- exitCode: 1,
1109
- });
1161
+ finally {
1162
+ interrupt.dispose();
1110
1163
  }
1111
- const syncAfterSchedule = await syncWorkbenchRemote({ ...core, remote: remote.name });
1112
- const initialRunIds = runs.map((run) => run.id);
1113
- writeCloudProgress(io, `workbench cloud: scheduled hosted ${command} on ${remote.url} (${formatCloudRunStatuses(runs)}).`, showProgress);
1114
- writeCloudProgress(io, `workbench cloud: waiting for terminal status; press Ctrl-C to detach and resume with workbench show ${displayRef(initialRunIds[0] ?? "run")}.`, showProgress);
1115
- const completed = await waitForCloudRuns({
1116
- command,
1117
- core,
1118
- io,
1119
- progress: showProgress,
1120
- remote,
1121
- runs,
1122
- source,
1123
- skillId,
1124
- initialSync: syncAfterSchedule,
1164
+ }
1165
+ function createCloudInterruptController(command, io, progress) {
1166
+ let interrupted = false;
1167
+ let runIds = [];
1168
+ let resolveSignal = () => undefined;
1169
+ const signal = new Promise((resolve) => {
1170
+ resolveSignal = resolve;
1125
1171
  });
1172
+ const onSigint = () => {
1173
+ interrupted = true;
1174
+ if (runIds.length > 0) {
1175
+ writeCloudProgress(io, `workbench cloud: detaching from hosted ${command} (${runIds.map(displayRef).join(", ")}).`, progress);
1176
+ }
1177
+ resolveSignal();
1178
+ };
1179
+ process.once("SIGINT", onSigint);
1126
1180
  return {
1127
- core,
1128
- remote,
1129
- skillId,
1130
- initialRunIds,
1131
- runs: completed.runs,
1132
- ...(completed.detached ? { detached: true } : {}),
1133
- startVersionId: request.versionId,
1134
- source,
1135
- sync: {
1136
- before: { pushed: syncBefore.pushed, pulled: syncBefore.pulled, upToDate: syncBefore.upToDate },
1137
- after: { pushed: completed.sync.pushed, pulled: completed.sync.pulled, upToDate: completed.sync.upToDate },
1181
+ signal,
1182
+ get interrupted() {
1183
+ return interrupted;
1184
+ },
1185
+ get runIds() {
1186
+ return runIds;
1187
+ },
1188
+ setRunIds(nextRunIds) {
1189
+ runIds = [...nextRunIds];
1190
+ },
1191
+ dispose() {
1192
+ process.off("SIGINT", onSigint);
1138
1193
  },
1139
1194
  };
1140
1195
  }
1196
+ async function cloudPreScheduleStep(command, interrupt, step) {
1197
+ if (interrupt.interrupted) {
1198
+ throw cloudCanceledBeforeRunIdError(command);
1199
+ }
1200
+ return await Promise.race([
1201
+ step,
1202
+ interrupt.signal.then(() => {
1203
+ throw cloudCanceledBeforeRunIdError(command);
1204
+ }),
1205
+ ]);
1206
+ }
1207
+ function cloudCanceledBeforeRunIdError(command) {
1208
+ return new WorkbenchCodedError("cloud_canceled", `Hosted ${command} was canceled before Workbench Cloud returned a run id.`, {
1209
+ remediation: `Run workbench ${command} --cloud.`,
1210
+ exitCode: 130,
1211
+ });
1212
+ }
1141
1213
  async function waitForCloudRuns(input) {
1142
1214
  const runIds = input.runs
1143
1215
  .map((run) => run.id)
@@ -1154,61 +1226,50 @@ async function waitForCloudRuns(input) {
1154
1226
  const pollIntervalMs = positiveIntEnv("WORKBENCH_CLOUD_RUN_POLL_INTERVAL_MS") ?? CLOUD_RUN_POLL_INTERVAL_MS;
1155
1227
  const deadline = Date.now() + timeoutMs;
1156
1228
  let runs = [...input.runs];
1157
- let interrupted = false;
1158
1229
  const startedAtMs = Date.now();
1159
1230
  let lastProgressAtMs = startedAtMs;
1160
- const onSigint = () => {
1161
- interrupted = true;
1162
- writeCloudProgress(input.io, `workbench cloud: detaching from hosted ${input.command} (${runIds.map(displayRef).join(", ")}).`, input.progress);
1163
- };
1164
- process.once("SIGINT", onSigint);
1165
1231
  const seenStatuses = new Map();
1166
- try {
1167
- while (true) {
1168
- runs = await fetchCloudRuns(input.source.baseUrl, input.skillId, runIds, runs);
1169
- let wroteProgress = false;
1170
- const nowMs = Date.now();
1171
- for (const run of runs) {
1172
- const previous = seenStatuses.get(run.id);
1173
- if (previous !== run.status) {
1174
- seenStatuses.set(run.id, run.status);
1175
- writeCloudProgress(input.io, `workbench cloud: ${formatCloudRunState(run, startedAtMs, nowMs)}.`, input.progress);
1176
- wroteProgress = input.progress || wroteProgress;
1177
- }
1178
- }
1179
- if (runs.length === runIds.length && runs.every(isTerminalRun)) {
1180
- sync = await syncWorkbenchRemote({ ...input.core, remote: input.remote.name });
1181
- return { runs, sync };
1182
- }
1183
- if (wroteProgress) {
1184
- lastProgressAtMs = nowMs;
1185
- }
1186
- else if (input.progress && nowMs - lastProgressAtMs >= 60_000) {
1187
- writeCloudProgress(input.io, `workbench cloud: still waiting (${formatCloudRunStates(runs, startedAtMs, nowMs)}).`);
1188
- lastProgressAtMs = nowMs;
1189
- }
1190
- if (interrupted) {
1191
- return { runs, sync, detached: true };
1192
- }
1193
- if (Date.now() >= deadline) {
1194
- throw new WorkbenchCodedError("cloud_run_pending", "Hosted Workbench run is still running.", {
1195
- retryable: true,
1196
- remediation: runIds[0] ? `Run workbench show ${runIds[0]}.` : "Run workbench log --runs.",
1197
- subject: {
1198
- runIds,
1199
- statuses: Object.fromEntries(runs.map((run) => [run.id, run.status])),
1200
- },
1201
- exitCode: 1,
1202
- });
1203
- }
1204
- await sleep(pollIntervalMs);
1205
- if (interrupted) {
1206
- return { runs, sync, detached: true };
1232
+ while (true) {
1233
+ runs = await fetchCloudRuns(input.source.baseUrl, input.skillId, runIds, runs);
1234
+ let wroteProgress = false;
1235
+ const nowMs = Date.now();
1236
+ for (const run of runs) {
1237
+ const previous = seenStatuses.get(run.id);
1238
+ if (previous !== run.status) {
1239
+ seenStatuses.set(run.id, run.status);
1240
+ writeCloudProgress(input.io, `workbench cloud: ${formatCloudRunState(run, startedAtMs, nowMs)}.`, input.progress);
1241
+ wroteProgress = input.progress || wroteProgress;
1207
1242
  }
1208
1243
  }
1209
- }
1210
- finally {
1211
- process.off("SIGINT", onSigint);
1244
+ if (runs.length === runIds.length && runs.every(isTerminalRun)) {
1245
+ sync = await syncWorkbenchRemote({ ...input.core, remote: input.remote.name });
1246
+ return { runs, sync };
1247
+ }
1248
+ if (wroteProgress) {
1249
+ lastProgressAtMs = nowMs;
1250
+ }
1251
+ else if (input.progress && nowMs - lastProgressAtMs >= 60_000) {
1252
+ writeCloudProgress(input.io, `workbench cloud: still waiting (${formatCloudRunStates(runs, startedAtMs, nowMs)}).`);
1253
+ lastProgressAtMs = nowMs;
1254
+ }
1255
+ if (input.interrupt.interrupted) {
1256
+ return { runs, sync, detached: true };
1257
+ }
1258
+ if (Date.now() >= deadline) {
1259
+ throw new WorkbenchCodedError("cloud_run_pending", "Hosted Workbench run is still running.", {
1260
+ retryable: true,
1261
+ remediation: runIds[0] ? `Run workbench show ${runIds[0]}.` : "Run workbench log --runs.",
1262
+ subject: {
1263
+ runIds,
1264
+ statuses: Object.fromEntries(runs.map((run) => [run.id, run.status])),
1265
+ },
1266
+ exitCode: 1,
1267
+ });
1268
+ }
1269
+ await Promise.race([sleep(pollIntervalMs), input.interrupt.signal]);
1270
+ if (input.interrupt.interrupted) {
1271
+ return { runs, sync, detached: true };
1272
+ }
1212
1273
  }
1213
1274
  }
1214
1275
  async function fetchCloudRuns(baseUrl, skillId, runIds, fallback) {
@@ -2735,6 +2796,15 @@ async function statusWithCausalNext(status, auth, core, machine) {
2735
2796
  if (unpublishedCloudRemote) {
2736
2797
  return { ...status, next: "workbench publish" };
2737
2798
  }
2799
+ const currentVersionId = status.project.currentVersionId ?? snapshot?.status.currentVersionId ?? snapshot?.refs.current;
2800
+ const stalePublishedCloudRemote = status.remotes.find((remote) => remote.kind === "workbench-cloud" &&
2801
+ remote.publication.status === "published" &&
2802
+ remote.sync.status === "up_to_date" &&
2803
+ currentVersionId !== undefined &&
2804
+ remote.publication.versionId !== currentVersionId);
2805
+ if (canPublish && stalePublishedCloudRemote) {
2806
+ return { ...status, next: "workbench publish" };
2807
+ }
2738
2808
  const publishedCloudRemote = status.remotes.find((remote) => remote.kind === "workbench-cloud" &&
2739
2809
  remote.publication.status === "published" &&
2740
2810
  Boolean(remote.publication.installUrl));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-ai/workbench",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/workbench-ai/workbench.git",
@@ -22,10 +22,10 @@
22
22
  "dependencies": {
23
23
  "skills": "1.5.11",
24
24
  "yaml": "^2.8.2",
25
- "@workbench-ai/workbench-built-in-adapters": "0.0.75",
26
- "@workbench-ai/workbench-contract": "0.0.75",
27
- "@workbench-ai/workbench-core": "0.0.75",
28
- "@workbench-ai/workbench-protocol": "0.0.75"
25
+ "@workbench-ai/workbench-built-in-adapters": "0.0.76",
26
+ "@workbench-ai/workbench-core": "0.0.76",
27
+ "@workbench-ai/workbench-contract": "0.0.76",
28
+ "@workbench-ai/workbench-protocol": "0.0.76"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@tailwindcss/postcss": "^4.2.2",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^19.2.0",
37
37
  "typescript": "^5.9.2",
38
38
  "vitest": "^3.2.4",
39
- "@workbench-ai/workbench-ui": "0.0.75"
39
+ "@workbench-ai/workbench-ui": "0.0.76"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "rm -rf dist && tsc -p tsconfig.json && chmod 755 dist/workbench.js && node ./scripts/build-dev-open-assets.mjs",