automata-cli 0.1.0-feature-030-do-work.192 → 0.1.0-feature-030-do-work.194

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 CHANGED
@@ -129,14 +129,7 @@ See [docs/execute-prompt.md](docs/execute-prompt.md) for full details.
129
129
 
130
130
  ## `automata do-work`
131
131
 
132
- Run one tick of the autonomous loop: find the open issues whose newest message from an authorized account the agent has not answered — on the issue or on its pull request — and answer them. Designed to run from cron in a disposable VM or container.
133
-
134
- ```bash
135
- automata do-work # one tick
136
- automata do-work --dry-run # show the work plan, change nothing
137
- ```
138
-
139
- See [docs/do-work.md](docs/do-work.md) for the command reference, and the [wiki](docs/wiki/Home.md) for the process: the trust model, the issue lifecycle, setup and operation.
132
+ One tick of the autonomous loop: answer the open issues whose newest message from an authorized account the agent has not answered. Reference: [docs/do-work.md](docs/do-work.md). Process, trust model and setup: [the wiki](docs/wiki/Home.md).
140
133
 
141
134
  ---
142
135
 
@@ -164,7 +164,7 @@ function ConfigWizard() {
164
164
  } else if (key.backspace || key.delete) {
165
165
  setDoWorkMaxRuns((v) => v.slice(0, -1));
166
166
  } else if (key.escape) {
167
- setScreen("do-work-executor");
167
+ setScreen("do-work-codex-model");
168
168
  } else if (key.ctrl && input === "c") {
169
169
  exit();
170
170
  } else if (input && !key.ctrl && !key.meta) {
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-SS7OOS5T.js")
186
+ import("./ConfigWizard-GN5JC4XP.js")
187
187
  ]);
188
188
  const { waitUntilExit } = render(React.createElement(ConfigWizard));
189
189
  await waitUntilExit();
@@ -1532,6 +1532,31 @@ function buildClaudeArgs(prompt, options = {}) {
1532
1532
  args.push("-p", prompt);
1533
1533
  return args;
1534
1534
  }
1535
+ var activeChildren = /* @__PURE__ */ new Set();
1536
+ function terminateActiveClaudeProcesses(timeoutMs = 1e4) {
1537
+ const children = [...activeChildren];
1538
+ if (children.length === 0) return Promise.resolve();
1539
+ const exits = children.map(
1540
+ (child) => new Promise((resolve2) => {
1541
+ if (child.exitCode !== null || child.signalCode !== null) {
1542
+ resolve2();
1543
+ return;
1544
+ }
1545
+ child.once("exit", () => resolve2());
1546
+ child.kill("SIGTERM");
1547
+ })
1548
+ );
1549
+ return Promise.race([
1550
+ Promise.all(exits).then(() => void 0),
1551
+ new Promise((resolve2) => {
1552
+ const timer = setTimeout(() => {
1553
+ for (const child of children) child.kill("SIGKILL");
1554
+ resolve2();
1555
+ }, timeoutMs);
1556
+ timer.unref();
1557
+ })
1558
+ ]);
1559
+ }
1535
1560
  function invokeClaudeCode(prompt, options = {}) {
1536
1561
  if (options.verbose) {
1537
1562
  return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model);
@@ -1550,6 +1575,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1550
1575
  const claudeBin = resolveCommand("claude");
1551
1576
  const args = buildClaudeArgs(prompt, { yolo, model, verbose: true });
1552
1577
  const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
1578
+ activeChildren.add(child);
1553
1579
  const rl = createInterface({ input: child.stdout });
1554
1580
  let turnCount = 0;
1555
1581
  child.on("error", (err) => {
@@ -1564,6 +1590,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1564
1590
  }
1565
1591
  });
1566
1592
  child.on("close", (code) => {
1593
+ activeChildren.delete(child);
1567
1594
  handleExitCode(code, "Claude Code");
1568
1595
  resolve2();
1569
1596
  });
@@ -2311,42 +2338,64 @@ function getIssueSurface(issueNumber) {
2311
2338
  };
2312
2339
  }
2313
2340
  var LINK_MAP_QUERY = `
2314
- query($owner:String!,$repo:String!){
2341
+ query($owner:String!,$repo:String!,$cursor:String){
2315
2342
  repository(owner:$owner,name:$repo){
2316
- pullRequests(states:OPEN, first:100, orderBy:{field:UPDATED_AT, direction:DESC}){
2343
+ pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
2344
+ pageInfo{ hasNextPage endCursor }
2317
2345
  nodes{
2318
2346
  number url title headRefName isDraft updatedAt
2319
- closingIssuesReferences(first:10){ nodes{ number } }
2347
+ closingIssuesReferences(first:50){
2348
+ pageInfo{ hasNextPage }
2349
+ nodes{ number }
2350
+ }
2320
2351
  }
2321
2352
  }
2322
2353
  }
2323
2354
  }`.trim();
2355
+ var MAX_LINK_MAP_PAGES = 50;
2324
2356
  function getOpenPrLinkMap() {
2325
2357
  const { owner, repo } = getRepoSlug();
2326
- const response = ghJson(
2327
- ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`],
2328
- "query open pull requests"
2329
- );
2330
2358
  const map = /* @__PURE__ */ new Map();
2331
- for (const node of response.data.repository.pullRequests.nodes) {
2332
- const ref = {
2333
- number: node.number,
2334
- url: node.url,
2335
- title: node.title,
2336
- headRefName: node.headRefName,
2337
- state: "OPEN",
2338
- isDraft: node.isDraft,
2339
- updatedAt: node.updatedAt
2340
- };
2341
- for (const issue of node.closingIssuesReferences.nodes) {
2342
- const existing = map.get(issue.number);
2343
- if (existing) {
2344
- existing.push(ref);
2345
- } else {
2346
- map.set(issue.number, [ref]);
2359
+ let cursor = null;
2360
+ for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
2361
+ const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
2362
+ if (cursor !== null) args.push("-f", `cursor=${cursor}`);
2363
+ const response = ghJson(args, "query open pull requests");
2364
+ const connection = response.data.repository.pullRequests;
2365
+ for (const node of connection.nodes) {
2366
+ const ref = {
2367
+ number: node.number,
2368
+ url: node.url,
2369
+ title: node.title,
2370
+ headRefName: node.headRefName,
2371
+ state: "OPEN",
2372
+ isDraft: node.isDraft,
2373
+ updatedAt: node.updatedAt
2374
+ };
2375
+ if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
2376
+ process.stderr.write(
2377
+ `Warning: pull request #${String(node.number)} closes more than 50 issues; some links were not read.
2378
+ `
2379
+ );
2380
+ }
2381
+ for (const issue of node.closingIssuesReferences.nodes) {
2382
+ const existing = map.get(issue.number);
2383
+ if (existing) {
2384
+ existing.push(ref);
2385
+ } else {
2386
+ map.set(issue.number, [ref]);
2387
+ }
2347
2388
  }
2348
2389
  }
2390
+ if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
2391
+ return map;
2392
+ }
2393
+ cursor = connection.pageInfo.endCursor;
2349
2394
  }
2395
+ process.stderr.write(
2396
+ `Warning: stopped paginating open pull requests after ${String(MAX_LINK_MAP_PAGES)} pages; the issue-to-pull-request map may be incomplete.
2397
+ `
2398
+ );
2350
2399
  return map;
2351
2400
  }
2352
2401
  var REVIEW_THREADS_QUERY2 = `
@@ -2501,9 +2550,19 @@ function isAssignedToAgent(assignees, agentUser) {
2501
2550
  return assignees.some((name) => name.toLowerCase() === agent);
2502
2551
  }
2503
2552
  function findActionableThreads(threads, p) {
2504
- return threads.filter(
2505
- (thread) => !thread.isResolved && lastAuthorClass(thread.comments, p) === "authorized"
2506
- );
2553
+ const actionable = [];
2554
+ for (const thread of threads) {
2555
+ if (thread.isResolved) continue;
2556
+ const comments = thread.comments.filter((comment) => classifyForThread(comment.author, p) !== "other");
2557
+ if (lastAuthorClass(comments, p) !== "authorized") continue;
2558
+ actionable.push({ ...thread, comments });
2559
+ }
2560
+ return actionable;
2561
+ }
2562
+ function classifyForThread(author, p) {
2563
+ const login2 = author.toLowerCase();
2564
+ if (login2 === p.agentUser.toLowerCase()) return "agent";
2565
+ return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
2507
2566
  }
2508
2567
  function newestPr(prs) {
2509
2568
  return [...prs].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
@@ -2710,6 +2769,7 @@ function preparePrBranch(headRefName) {
2710
2769
 
2711
2770
  // src/run/runLock.ts
2712
2771
  import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync } from "fs";
2772
+ import { randomUUID } from "crypto";
2713
2773
  import { hostname } from "os";
2714
2774
  import { join as join2 } from "path";
2715
2775
  var LOCK_DIR = ".automata";
@@ -2733,7 +2793,8 @@ function readOwner(path) {
2733
2793
  pid: parsed.pid,
2734
2794
  startedAt: parsed.startedAt,
2735
2795
  host: parsed.host ?? "unknown",
2736
- command: parsed.command ?? "unknown"
2796
+ command: parsed.command ?? "unknown",
2797
+ token: parsed.token ?? ""
2737
2798
  };
2738
2799
  } catch {
2739
2800
  return null;
@@ -2741,17 +2802,23 @@ function readOwner(path) {
2741
2802
  }
2742
2803
  function isStale(owner, staleMinutes) {
2743
2804
  if (owner === null) return true;
2744
- if (owner.host === hostname() && !isAlive(owner.pid)) return true;
2805
+ if (owner.host === hostname()) {
2806
+ return !isAlive(owner.pid);
2807
+ }
2745
2808
  const startedAt = Date.parse(owner.startedAt);
2746
2809
  if (Number.isNaN(startedAt)) return true;
2747
2810
  return Date.now() - startedAt > staleMinutes * 60 * 1e3;
2748
2811
  }
2749
- function makeHandle(path) {
2812
+ function makeHandle(path, token) {
2750
2813
  let released = false;
2751
2814
  return {
2752
2815
  release() {
2753
2816
  if (released) return;
2754
2817
  released = true;
2818
+ const current = readOwner(path);
2819
+ if (current !== null && current.token !== token) {
2820
+ return;
2821
+ }
2755
2822
  try {
2756
2823
  unlinkSync(path);
2757
2824
  } catch {
@@ -2759,21 +2826,23 @@ function makeHandle(path) {
2759
2826
  }
2760
2827
  };
2761
2828
  }
2762
- function write(path, command) {
2829
+ function write(path, command, token) {
2763
2830
  const owner = {
2764
2831
  pid: process.pid,
2765
2832
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2766
2833
  host: hostname(),
2767
- command
2834
+ command,
2835
+ token
2768
2836
  };
2769
2837
  writeFileSync(path, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
2770
2838
  }
2771
2839
  function acquireRunLock(command, staleMinutes) {
2772
2840
  const path = lockPath();
2841
+ const token = randomUUID();
2773
2842
  mkdirSync(join2(process.cwd(), LOCK_DIR), { recursive: true });
2774
2843
  try {
2775
- write(path, command);
2776
- return { ok: true, handle: makeHandle(path) };
2844
+ write(path, command, token);
2845
+ return { ok: true, handle: makeHandle(path, token) };
2777
2846
  } catch (err) {
2778
2847
  if (err.code !== "EEXIST") throw err;
2779
2848
  }
@@ -2786,14 +2855,20 @@ function acquireRunLock(command, staleMinutes) {
2786
2855
  } catch {
2787
2856
  }
2788
2857
  try {
2789
- write(path, command);
2790
- return { ok: true, handle: makeHandle(path) };
2858
+ write(path, command, token);
2859
+ return { ok: true, handle: makeHandle(path, token) };
2791
2860
  } catch (err) {
2792
2861
  if (err.code !== "EEXIST") throw err;
2793
2862
  const winner = readOwner(path);
2794
2863
  return {
2795
2864
  ok: false,
2796
- heldBy: winner ?? { pid: 0, startedAt: (/* @__PURE__ */ new Date()).toISOString(), host: "unknown", command: "unknown" }
2865
+ heldBy: winner ?? {
2866
+ pid: 0,
2867
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2868
+ host: "unknown",
2869
+ command: "unknown",
2870
+ token: ""
2871
+ }
2797
2872
  };
2798
2873
  }
2799
2874
  }
@@ -2886,9 +2961,8 @@ function checkAuthenticatedIdentity(agentUser, allowedUsers) {
2886
2961
  `\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
2887
2962
  );
2888
2963
  }
2889
- progress(
2890
- `Warning: \`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". The agent will not recognise its own messages and may repeat itself. Authenticate as the agent account, or correct \`agentUser\`.
2891
- `
2964
+ fail(
2965
+ `\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
2892
2966
  );
2893
2967
  }
2894
2968
  function planRun(item, settings, silent) {
@@ -3016,8 +3090,14 @@ async function invokeExecutor(prompt, settings, silent) {
3016
3090
  }
3017
3091
  await invokeClaudeCode(prompt, { yolo: true, verbose: !silent, model: settings.model });
3018
3092
  }
3019
- function repairIssueLink(item) {
3093
+ function repairIssueLink(item, baseBranch) {
3020
3094
  try {
3095
+ const branch = getCurrentBranch();
3096
+ if (branch === baseBranch) {
3097
+ progress(` issue #${String(item.issue.number)} is still in discussion (no branch was created).
3098
+ `);
3099
+ return;
3100
+ }
3021
3101
  const pr = getCurrentBranchPr();
3022
3102
  if (!pr) {
3023
3103
  progress(` issue #${String(item.issue.number)} is still in discussion (no pull request).
@@ -3088,7 +3168,7 @@ async function processItem(item, settings, silent) {
3088
3168
  progress(` ${reconciled.detail}
3089
3169
  `);
3090
3170
  if (item.turn === "issue-discuss") {
3091
- repairIssueLink(item);
3171
+ repairIssueLink(item, settings.baseBranch);
3092
3172
  }
3093
3173
  return { ...base, outcome: reconciled.outcome, detail: reconciled.detail };
3094
3174
  }
@@ -3119,9 +3199,15 @@ var doWorkCommand = new Command6("do-work").description(
3119
3199
  return;
3120
3200
  }
3121
3201
  const handle = lock.handle;
3202
+ let shuttingDown = false;
3122
3203
  const onSignal = () => {
3123
- handle.release();
3124
- process.exit(130);
3204
+ if (shuttingDown) return;
3205
+ shuttingDown = true;
3206
+ progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
3207
+ void terminateActiveClaudeProcesses().then(() => {
3208
+ handle.release();
3209
+ process.exit(130);
3210
+ });
3125
3211
  };
3126
3212
  process.once("SIGINT", onSignal);
3127
3213
  process.once("SIGTERM", onSignal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.1.0-feature-030-do-work.192",
3
+ "version": "0.1.0-feature-030-do-work.194",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {