automata-cli 0.1.0-feature-030-do-work.204 → 0.1.0-feature-030-do-work.208

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.
@@ -73,7 +73,7 @@ function handleMenu(input, key, menu, cancel) {
73
73
  cancel();
74
74
  }
75
75
  }
76
- function TextEntryScreen({ title, label, value, hint }) {
76
+ function TextEntryScreen({ title, label, value, hint, error }) {
77
77
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
78
78
  /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
79
79
  /* @__PURE__ */ jsx(Text, { children: " " }),
@@ -85,7 +85,7 @@ function TextEntryScreen({ title, label, value, hint }) {
85
85
  /* @__PURE__ */ jsx(Text, { children: "_" })
86
86
  ] })
87
87
  ] }),
88
- /* @__PURE__ */ jsx(Text, { children: " " }),
88
+ error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : /* @__PURE__ */ jsx(Text, { children: " " }),
89
89
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint })
90
90
  ] });
91
91
  }
@@ -102,6 +102,12 @@ function MenuEntryScreen({ title, options, index, hint }) {
102
102
  ] });
103
103
  }
104
104
  var BACK = "Esc to go back \xB7 Ctrl+C to cancel";
105
+ function parseWholeInt(value) {
106
+ const trimmed = value.trim();
107
+ if (!/^\d+$/.test(trimmed)) return null;
108
+ const parsed = Number(trimmed);
109
+ return Number.isSafeInteger(parsed) ? parsed : null;
110
+ }
105
111
  function savePrompt(filename, content, merge) {
106
112
  let value;
107
113
  if (content) {
@@ -143,6 +149,10 @@ function ConfigWizard() {
143
149
  const [doWorkExecutorIndex, setDoWorkExecutorIndex] = useState(Math.max(initialExecutorIndex, 0));
144
150
  const [doWorkClaudeModel, setDoWorkClaudeModel] = useState(existing.doWork?.models?.claude ?? "");
145
151
  const [doWorkCodexModel, setDoWorkCodexModel] = useState(existing.doWork?.models?.codex ?? "");
152
+ const [doWorkLockStale, setDoWorkLockStale] = useState(
153
+ String(existing.doWork?.lockStaleMinutes ?? DEFAULT_DO_WORK.lockStaleMinutes)
154
+ );
155
+ const [validationError, setValidationError] = useState("");
146
156
  const [doWorkMaxRuns, setDoWorkMaxRuns] = useState(
147
157
  String(existing.doWork?.maxRunsPerTick ?? DEFAULT_DO_WORK.maxRunsPerTick)
148
158
  );
@@ -250,10 +260,33 @@ function ConfigWizard() {
250
260
  onBack: () => setScreen("do-work-claude-model")
251
261
  },
252
262
  "do-work-max-runs": {
253
- setValue: setDoWorkMaxRuns,
263
+ setValue: (update) => {
264
+ setValidationError("");
265
+ setDoWorkMaxRuns(update);
266
+ },
254
267
  onSubmit: () => {
255
- const trimmedMaxRuns = doWorkMaxRuns.trim();
256
- const parsedMaxRuns = /^\d+$/.test(trimmedMaxRuns) ? Number(trimmedMaxRuns) : Number.NaN;
268
+ const parsed = parseWholeInt(doWorkMaxRuns);
269
+ if (parsed === null || parsed < 0) {
270
+ setValidationError("Enter a non-negative whole number (0 = unlimited).");
271
+ return;
272
+ }
273
+ setValidationError("");
274
+ setScreen("do-work-lock-stale");
275
+ },
276
+ onBack: () => setScreen("do-work-codex-model")
277
+ },
278
+ "do-work-lock-stale": {
279
+ setValue: (update) => {
280
+ setValidationError("");
281
+ setDoWorkLockStale(update);
282
+ },
283
+ onSubmit: () => {
284
+ const parsed = parseWholeInt(doWorkLockStale);
285
+ if (parsed === null || parsed <= 0) {
286
+ setValidationError("Enter a whole number of minutes greater than zero.");
287
+ return;
288
+ }
289
+ const maxRuns = parseWholeInt(doWorkMaxRuns);
257
290
  const current = readRawConfig();
258
291
  writeConfig({
259
292
  ...current,
@@ -265,12 +298,13 @@ function ConfigWizard() {
265
298
  claude: doWorkClaudeModel.trim() || void 0,
266
299
  codex: doWorkCodexModel.trim() || void 0
267
300
  },
268
- maxRunsPerTick: Number.isSafeInteger(parsedMaxRuns) && parsedMaxRuns >= 0 ? parsedMaxRuns : void 0
301
+ maxRunsPerTick: maxRuns ?? void 0,
302
+ lockStaleMinutes: parsed
269
303
  }
270
304
  });
271
305
  exit();
272
306
  },
273
- onBack: () => setScreen("do-work-codex-model")
307
+ onBack: () => setScreen("do-work-max-runs")
274
308
  },
275
309
  "do-work-discuss-prompt": {
276
310
  setValue: setDoWorkDiscussPrompt,
@@ -421,6 +455,12 @@ function ConfigWizard() {
421
455
  title: "Do Work \u2014 Max Runs Per Tick",
422
456
  label: "Model runs allowed per tick (0 = unlimited):",
423
457
  value: doWorkMaxRuns,
458
+ hint: `Type a number \xB7 Enter to continue \xB7 ${BACK}`
459
+ },
460
+ "do-work-lock-stale": {
461
+ title: "Do Work \u2014 Lock Staleness",
462
+ label: "Minutes before a run lock from another host is treated as stale:",
463
+ value: doWorkLockStale,
424
464
  hint: `Type a number \xB7 Enter to save \xB7 ${BACK}`
425
465
  },
426
466
  "do-work-discuss-prompt": {
@@ -469,7 +509,7 @@ function ConfigWizard() {
469
509
  }
470
510
  };
471
511
  const textView = textViews[screen];
472
- if (textView) return /* @__PURE__ */ jsx(TextEntryScreen, { ...textView });
512
+ if (textView) return /* @__PURE__ */ jsx(TextEntryScreen, { ...textView, error: validationError });
473
513
  const menuView = menuViews[screen];
474
514
  if (menuView) return /* @__PURE__ */ jsx(MenuEntryScreen, { ...menuView });
475
515
  return null;
package/dist/index.js CHANGED
@@ -183,7 +183,7 @@ var configCommand = new Command("config").description("Configure automata settin
183
183
  const [{ render }, React, { ConfigWizard }] = await Promise.all([
184
184
  import("ink"),
185
185
  import("react"),
186
- import("./ConfigWizard-26Y5YBF7.js")
186
+ import("./ConfigWizard-OYNUMRTO.js")
187
187
  ]);
188
188
  const { waitUntilExit } = render(React.createElement(ConfigWizard));
189
189
  await waitUntilExit();
@@ -1529,29 +1529,38 @@ function trackChild(child) {
1529
1529
  function untrackChild(child) {
1530
1530
  active.delete(child);
1531
1531
  }
1532
- function terminateTrackedChildren(timeoutMs = 1e4) {
1532
+ function waitForExit(child) {
1533
+ return new Promise((resolve2) => {
1534
+ if (child.exitCode !== null || child.signalCode !== null) {
1535
+ resolve2();
1536
+ return;
1537
+ }
1538
+ child.once("exit", () => resolve2());
1539
+ });
1540
+ }
1541
+ function afterDelay(ms) {
1542
+ return new Promise((resolve2) => {
1543
+ const timer = setTimeout(() => resolve2("timeout"), ms);
1544
+ timer.unref();
1545
+ });
1546
+ }
1547
+ async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
1533
1548
  const children = [...active];
1534
- if (children.length === 0) return Promise.resolve();
1535
- const exits = children.map(
1536
- (child) => new Promise((resolve2) => {
1537
- if (child.exitCode !== null || child.signalCode !== null) {
1538
- resolve2();
1539
- return;
1540
- }
1541
- child.once("exit", () => resolve2());
1542
- child.kill("SIGTERM");
1543
- })
1544
- );
1545
- return Promise.race([
1546
- Promise.all(exits).then(() => void 0),
1547
- new Promise((resolve2) => {
1548
- const timer = setTimeout(() => {
1549
- for (const child of children) child.kill("SIGKILL");
1550
- resolve2();
1551
- }, timeoutMs);
1552
- timer.unref();
1553
- })
1549
+ if (children.length === 0) return true;
1550
+ const exits = children.map((child) => waitForExit(child));
1551
+ for (const child of children) {
1552
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
1553
+ }
1554
+ const settled = await Promise.race([Promise.all(exits).then(() => "exited"), afterDelay(timeoutMs)]);
1555
+ if (settled === "exited") return true;
1556
+ for (const child of children) {
1557
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
1558
+ }
1559
+ const escalated = await Promise.race([
1560
+ Promise.all(exits).then(() => "exited"),
1561
+ afterDelay(killGraceMs)
1554
1562
  ]);
1563
+ return escalated === "exited";
1555
1564
  }
1556
1565
 
1557
1566
  // src/claude/claudeService.ts
@@ -2916,23 +2925,32 @@ var UNKNOWN_OWNER = {
2916
2925
  command: "unknown",
2917
2926
  token: ""
2918
2927
  };
2928
+ function claimStaleLock(path, token) {
2929
+ try {
2930
+ renameSync(path, `${path}.stale.${token}`);
2931
+ return true;
2932
+ } catch (err) {
2933
+ if (err.code === "ENOENT") return false;
2934
+ throw err;
2935
+ }
2936
+ }
2919
2937
  function reclaim(path, command, token) {
2920
- const candidate = `${path}.${token}`;
2938
+ if (!claimStaleLock(path, token)) {
2939
+ return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER };
2940
+ }
2941
+ const claimed = `${path}.stale.${token}`;
2921
2942
  try {
2922
- write(candidate, command, token, "w");
2923
- renameSync(candidate, path);
2943
+ write(path, command, token, "wx");
2944
+ return { ok: true, handle: makeHandle(path, token) };
2924
2945
  } catch (err) {
2946
+ if (err.code !== "EEXIST") throw err;
2947
+ return { ok: false, heldBy: readOwner(path) ?? UNKNOWN_OWNER };
2948
+ } finally {
2925
2949
  try {
2926
- unlinkSync(candidate);
2950
+ unlinkSync(claimed);
2927
2951
  } catch {
2928
2952
  }
2929
- throw err;
2930
2953
  }
2931
- const winner = readOwner(path);
2932
- if (winner !== null && winner.token === token) {
2933
- return { ok: true, handle: makeHandle(path, token) };
2934
- }
2935
- return { ok: false, heldBy: winner ?? UNKNOWN_OWNER };
2936
2954
  }
2937
2955
 
2938
2956
  // src/git/workspaceService.ts
@@ -3202,7 +3220,8 @@ function describePlan(decisions) {
3202
3220
  });
3203
3221
  return lines.length === 0 ? " (no issues matched the discovery filter)\n" : lines.join("\n") + "\n";
3204
3222
  }
3205
- function refreshItem(item, settings, linkMap) {
3223
+ function refreshItem(item, settings) {
3224
+ const linkMap = getOpenPrLinkMap();
3206
3225
  return decideWork(
3207
3226
  buildIssueState(item.issue, linkMap),
3208
3227
  settings.participants,
@@ -3299,26 +3318,32 @@ function repairIssueLink(item, baseBranch) {
3299
3318
  `);
3300
3319
  }
3301
3320
  }
3302
- async function processItem(planned, settings, linkMap, silent) {
3303
- const base = {
3304
- issue: planned.issue.number,
3305
- title: planned.issue.title,
3306
- turn: planned.turn
3307
- };
3321
+ async function processItem(planned, settings, silent) {
3308
3322
  progress(`
3309
3323
  #${String(planned.issue.number)} ${planned.turn}: ${planned.reason}
3310
3324
  `);
3311
- const refreshed = refreshItem(planned, settings, linkMap);
3325
+ const refreshed = refreshItem(planned, settings);
3312
3326
  if (refreshed.kind === "skip") {
3313
3327
  progress(` skipped: ${refreshed.detail}
3314
3328
  `);
3315
- return { ...base, outcome: "skipped", detail: `no longer actionable: ${refreshed.detail}` };
3329
+ return {
3330
+ issue: planned.issue.number,
3331
+ title: planned.issue.title,
3332
+ turn: planned.turn,
3333
+ outcome: "skipped",
3334
+ detail: `no longer actionable: ${refreshed.detail}`
3335
+ };
3316
3336
  }
3317
3337
  const item = refreshed.item;
3318
3338
  if (item.turn !== planned.turn) {
3319
3339
  progress(` turn changed to ${item.turn} since the plan was built; using the current state.
3320
3340
  `);
3321
3341
  }
3342
+ const base = {
3343
+ issue: item.issue.number,
3344
+ title: item.issue.title,
3345
+ turn: item.turn
3346
+ };
3322
3347
  const prepared = item.turn === "issue-discuss" ? prepareBaseBranch(item.branch) : preparePrBranch(item.branch);
3323
3348
  if (!prepared.ok) {
3324
3349
  progress(` skipped: ${prepared.reason} \u2014 ${prepared.detail}
@@ -3397,8 +3422,14 @@ var doWorkCommand = new Command6("do-work").description(
3397
3422
  if (shuttingDown) return;
3398
3423
  shuttingDown = true;
3399
3424
  progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
3400
- void terminateTrackedChildren().then(() => {
3401
- handle.release();
3425
+ void terminateTrackedChildren().then((allExited) => {
3426
+ if (allExited) {
3427
+ handle.release();
3428
+ } else {
3429
+ progress(
3430
+ "Warning: could not confirm the executor exited; leaving the run lock in place. Check for a stray executor process before the next tick.\n"
3431
+ );
3432
+ }
3402
3433
  process.exit(130);
3403
3434
  });
3404
3435
  };
@@ -3434,11 +3465,17 @@ ${describePlan(decisions)}`;
3434
3465
  reportDryRun(items, decisions, settings, options);
3435
3466
  return 0;
3436
3467
  }
3437
- const runnable = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
3438
- const deferred = items.slice(runnable.length);
3439
3468
  const reports = [];
3440
- for (const item of runnable) {
3441
- reports.push(await processItem(item, settings, linkMap, options.silent === true));
3469
+ const deferred = [];
3470
+ let runsUsed = 0;
3471
+ for (const item of items) {
3472
+ if (settings.maxRuns > 0 && runsUsed >= settings.maxRuns) {
3473
+ deferred.push(item);
3474
+ continue;
3475
+ }
3476
+ const report = await processItem(item, settings, options.silent === true);
3477
+ if (report.outcome !== "skipped") runsUsed++;
3478
+ reports.push(report);
3442
3479
  }
3443
3480
  for (const item of deferred) {
3444
3481
  progress(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.1.0-feature-030-do-work.204",
3
+ "version": "0.1.0-feature-030-do-work.208",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {