@pasko70/pibo 3.0.2 → 3.1.1

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 (72) hide show
  1. package/compute-image/Dockerfile +47 -0
  2. package/compute-image/Dockerfile.dockerignore +5 -0
  3. package/dist/agent-runtime/contract.js +78 -28
  4. package/dist/agent-runtime/portable-history.js +19 -9
  5. package/dist/agent-runtime/registry.js +8 -2
  6. package/dist/agent-runtime/routed-session.js +12 -6
  7. package/dist/agent-runtimes/pi/routed-session.js +10 -4
  8. package/dist/apps/chat/agent-store.js +55 -3
  9. package/dist/apps/chat/data/project-service.js +119 -15
  10. package/dist/apps/chat/data/room-service.js +21 -0
  11. package/dist/apps/chat/loop-api.js +10 -2
  12. package/dist/apps/chat/web-app.js +84 -25
  13. package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
  18. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  19. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  20. package/dist/apps/chat-ui/index.html +2 -2
  21. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  22. package/dist/apps/chat-vscode-web/index.html +1 -1
  23. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  24. package/dist/compute/cli.js +35 -16
  25. package/dist/compute/docker.js +341 -94
  26. package/dist/config/config.js +3 -0
  27. package/dist/core/output-render-sequence.js +148 -1
  28. package/dist/core/session-router.js +33 -105
  29. package/dist/data/ingest-service.js +14 -6
  30. package/dist/data/pibo-store.js +8 -1
  31. package/dist/data/schema.js +135 -5
  32. package/dist/debug/agents.js +91 -89
  33. package/dist/debug/index.js +2 -0
  34. package/dist/debug/pty.js +127 -48
  35. package/dist/gateway/cli.js +4 -0
  36. package/dist/loops/channel.js +3 -1
  37. package/dist/loops/cli.js +2 -1
  38. package/dist/loops/service.js +90 -11
  39. package/dist/loops/store.js +100 -74
  40. package/dist/mcp/agent-context.js +13 -26
  41. package/dist/mcp/commands/info.js +3 -1
  42. package/dist/mcp/config-command.js +46 -2
  43. package/dist/mcp/config.js +18 -4
  44. package/dist/plugins/context-files-store.js +6 -6
  45. package/dist/previews/base-url.js +34 -0
  46. package/dist/previews/config.js +2 -32
  47. package/dist/ralph/store.js +5 -0
  48. package/dist/reliability/store.js +174 -111
  49. package/dist/session-ui/terminalRows.js +2 -1
  50. package/dist/sessions/pibo-data-store.js +81 -0
  51. package/dist/subagents/observation-query.js +121 -0
  52. package/dist/tools/agent-browser-leases.js +58 -23
  53. package/dist/tools/agent-browser-wrapper.js +1 -1
  54. package/dist/tools/browser-use-leases.js +129 -16
  55. package/dist/tools/browser-use-wrapper.js +1 -1
  56. package/dist/tools/index.js +27 -11
  57. package/dist/tools/python-runtime.js +10 -3
  58. package/dist/tools/registry.js +1 -1
  59. package/dist/tools/runtime/node-backend.js +16 -5
  60. package/dist/tools/runtime/node-worker-source.js +112 -33
  61. package/dist/tools/runtime/python-backend.js +16 -5
  62. package/dist/tools/runtime/python-worker-source.js +167 -16
  63. package/dist/tools/runtime/registry.js +23 -10
  64. package/dist/user-skills/store.js +0 -1
  65. package/npm-shrinkwrap.json +9 -2
  66. package/package.json +8 -2
  67. package/scripts/docker-entrypoint.sh +46 -0
  68. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  69. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  70. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  71. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  72. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-U-MSErGa.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-JvrPUGvI.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-SluZr_-r.css">
10
10
  </head>
11
11
  <body>
@@ -5,10 +5,23 @@ import { buildCompactTerminalRows, buildSlashCommandCatalog, buildTerminalStatus
5
5
  import { InkTerminalView } from "./InkTerminalView.js";
6
6
  import { isExpandableTerminalRow } from "./InkTerminalRow.js";
7
7
  const IDENTIFIER_PATTERN = /^(?:user|room|ps|pi|entry|evt|run|create|provider|model|agent)[_:]/i;
8
+ export async function runInkPickerActivation(guard, picker, activate) {
9
+ if (guard.inFlight || guard.lastPicker === picker)
10
+ return undefined;
11
+ guard.inFlight = true;
12
+ guard.lastPicker = picker;
13
+ try {
14
+ return await activate();
15
+ }
16
+ finally {
17
+ guard.inFlight = false;
18
+ }
19
+ }
8
20
  const INITIAL_STATE = {
9
21
  loading: true,
10
22
  rows: [],
11
23
  input: "",
24
+ inputCursor: 0,
12
25
  mode: "transcript",
13
26
  };
14
27
  export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars, onExit }) {
@@ -18,6 +31,7 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
18
31
  const unsubscribeRef = useRef(undefined);
19
32
  const closedRef = useRef(false);
20
33
  const stateRef = useRef(state);
34
+ const pickerActivationRef = useRef({ inFlight: false });
21
35
  useEffect(() => {
22
36
  stateRef.current = state;
23
37
  }, [state]);
@@ -120,7 +134,7 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
120
134
  error: undefined,
121
135
  }));
122
136
  }, [source]);
123
- const selectPickerItem = useCallback(async () => {
137
+ const activatePickerItem = useCallback(async () => {
124
138
  const picker = stateRef.current.picker;
125
139
  const item = picker?.items[picker.selectedIndex];
126
140
  if (!picker || !item) {
@@ -136,8 +150,12 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
136
150
  return;
137
151
  }
138
152
  if (picker.action === "select-room") {
139
- const status = await source.getStatus({ sessionId: stateRef.current.session?.id });
140
- setState((current) => ({ ...current, status, activeRoom: room, mode: "transcript", picker: undefined, overlayStack: undefined, message: `Selected room ${room.title}.`, error: undefined }));
153
+ const currentSession = stateRef.current.session;
154
+ const preserveSession = currentSession?.roomId === room.id;
155
+ const status = await source.getStatus({ sessionId: preserveSession ? currentSession.id : undefined });
156
+ if (!preserveSession)
157
+ closeOpenSession();
158
+ setState((current) => selectInkSessionRoom(current, room, status));
141
159
  return;
142
160
  }
143
161
  await openSessionPickerForRoom(room, picker);
@@ -174,7 +192,13 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
174
192
  catch (error) {
175
193
  setState((current) => ({ ...current, mode: "transcript", picker: undefined, error: formatCliSessionError(error), message: undefined }));
176
194
  }
177
- }, [openSession, openSessionPickerForRoom, source]);
195
+ }, [closeOpenSession, openSession, openSessionPickerForRoom, source]);
196
+ const selectPickerItem = useCallback(async () => {
197
+ const picker = stateRef.current.picker;
198
+ if (!picker)
199
+ return;
200
+ await runInkPickerActivation(pickerActivationRef.current, picker, activatePickerItem);
201
+ }, [activatePickerItem]);
178
202
  useEffect(() => {
179
203
  closedRef.current = false;
180
204
  void (async () => {
@@ -215,6 +239,22 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
215
239
  setState((current) => reduceInkSessionInputState(current, { type: "down" }));
216
240
  return;
217
241
  }
242
+ if (key.leftArrow) {
243
+ setState((current) => reduceInkSessionInputState(current, { type: "left" }));
244
+ return;
245
+ }
246
+ if (key.rightArrow) {
247
+ setState((current) => reduceInkSessionInputState(current, { type: "right" }));
248
+ return;
249
+ }
250
+ if (key.home) {
251
+ setState((current) => reduceInkSessionInputState(current, { type: "home" }));
252
+ return;
253
+ }
254
+ if (key.end) {
255
+ setState((current) => reduceInkSessionInputState(current, { type: "end" }));
256
+ return;
257
+ }
218
258
  if (key.return) {
219
259
  const submitted = stateRef.current.input;
220
260
  if (submitted.length === 0 && !stateRef.current.picker && !stateRef.current.slashSuggestions && canToggleSelectedRowDetails(stateRef.current)) {
@@ -224,11 +264,11 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
224
264
  if (stateRef.current.slashSuggestions && !stateRef.current.picker) {
225
265
  const accepted = acceptSlashSuggestion(stateRef.current);
226
266
  if (accepted.runInput) {
227
- setState((current) => ({ ...current, input: "", slashSuggestions: undefined, message: undefined, error: undefined }));
267
+ setState((current) => ({ ...current, input: "", inputCursor: 0, slashSuggestions: undefined, message: undefined, error: undefined }));
228
268
  void submitCommandOrMessage(accepted.runInput);
229
269
  }
230
270
  else {
231
- setState((current) => ({ ...current, input: accepted.input, slashSuggestions: undefined, message: `Accepted ${accepted.input.trim()}. Press Enter to run or add arguments.`, error: undefined }));
271
+ setState((current) => ({ ...current, input: accepted.input, inputCursor: accepted.input.length, slashSuggestions: undefined, message: `Accepted ${accepted.input.trim()}. Press Enter to run or add arguments.`, error: undefined }));
232
272
  }
233
273
  return;
234
274
  }
@@ -238,6 +278,7 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
238
278
  return;
239
279
  }
240
280
  if (stateRef.current.picker) {
281
+ setState((current) => reduceInkSessionInputState(current, { type: "enter" }));
241
282
  void selectPickerItem();
242
283
  return;
243
284
  }
@@ -292,19 +333,44 @@ export function InkSessionPickerView({ picker, maxLineChars }) {
292
333
  return React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { color: "yellow" }, title), ...picker.items.map((item, index) => React.createElement(Text, { key: item.id, color: overlayItemColor(index === picker.selectedIndex, item.disabled === true) }, formatOverlayItemLine(item, index === picker.selectedIndex, lineLimit))), React.createElement(Text, { color: "gray" }, "↑↓ select · enter confirm · esc back/cancel · ctrl-c exit"));
293
334
  }
294
335
  export function reduceInkSessionInputState(state, action) {
295
- if (action.type === "text")
296
- return withSlashSuggestions({ ...state, input: state.input + action.value, message: undefined, error: undefined });
297
- if (action.type === "backspace")
298
- return withSlashSuggestions({ ...state, input: state.input.slice(0, -1) });
336
+ if (action.type === "text") {
337
+ const inputCursor = normalizeInputCursor(state.input, state.inputCursor);
338
+ return withSlashSuggestions({
339
+ ...state,
340
+ input: state.input.slice(0, inputCursor) + action.value + state.input.slice(inputCursor),
341
+ inputCursor: inputCursor + action.value.length,
342
+ message: undefined,
343
+ error: undefined,
344
+ });
345
+ }
346
+ if (action.type === "backspace") {
347
+ const inputCursor = normalizeInputCursor(state.input, state.inputCursor);
348
+ const previousCursor = adjacentInputCursor(state.input, inputCursor, -1);
349
+ if (previousCursor === inputCursor)
350
+ return state;
351
+ return withSlashSuggestions({
352
+ ...state,
353
+ input: state.input.slice(0, previousCursor) + state.input.slice(inputCursor),
354
+ inputCursor: previousCursor,
355
+ });
356
+ }
357
+ if (action.type === "left" || action.type === "right") {
358
+ const inputCursor = normalizeInputCursor(state.input, state.inputCursor);
359
+ return { ...state, inputCursor: adjacentInputCursor(state.input, inputCursor, action.type === "left" ? -1 : 1) };
360
+ }
361
+ if (action.type === "home")
362
+ return { ...state, inputCursor: 0 };
363
+ if (action.type === "end")
364
+ return { ...state, inputCursor: state.input.length };
299
365
  if (action.type === "toggle-details")
300
366
  return toggleSelectedRowDetails(state);
301
367
  if (action.type === "escape") {
302
368
  if (state.slashSuggestions)
303
369
  return { ...state, slashSuggestions: undefined, overlayStack: popInkSessionOverlay(state.overlayStack), message: "Closed slash suggestions." };
304
370
  if (state.picker?.parent) {
305
- return withPickerOverlay({ ...state, picker: state.picker.parent, mode: pickerMode(state.picker.parent), message: "Back." }, state.picker.parent);
371
+ return withPickerOverlay({ ...state, input: "", picker: state.picker.parent, mode: pickerMode(state.picker.parent), message: "Back." }, state.picker.parent);
306
372
  }
307
- return { ...state, input: "", mode: "transcript", picker: undefined, overlayStack: popInkSessionOverlay(state.overlayStack), message: "Canceled." };
373
+ return { ...state, input: "", inputCursor: 0, mode: "transcript", picker: undefined, overlayStack: popInkSessionOverlay(state.overlayStack), message: "Canceled." };
308
374
  }
309
375
  if (action.type === "up" || action.type === "down") {
310
376
  if (state.slashSuggestions && !state.picker && state.slashSuggestions.items.length > 0) {
@@ -323,11 +389,30 @@ export function reduceInkSessionInputState(state, action) {
323
389
  return {
324
390
  ...state,
325
391
  input: "",
392
+ inputCursor: 0,
326
393
  slashSuggestions: undefined,
327
394
  message: undefined,
328
395
  error: undefined,
329
396
  };
330
397
  }
398
+ const INPUT_GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
399
+ function normalizeInputCursor(input, inputCursor) {
400
+ if (inputCursor === undefined)
401
+ return input.length;
402
+ const clamped = Math.max(0, Math.min(input.length, inputCursor));
403
+ return inputGraphemeBoundaries(input).findLast((boundary) => boundary <= clamped) ?? 0;
404
+ }
405
+ function adjacentInputCursor(input, inputCursor, direction) {
406
+ const boundaries = inputGraphemeBoundaries(input);
407
+ const index = boundaries.indexOf(inputCursor);
408
+ return boundaries[Math.max(0, Math.min(boundaries.length - 1, index + direction))] ?? inputCursor;
409
+ }
410
+ function inputGraphemeBoundaries(input) {
411
+ const boundaries = [0];
412
+ for (const segment of INPUT_GRAPHEME_SEGMENTER.segment(input))
413
+ boundaries.push(segment.index + segment.segment.length);
414
+ return boundaries;
415
+ }
331
416
  export function normalizeInkRowSelection(state) {
332
417
  const expandableIds = expandableRowIds(state.rows);
333
418
  const expandedRowIds = (state.expandedRowIds ?? []).filter((id) => expandableIds.includes(id));
@@ -338,6 +423,20 @@ export function normalizeInkRowSelection(state) {
338
423
  : expandableIds[expandableIds.length - 1];
339
424
  return { ...state, selectedRowId, expandedRowIds };
340
425
  }
426
+ export function selectInkSessionRoom(state, room, status) {
427
+ const preserveSession = state.session?.roomId === room.id;
428
+ return {
429
+ ...state,
430
+ status,
431
+ activeRoom: room,
432
+ ...(preserveSession ? {} : { session: undefined, rows: [], selectedRowId: undefined, expandedRowIds: [] }),
433
+ mode: "transcript",
434
+ picker: undefined,
435
+ overlayStack: undefined,
436
+ message: `Selected room ${room.title}.`,
437
+ error: undefined,
438
+ };
439
+ }
341
440
  function selectExpandableRow(state, direction) {
342
441
  const normalized = normalizeInkRowSelection(state);
343
442
  const ids = expandableRowIds(normalized.rows);
@@ -1,5 +1,5 @@
1
- import { Command } from "commander";
2
- import { IMAGE_NAME, imageExists, shouldRebuild, shouldRebuildDeps, dockerBuild, saveHash, saveDepHash, spawnWorker, spawnDevWorker, listWorkers, releaseWorker, planReapWorkers, applyComputeWorkerReapPlan, getComputeDiskDiagnostics, } from "./docker.js";
1
+ import { Command, InvalidArgumentError } from "commander";
2
+ import { imageExists, shouldRebuild, shouldRebuildDeps, dockerBuild, resolveComputeImageBuildConfig, saveHash, saveDepHash, spawnWorker, spawnDevWorker, listWorkers, releaseWorker, planReapWorkers, applyComputeWorkerReapPlan, getComputeDiskDiagnostics, } from "./docker.js";
3
3
  import { getComputeResourceHealth } from "./resource-health.js";
4
4
  import { mkdir } from "node:fs/promises";
5
5
  import path from "node:path";
@@ -7,6 +7,9 @@ import os from "node:os";
7
7
  const WORKSPACE_DIR = process.env.PIBO_COMPUTE_WORKSPACE || process.cwd();
8
8
  const HASH_FILE = path.join(os.homedir(), ".pibo", "compute-image-hash");
9
9
  const DEP_HASH_FILE = path.join(os.homedir(), ".pibo", "compute-dep-hash");
10
+ function computeImageBuildConfig() {
11
+ return resolveComputeImageBuildConfig(WORKSPACE_DIR);
12
+ }
10
13
  function printJson(value) {
11
14
  console.log(JSON.stringify(value, null, 2));
12
15
  }
@@ -16,6 +19,12 @@ function parsePositiveIntegerOption(value) {
16
19
  const parsed = Number(value);
17
20
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
18
21
  }
22
+ function parseNonNegativeNumber(value) {
23
+ const parsed = Number(value);
24
+ if (!Number.isFinite(parsed) || parsed < 0)
25
+ throw new InvalidArgumentError("Value must be a non-negative number");
26
+ return parsed;
27
+ }
19
28
  function hostPort(value) {
20
29
  if (!value || value === "-")
21
30
  return undefined;
@@ -233,18 +242,24 @@ Creates a worker container and starts the gateway. Prints JSON with the containe
233
242
 
234
243
  Use this for quick isolated checks. For code changes, prefer:
235
244
  $ pibo compute dev spawn --worktree my-fix
245
+
246
+ Environment:
247
+ PIBO_COMPUTE_IMAGE Override the Docker image name.
248
+ PIBO_COMPUTE_WORKSPACE Use an explicit workspace or source checkout.
236
249
  `)
237
250
  .action(async (options) => {
238
251
  await mkdir(path.dirname(HASH_FILE), { recursive: true });
239
- const needsBuild = !(await imageExists(IMAGE_NAME)) || (await shouldRebuild(WORKSPACE_DIR, HASH_FILE));
252
+ const image = computeImageBuildConfig();
253
+ const needsBuild = !(await imageExists(image.imageName)) || (await shouldRebuild(image.buildContext, HASH_FILE, image.dockerfile));
240
254
  if (needsBuild) {
241
- console.error(`Building ${IMAGE_NAME} from ${WORKSPACE_DIR}...`);
242
- await dockerBuild(WORKSPACE_DIR);
243
- await saveHash(WORKSPACE_DIR, HASH_FILE);
255
+ console.error(`Building ${image.imageName} from ${image.buildContext}...`);
256
+ await dockerBuild(image);
257
+ await saveHash(image.buildContext, HASH_FILE, image.dockerfile);
244
258
  console.error("Build complete.");
245
259
  }
246
260
  const worker = await spawnWorker({
247
261
  workspaceDir: WORKSPACE_DIR,
262
+ imageName: image.imageName,
248
263
  name: options.name,
249
264
  holder: options.holder,
250
265
  ttlSeconds: parsePositiveIntegerOption(options.ttlSeconds),
@@ -282,12 +297,13 @@ Example:
282
297
  `)
283
298
  .action(async (options) => {
284
299
  await mkdir(path.dirname(DEP_HASH_FILE), { recursive: true });
300
+ const image = resolveComputeImageBuildConfig(options.repo, { packageRoot: options.repo });
285
301
  console.error("[pibo compute] Checking Docker image status...");
286
- const needsBuild = !(await imageExists(IMAGE_NAME)) || (await shouldRebuildDeps(options.repo, DEP_HASH_FILE));
302
+ const needsBuild = !(await imageExists(image.imageName)) || (await shouldRebuildDeps(options.repo, DEP_HASH_FILE));
287
303
  if (needsBuild) {
288
304
  console.error(`[pibo compute] Dependencies changed (package.json, package-lock.json, or Dockerfile).`);
289
- console.error(`[pibo compute] Rebuilding Docker image ${IMAGE_NAME} — this takes 1-2 minutes...`);
290
- await dockerBuild(options.repo);
305
+ console.error(`[pibo compute] Rebuilding Docker image ${image.imageName} — this takes 1-2 minutes...`);
306
+ await dockerBuild(image);
291
307
  await saveDepHash(options.repo, DEP_HASH_FILE);
292
308
  console.error("[pibo compute] Docker image build complete.");
293
309
  }
@@ -298,6 +314,7 @@ Example:
298
314
  const worker = await spawnDevWorker({
299
315
  repoDir: options.repo,
300
316
  worktreeName: options.worktree,
317
+ imageName: image.imageName,
301
318
  holder: options.holder,
302
319
  ttlSeconds: parsePositiveIntegerOption(options.ttlSeconds),
303
320
  idleSeconds: parsePositiveIntegerOption(options.idleSeconds),
@@ -317,10 +334,11 @@ Example:
317
334
  Rebuilds from the current workspace and refreshes compute image hashes.
318
335
  `)
319
336
  .action(async () => {
320
- console.error(`Rebuilding ${IMAGE_NAME} from ${WORKSPACE_DIR}...`);
321
- await dockerBuild(WORKSPACE_DIR);
322
- await saveHash(WORKSPACE_DIR, HASH_FILE);
323
- await saveDepHash(WORKSPACE_DIR, DEP_HASH_FILE);
337
+ const image = computeImageBuildConfig();
338
+ console.error(`Rebuilding ${image.imageName} from ${image.buildContext}...`);
339
+ await dockerBuild(image);
340
+ await saveHash(image.buildContext, HASH_FILE, image.dockerfile);
341
+ await saveDepHash(image.buildContext, DEP_HASH_FILE);
324
342
  console.error("Build complete.");
325
343
  });
326
344
  program
@@ -403,7 +421,7 @@ Use the name or id shown by:
403
421
  program
404
422
  .command("reap")
405
423
  .description("Preview or apply compute worker container cleanup")
406
- .option("--max-age-minutes <n>", "Select workers older than this many minutes", "60")
424
+ .option("--max-age-minutes <n>", "Select workers older than this many minutes", parseNonNegativeNumber, 60)
407
425
  .option("--include-dev", "Also select dev-worker containers; dev workers are skipped by default")
408
426
  .option("--no-stopped", "Do not select stopped, dead, created, or restarting containers")
409
427
  .option("--no-dirty", "Do not select dirty or OOM-killed containers")
@@ -416,12 +434,13 @@ Stopped, dirty/OOM, and old workers are selected unless disabled by selector fla
416
434
  Worktrees are preserved; container cleanup never deletes Git worktrees.
417
435
  `)
418
436
  .action(async (options) => {
419
- const maxAge = Number(options.maxAgeMinutes);
437
+ if (options.apply && options.dryRun)
438
+ throw new Error("Use either --apply or --dry-run, not both");
420
439
  const plan = await planReapWorkers({
421
440
  includeDev: options.includeDev === true,
422
441
  includeStopped: options.stopped !== false,
423
442
  includeDirty: options.dirty !== false,
424
- maxAgeMinutes: Number.isFinite(maxAge) ? maxAge : 60,
443
+ maxAgeMinutes: options.maxAgeMinutes,
425
444
  });
426
445
  const shouldApply = options.apply === true;
427
446
  const removed = shouldApply ? await applyComputeWorkerReapPlan(plan) : [];