@sleds/cli 0.1.32 → 0.1.33

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/sleds.mjs +378 -100
  2. package/package.json +1 -1
package/dist/sleds.mjs CHANGED
@@ -5274,11 +5274,26 @@ var init_statusMap = __esm(() => {
5274
5274
  status: false,
5275
5275
  assignee: false,
5276
5276
  children: false,
5277
- comments: false
5277
+ comments: false,
5278
+ fields: false
5278
5279
  };
5279
5280
  WAY_SWITCHES = {
5280
- jira: { create: true, status: true, assignee: true, children: true, comments: true },
5281
- moving: { create: false, status: true, assignee: true, children: true, comments: true },
5281
+ jira: {
5282
+ create: true,
5283
+ status: true,
5284
+ assignee: true,
5285
+ children: true,
5286
+ comments: true,
5287
+ fields: true
5288
+ },
5289
+ moving: {
5290
+ create: false,
5291
+ status: true,
5292
+ assignee: true,
5293
+ children: true,
5294
+ comments: true,
5295
+ fields: true
5296
+ },
5282
5297
  readonly: { ...DEFAULT_WRITES }
5283
5298
  };
5284
5299
  });
@@ -6075,7 +6090,8 @@ function nodePrompt(o) {
6075
6090
  `- Work on branch \`${o.branch}\` from the latest \`origin/${base}\` (create it if this checkout is not on it). One branch, one PR.`,
6076
6091
  `- Open the PR against \`${base}\` with \`gh pr create\`${o.prTitle ? ` and title it exactly \`${o.prTitle}\` (the repo's CI checks it)` : ""}. Its body carries \`SLEDS-Node: ${claim.node}\` on the line under \`SLEDS-Source:\` when the pack names a source.`,
6077
6092
  "- Run the repo's own gate before you push. Never merge: a person (or the musher, when the attachment says so) does.",
6078
- "- Stay inside this node's scope. Work another node needs goes in the PR description, not in this branch."
6093
+ "- Stay inside this node's scope. Work another node needs goes in the PR description, not in this branch.",
6094
+ `- End every comment and reply you post on the PR with the line \`${ENGINE_MARK}\`, so the engine knows it is yours and not a reviewer's.`
6079
6095
  ];
6080
6096
  if (o.sledsTools && claim.runId)
6081
6097
  lines.push(`- If only a person can decide something, first search the record (the pack, the decision log in it, the specs and docs it names); if it is still open, call the SLEDS \`${toolName("run", o.engineMcp)}\` tool with \`{runId: "${claim.runId}", park: {question, options?, irreversible?}}\` — say in the question what you already read — and wait; do not guess. An answerer takes it first, then a person. Mark \`irreversible: true\` when acting on either answer could not be undone by reverting the PR (data deleted or migrated in place, a deploy, anything that leaves the org, spend, access, the brief). The engine leaves this session open for the answer.`, `- Before you finish, \`${toolName("land", o.engineMcp)}\` with \`{project: "${claim.item}", node: "${claim.node}", state: "pr", ref: <the PR url>, notes}\` — notes are what the next node should know${deviations}.`);
@@ -6332,9 +6348,15 @@ function eventPrompt(o) {
6332
6348
  return conflictPrompt({ ...o, event });
6333
6349
  return ciTwicePrompt({ ...o, event });
6334
6350
  }
6335
- var DEFAULT_SEAT = "\x00default", MAX_SEAT_AT_ONCE = 16, DRAFT_NODE = "draft", BRANCH_FIELDS, PR_TITLE_FIELDS, ENGINE_MCP_SERVER = "sleds", RUN_TOKEN_ENV = "SLEDS_RUN_TOKEN", NO_RUN_TOKEN = "no run token from SLEDS: this session acts as the seat", toolName = (name, engineMcp) => engineMcp ? `mcp__${ENGINE_MCP_SERVER}__${name}` : name, APPROACH_SHAPE, AUTONOMY_SAYS, IRREVERSIBLE = "deleting or migrating data in place; deploying outside the playbook's window; anything that leaves the org (a message to a customer, a publish, a release, an email); spend past the band or the allowance; force-pushing or deleting a branch someone else holds; granting or removing access; changing the brief. When in doubt, it is irreversible.", oneLine = (s, max) => s.replace(/\s+/g, " ").trim().slice(0, max), VERDICT_SHAPE, codes = (xs) => xs.map((x) => `\`${x}\``).join(", "), RESOLVE_MAX_FILES = 8;
6351
+ var DEFAULT_SEAT = "\x00default", MAX_SEAT_AT_ONCE = 16, HOLD_DEFAULTS, DRAFT_NODE = "draft", BRANCH_FIELDS, PR_TITLE_FIELDS, ENGINE_MCP_SERVER = "sleds", RUN_TOKEN_ENV = "SLEDS_RUN_TOKEN", NO_RUN_TOKEN = "no run token from SLEDS: this session acts as the seat", toolName = (name, engineMcp) => engineMcp ? `mcp__${ENGINE_MCP_SERVER}__${name}` : name, ENGINE_MARK = "<!-- sleds:engine -->", APPROACH_SHAPE, AUTONOMY_SAYS, IRREVERSIBLE = "deleting or migrating data in place; deploying outside the playbook's window; anything that leaves the org (a message to a customer, a publish, a release, an email); spend past the band or the allowance; force-pushing or deleting a branch someone else holds; granting or removing access; changing the brief. When in doubt, it is irreversible.", oneLine = (s, max) => s.replace(/\s+/g, " ").trim().slice(0, max), VERDICT_SHAPE, codes = (xs) => xs.map((x) => `\`${x}\``).join(", "), RESOLVE_MAX_FILES = 8;
6336
6352
  var init_engine = __esm(() => {
6337
6353
  init_route();
6354
+ HOLD_DEFAULTS = {
6355
+ holdCpu: 85,
6356
+ holdLoad: 1.5,
6357
+ holdPressure: "warn",
6358
+ holdUsage: 80
6359
+ };
6338
6360
  BRANCH_FIELDS = ["key", "item", "node", "slug", "ticket"];
6339
6361
  PR_TITLE_FIELDS = ["key", "item", "node", "title", "ticket"];
6340
6362
  APPROACH_SHAPE = [
@@ -7369,9 +7391,12 @@ function parseRun(v) {
7369
7391
  url: v.pr.url,
7370
7392
  failing: Array.isArray(v.pr.failing) ? v.pr.failing.filter((x) => typeof x === "string") : [],
7371
7393
  feedback: typeof v.pr.feedback === "number" ? v.pr.feedback : 0,
7394
+ ...typeof v.pr.authorFeedback === "number" ? { authorFeedback: v.pr.authorFeedback } : {},
7395
+ ...typeof v.pr.selfNudges === "number" ? { selfNudges: v.pr.selfNudges } : {},
7372
7396
  ...v.pr.clear === true ? { clear: true } : {},
7373
7397
  ...typeof v.pr.ci === "string" ? { ci: v.pr.ci } : {},
7374
7398
  ...typeof v.pr.review === "string" ? { review: v.pr.review } : {},
7399
+ ...typeof v.pr.posted === "string" ? { posted: v.pr.posted } : {},
7375
7400
  ...Array.isArray(v.pr.failedHeads) ? { failedHeads: v.pr.failedHeads.filter((x) => typeof x === "string") } : {},
7376
7401
  ...typeof v.pr.conflict === "string" ? { conflict: v.pr.conflict } : {},
7377
7402
  ...typeof v.pr.resolving === "string" ? { resolving: v.pr.resolving } : {},
@@ -7387,10 +7412,12 @@ function parseState(text) {
7387
7412
  return { runs: [] };
7388
7413
  const decisions = Array.isArray(raw.decisions) ? raw.decisions.map(parseDecision).filter((x) => x !== null) : [];
7389
7414
  const merged = Array.isArray(raw.merged) ? raw.merged.map(parseMerged).filter((x) => x !== null) : [];
7415
+ const slowStarts = isObject8(raw.slowStarts) ? Object.fromEntries(Object.entries(raw.slowStarts).filter((e) => typeof e[1] === "number" && e[1] > 0)) : {};
7390
7416
  return {
7391
7417
  runs: raw.runs.map(parseRun).filter((r) => r !== null),
7392
7418
  ...decisions.length ? { decisions } : {},
7393
- ...merged.length ? { merged } : {}
7419
+ ...merged.length ? { merged } : {},
7420
+ ...Object.keys(slowStarts).length ? { slowStarts } : {}
7394
7421
  };
7395
7422
  } catch {
7396
7423
  return { runs: [] };
@@ -9258,6 +9285,8 @@ var defaultExec = (cmd, args, o) => {
9258
9285
  }
9259
9286
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
9260
9287
  };
9288
+ var isTimeout = (msg) => /\btime[ds]?[\s_-]?out\b|ETIMEDOUT/i.test(msg);
9289
+ var UNWATCHED = "the agent may be running in it unwatched";
9261
9290
  var shq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
9262
9291
  var agentShellCommand = (o) => {
9263
9292
  const prompt = `"$(cat ${shq(o.promptFile)})"`;
@@ -9350,6 +9379,18 @@ var cmuxIdForRef = (stdout, ref, name) => {
9350
9379
  }
9351
9380
  return null;
9352
9381
  };
9382
+ var cmuxIdForName = (stdout, name) => {
9383
+ let found = null;
9384
+ for (const words of namedLines(stdout, name)) {
9385
+ const id = words[1]?.match(UUID)?.[0];
9386
+ if (!id || found)
9387
+ return null;
9388
+ found = id.toLowerCase();
9389
+ }
9390
+ return found;
9391
+ };
9392
+ var namedLines = (stdout, name) => stdout.split(`
9393
+ `).map((line) => line.trim().replace(/^\*\s*/, "").split(/\s+/)).filter((words) => words.slice(2).includes(name));
9353
9394
  var cmuxWorkspaceListed = (status, stdout, id) => status !== 0 || stdout.toLowerCase().includes(id.toLowerCase());
9354
9395
  var cmuxGroupName = (g) => g.title.trim() ? `${g.key} · ${g.title.trim()}` : g.key;
9355
9396
  var str7 = (v) => typeof v === "string" && v ? v : undefined;
@@ -9410,6 +9451,28 @@ var cmuxProbeReason = (r) => {
9410
9451
  const why = line.replace(/^(cmux:\s*)?(error:\s*)?/i, "").trim();
9411
9452
  return `cmux: ${why || `exit ${r.status}`}`;
9412
9453
  };
9454
+ var cmuxAuthRefused = (r) => r.status !== 0 && /\b(auth_required|unauthorized|authentication required|invalid password)\b/i.test(`${r.stderr}
9455
+ ${r.stdout}`);
9456
+ var CMUX_SETTLE_MS = 300;
9457
+ var CMUX_ENTER_RETRIES = 2;
9458
+ var PROMPT_LINE = /^[\s│|]*[>❯›]\s?(.*)$/;
9459
+ var PASTED = /\[Pasted text\b/i;
9460
+ var cmuxInputPending = (screen, text) => {
9461
+ const squash = (s) => s.replace(/\s+/g, " ").trim();
9462
+ const head = squash(text.split(`
9463
+ `).find((l) => l.trim()) ?? "").slice(0, 24);
9464
+ let input = null;
9465
+ for (const line of screen.split(`
9466
+ `)) {
9467
+ const m = line.match(PROMPT_LINE);
9468
+ if (m)
9469
+ input = squash((m[1] ?? "").replace(/│\s*$/, ""));
9470
+ }
9471
+ if (!input || !head)
9472
+ return false;
9473
+ return PASTED.test(input) || input.startsWith(head) || input.length >= 8 && head.startsWith(input);
9474
+ };
9475
+ var sleepSync = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
9413
9476
  var CMUX_LANE = {
9414
9477
  working: "working",
9415
9478
  parked: "needs-attention",
@@ -9419,18 +9482,26 @@ var CMUX_LANE = {
9419
9482
  var launcherPath = (promptFile) => logPathFor(promptFile, "cmux.sh");
9420
9483
  var exitPath = (promptFile) => logPathFor(promptFile, "cmux.exit");
9421
9484
  function cmuxRunner(exec, deps = {}) {
9485
+ const sleep = deps.sleep ?? sleepSync;
9422
9486
  const writeFile = deps.writeFile ?? ((p, t) => writeFileSync10(p, t, { mode: 384 }));
9423
9487
  const exists = deps.exists ?? existsSync11;
9424
9488
  const remove = deps.remove ?? ((p) => rmSync2(p, { force: true }));
9425
9489
  const bin = resolveCmuxBin(exists);
9426
9490
  const password = deps.password ?? (() => readCmuxPassword(exec));
9427
- let env = null;
9491
+ let pw;
9492
+ const run = (args) => pw ? exec(bin, args, { env: { CMUX_SOCKET_PASSWORD: pw } }) : exec(bin, args);
9428
9493
  const cmux = (args) => {
9429
- if (env === null) {
9430
- const pw = password();
9431
- env = pw ? { CMUX_SOCKET_PASSWORD: pw } : undefined;
9432
- }
9433
- return env ? exec(bin, args, { env }) : exec(bin, args);
9494
+ const fresh = pw === undefined;
9495
+ if (fresh)
9496
+ pw = password();
9497
+ const r = run(args);
9498
+ if (fresh || !cmuxAuthRefused(r))
9499
+ return r;
9500
+ const now = password();
9501
+ if (now === pw)
9502
+ return r;
9503
+ pw = now;
9504
+ return pw ? run(args) : r;
9434
9505
  };
9435
9506
  const failed = (r) => r.stderr.trim() || r.stdout.trim() || `exit ${r.status}`;
9436
9507
  return {
@@ -9467,19 +9538,27 @@ function cmuxRunner(exec, deps = {}) {
9467
9538
  joined = undefined;
9468
9539
  r = cmux(args);
9469
9540
  }
9470
- if (r.status !== 0)
9471
- throw new Error(`cmux new-workspace ${o.name} failed: ${failed(r)}`);
9541
+ if (r.status !== 0) {
9542
+ const why = failed(r);
9543
+ const l = cmux(["--id-format", "both", "list-workspaces"]);
9544
+ const stray = l.status === 0 ? cmuxIdForName(l.stdout, o.name) : null;
9545
+ const left = l.status !== 0 || (stray ? cmux(["close-workspace", "--workspace", stray]).status !== 0 : namedLines(l.stdout, o.name).length > 0);
9546
+ throw new Error(`cmux new-workspace ${o.name} failed: ${why}${left ? `; ${UNWATCHED}` : ""}`);
9547
+ }
9472
9548
  let id = parseCmuxWorkspace(r.stdout);
9473
9549
  const ref = id ? null : parseCmuxRef(r.stdout);
9550
+ let closed = false;
9474
9551
  if (ref) {
9475
9552
  const l = cmux(["--id-format", "both", "list-workspaces"]);
9476
- if (l.status === 0)
9553
+ if (l.status === 0) {
9477
9554
  id = cmuxIdForRef(l.stdout, ref, o.name);
9478
- if (!id)
9479
- cmux(["close-workspace", "--workspace", ref]);
9555
+ const stray = id ? null : cmuxIdForName(l.stdout, o.name);
9556
+ if (stray)
9557
+ closed = cmux(["close-workspace", "--workspace", stray]).status === 0;
9558
+ }
9480
9559
  }
9481
9560
  if (!id) {
9482
- throw new Error(ref ? `cmux new-workspace ${o.name} gave ${ref} but no workspace id for it; that workspace was closed` : `cmux new-workspace ${o.name} returned no workspace (${r.stdout.trim() || "nothing"}); the agent may be running in it unwatched`);
9561
+ throw new Error(ref ? `cmux new-workspace ${o.name} gave ${ref} but no workspace id for it; ${closed ? "that workspace was closed" : UNWATCHED}` : `cmux new-workspace ${o.name} returned no workspace (${r.stdout.trim() || "nothing"}); ${UNWATCHED}`);
9483
9562
  }
9484
9563
  if (group && listed && !joined) {
9485
9564
  const c = cmux(["workspace-group", "create", "--name", group, "--from", id, "--json"]);
@@ -9505,9 +9584,26 @@ function cmuxRunner(exec, deps = {}) {
9505
9584
  const r = cmux(["send", "--workspace", h.id, "--", text]);
9506
9585
  if (r.status !== 0)
9507
9586
  throw new Error(`cmux send ${h.id} failed: ${failed(r)}`);
9508
- const k = cmux(["send-key", "--workspace", h.id, "enter"]);
9509
- if (k.status !== 0)
9510
- throw new Error(`cmux send-key ${h.id} failed: ${failed(k)}`);
9587
+ const enter = () => {
9588
+ const k = cmux(["send-key", "--workspace", h.id, "enter"]);
9589
+ if (k.status !== 0)
9590
+ throw new Error(`cmux send-key ${h.id} failed: ${failed(k)}`);
9591
+ };
9592
+ sleep(CMUX_SETTLE_MS);
9593
+ enter();
9594
+ const pending = () => {
9595
+ sleep(CMUX_SETTLE_MS);
9596
+ const s = cmux(["read-screen", "--workspace", h.id, "--lines", "20"]);
9597
+ return s.status === 0 && cmuxInputPending(s.stdout, text);
9598
+ };
9599
+ let again = 0;
9600
+ while (again < CMUX_ENTER_RETRIES && pending()) {
9601
+ enter();
9602
+ again++;
9603
+ }
9604
+ if (again === 0)
9605
+ return;
9606
+ return pending() ? `the Enter was swallowed; pressed it ${again} more times and the text is still in the input` : `the Enter was swallowed; pressed it again${again > 1 ? ` (${again}×)` : ""}`;
9511
9607
  },
9512
9608
  stop(h) {
9513
9609
  cmux(["close-workspace", "--workspace", h.id]);
@@ -9788,6 +9884,7 @@ function noteEvent(line) {
9788
9884
  }
9789
9885
 
9790
9886
  // src/engine/gh.ts
9887
+ init_src();
9791
9888
  var isGhError = (r) => r !== null && ("error" in r);
9792
9889
  function prForBranch(exec, repoPath, branch) {
9793
9890
  try {
@@ -9884,6 +9981,7 @@ function parsePrView(stdout) {
9884
9981
  pending,
9885
9982
  reviewDecision: typeof o.reviewDecision === "string" ? o.reviewDecision : "",
9886
9983
  feedback: 0,
9984
+ authorFeedback: 0,
9887
9985
  ...typeof o.headRefOid === "string" && o.headRefOid ? { head: o.headRefOid } : {},
9888
9986
  ...typeof mergeCommit === "string" && mergeCommit ? { mergeCommit } : {}
9889
9987
  };
@@ -9892,22 +9990,39 @@ function prRef(url) {
9892
9990
  const m = /github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/.exec(url);
9893
9991
  return m?.[1] && m[2] ? { repo: m[1], number: Number(m[2]) } : null;
9894
9992
  }
9993
+ function countFeedback(stdout, author) {
9994
+ const n = { all: 0, author: 0 };
9995
+ for (const line of stdout.split(`
9996
+ `)) {
9997
+ const [login = "", marked, hasBody] = line.trim().split("\t");
9998
+ if (!login || marked === "true")
9999
+ continue;
10000
+ if (login === author && hasBody !== "true")
10001
+ continue;
10002
+ n.all++;
10003
+ if (login === author)
10004
+ n.author++;
10005
+ }
10006
+ return n;
10007
+ }
10008
+ var FEEDBACK_JQ = `.[] | select(.user.type != "Bot") | [.user.login, ((.body // "") | contains("${ENGINE_MARK}") | tostring), ((.body // "") | length > 0 | tostring)] | @tsv`;
9895
10009
  function prFeedback(exec, repoPath, url, author) {
9896
10010
  const ref = prRef(url);
9897
10011
  if (!ref)
9898
10012
  return null;
9899
- let total = 0;
10013
+ const total = { all: 0, author: 0 };
9900
10014
  for (const path of [
9901
10015
  `repos/${ref.repo}/issues/${ref.number}/comments`,
9902
10016
  `repos/${ref.repo}/pulls/${ref.number}/reviews`,
9903
10017
  `repos/${ref.repo}/pulls/${ref.number}/comments`
9904
10018
  ]) {
9905
10019
  try {
9906
- const r = exec("gh", ["api", path, "--paginate", "--jq", '.[] | select(.user.type != "Bot") | .user.login'], { cwd: repoPath });
10020
+ const r = exec("gh", ["api", path, "--paginate", "--jq", FEEDBACK_JQ], { cwd: repoPath });
9907
10021
  if (r.status !== 0)
9908
10022
  return null;
9909
- total += r.stdout.split(`
9910
- `).filter((l) => l.trim() && l.trim() !== author).length;
10023
+ const n = countFeedback(r.stdout, author);
10024
+ total.all += n.all;
10025
+ total.author += n.author;
9911
10026
  } catch {
9912
10027
  return null;
9913
10028
  }
@@ -9927,7 +10042,7 @@ function prView(exec, repoPath, url) {
9927
10042
  if (!view || view.state !== "OPEN")
9928
10043
  return view;
9929
10044
  const feedback = prFeedback(exec, repoPath, url, view.author);
9930
- return feedback === null ? view : { ...view, feedback };
10045
+ return feedback === null ? view : { ...view, feedback: feedback.all, authorFeedback: feedback.author };
9931
10046
  } catch {
9932
10047
  return null;
9933
10048
  }
@@ -10400,6 +10515,17 @@ class SeatTokenError extends Error {
10400
10515
  this.name = "SeatTokenError";
10401
10516
  }
10402
10517
  }
10518
+
10519
+ class ReportRefusedError extends Error {
10520
+ status;
10521
+ final;
10522
+ constructor(status, message, final = false) {
10523
+ super(message);
10524
+ this.status = status;
10525
+ this.final = final;
10526
+ this.name = "ReportRefusedError";
10527
+ }
10528
+ }
10403
10529
  async function readOrSay(client, path, what) {
10404
10530
  if (!client.getFull) {
10405
10531
  const r = await client.get(path);
@@ -10475,7 +10601,11 @@ function sledItems(r, armedOnly, engine) {
10475
10601
  }
10476
10602
  return out;
10477
10603
  }
10478
- var HOLD_DEFAULTS = { holdCpu: 85, holdLoad: 1.5, holdPressure: "warn" };
10604
+ var HOLD_DEFAULTS2 = {
10605
+ holdCpu: HOLD_DEFAULTS.holdCpu,
10606
+ holdLoad: HOLD_DEFAULTS.holdLoad,
10607
+ holdPressure: HOLD_DEFAULTS.holdPressure
10608
+ };
10479
10609
  var PROFILE_TEXT = [
10480
10610
  "runner",
10481
10611
  "launcher",
@@ -10498,7 +10628,7 @@ function profileOf(r) {
10498
10628
  const p = r.profile;
10499
10629
  const out = {
10500
10630
  pickup: p.pickup === "auto" ? "auto" : "approve",
10501
- ...HOLD_DEFAULTS,
10631
+ ...HOLD_DEFAULTS2,
10502
10632
  holdOff: p.holdOff === true
10503
10633
  };
10504
10634
  for (const k of PROFILE_TEXT) {
@@ -10795,8 +10925,12 @@ function sledsSource(client, o) {
10795
10925
  throw new Error(`${UNREACHABLE2} — ${claim.item}/${claim.node} ${outcome.kind} not recorded`);
10796
10926
  if (r.status === 409 && path === "/proxy/run")
10797
10927
  return;
10798
- if (r.status >= 300)
10799
- throw new Error(`${claim.item}/${claim.node} ${outcome.kind}: ${serverMessage(r)}`);
10928
+ if (r.status < 300)
10929
+ return;
10930
+ if (r.status === 401 || r.status === 403 || r.status === 404 && !r.json)
10931
+ throw new SeatTokenError(r.status);
10932
+ const final = r.json !== null && (r.status === 404 || r.status === 400 && r.json.badState === true);
10933
+ throw new ReportRefusedError(r.status, `${claim.item}/${claim.node} ${outcome.kind}: ${serverMessage(r)}`, final);
10800
10934
  },
10801
10935
  async openDecision(item, node) {
10802
10936
  const route = await routeOf(item);
@@ -10879,6 +11013,14 @@ function sledsSource(client, o) {
10879
11013
  ] : [];
10880
11014
  });
10881
11015
  },
11016
+ async prRead(item, node, read) {
11017
+ try {
11018
+ const r = await client.postFull("/proxy/prread", { project: item, node, ...read });
11019
+ return !!r && r.status >= 200 && r.status < 300;
11020
+ } catch {
11021
+ return false;
11022
+ }
11023
+ },
10882
11024
  async answerDecision(a, engine, answer) {
10883
11025
  const r = await client.postFull("/proxy/run", { runId: a.runId, engine, answer });
10884
11026
  if (!r)
@@ -10997,6 +11139,13 @@ function removeBranch(exec, repoPath, branch, head) {
10997
11139
  }
10998
11140
 
10999
11141
  // src/engine/loop.ts
11142
+ function profileOnce(read) {
11143
+ let p;
11144
+ return () => {
11145
+ p ??= read ? read().catch(() => null) : Promise.resolve(null);
11146
+ return p;
11147
+ };
11148
+ }
11000
11149
  function ciLine(v) {
11001
11150
  if (v.failing.length)
11002
11151
  return `failing: ${v.failing.join(", ")}`;
@@ -11090,6 +11239,7 @@ async function tick(deps) {
11090
11239
  } finally {
11091
11240
  if (d !== deps) {
11092
11241
  deps.runnerHold = d.runnerHold;
11242
+ deps.startBackoff = d.startBackoff;
11093
11243
  deps.atOnce = d.atOnce;
11094
11244
  }
11095
11245
  }
@@ -11115,6 +11265,7 @@ async function tick(deps) {
11115
11265
  return report;
11116
11266
  }
11117
11267
  async function tickOnce(d) {
11268
+ d.tickProfile = profileOnce(d.readProfile);
11118
11269
  const state = d.readState();
11119
11270
  const seat = seatNow(d);
11120
11271
  if (seat.changed) {
@@ -11130,6 +11281,8 @@ async function tickOnce(d) {
11130
11281
  const decided = [];
11131
11282
  const save = () => d.writeState(state);
11132
11283
  probeRunner(d);
11284
+ if (d.startBackoff && --d.startBackoff.checks <= 0)
11285
+ d.startBackoff = undefined;
11133
11286
  const keep = [];
11134
11287
  for (const run of state.runs) {
11135
11288
  const claim = claimOf(run);
@@ -11284,7 +11437,7 @@ async function tickOnce(d) {
11284
11437
  const answers = answersOf(d.source);
11285
11438
  if (answers) {
11286
11439
  for (const run of state.runs)
11287
- if (run.parked && !run.parked.answerer && !d.runnerHold) {
11440
+ if (run.parked && !run.parked.answerer && !startsHeld(d)) {
11288
11441
  await startAnswerer(d, answers, run);
11289
11442
  save();
11290
11443
  }
@@ -11301,7 +11454,7 @@ async function tickOnce(d) {
11301
11454
  const items = await d.source.read();
11302
11455
  const takeNone = seat.gone;
11303
11456
  for (const it of items)
11304
- if (it.draft && !takeNone && !d.runnerHold)
11457
+ if (it.draft && !takeNone && !startsHeld(d))
11305
11458
  await startDraft(d, it.draft.claim, it.draft.why, state, started, finished);
11306
11459
  const live = new Map(items.map((i) => [i.item, i]));
11307
11460
  const unheld = state.runs.filter((r) => r.parked && (live.get(r.item)?.landed ?? true));
@@ -11319,9 +11472,16 @@ async function tickOnce(d) {
11319
11472
  state.decisions = (state.decisions ?? []).filter((x) => alive(x) || x.answerer);
11320
11473
  save();
11321
11474
  }
11475
+ const counted = Object.keys(state.slowStarts ?? {});
11476
+ const gone = counted.filter((k) => !alive({ item: k.slice(0, k.lastIndexOf("/")) }));
11477
+ if (gone.length) {
11478
+ for (const k of gone)
11479
+ forgetSlowStarts(state, k);
11480
+ save();
11481
+ }
11322
11482
  await rereadAtOnce(d);
11323
11483
  const cap = await machineCap(d);
11324
- const atOnce = d.runnerHold ? 0 : cap;
11484
+ const atOnce = startsHeld(d) ? 0 : cap;
11325
11485
  const takes = planTakes({
11326
11486
  items,
11327
11487
  tracked: [
@@ -11349,7 +11509,7 @@ async function tickOnce(d) {
11349
11509
  return p;
11350
11510
  };
11351
11511
  for (const t of takeNone ? [] : takes) {
11352
- if (d.runnerHold)
11512
+ if (startsHeld(d))
11353
11513
  break;
11354
11514
  const at = ref(t);
11355
11515
  let claim;
@@ -11455,10 +11615,26 @@ async function tickOnce(d) {
11455
11615
  await report({ kind: "release", why: `runner unavailable: ${down}` });
11456
11616
  continue;
11457
11617
  }
11618
+ const slow = slowStart(d, message(e));
11619
+ const inRow = (state.slowStarts?.[at] ?? 0) + 1;
11620
+ if (slow && inRow <= SLOW_START_RELEASES) {
11621
+ d.log(`${at}: released — ${slow}: ${message(e)}; backing off one check`);
11622
+ d.startBackoff = { why: slow, checks: 2 };
11623
+ state.slowStarts = { ...state.slowStarts, [at]: inRow };
11624
+ save();
11625
+ await report({ kind: "release", why: slow });
11626
+ continue;
11627
+ }
11628
+ forgetSlowStarts(state, at);
11629
+ save();
11630
+ if (slow)
11631
+ d.log(`${at}: ${slow} ${inRow} times in a row — failed, not released again`);
11458
11632
  d.log(`${at}: could not start: ${message(e)}`);
11459
11633
  await report({ kind: "fail", log: `The engine could not start the session: ${message(e)}` });
11460
11634
  continue;
11461
11635
  }
11636
+ if (forgetSlowStarts(state, at))
11637
+ save();
11462
11638
  const watch = safeWatch(d.runner, handle);
11463
11639
  const run = {
11464
11640
  item: claim.item,
@@ -11625,12 +11801,34 @@ function probeRunner(d) {
11625
11801
  }
11626
11802
  return why;
11627
11803
  }
11804
+ function profileOf2(d) {
11805
+ return d.tickProfile ?? profileOnce(d.readProfile);
11806
+ }
11807
+ function forgetSlowStarts(state, at) {
11808
+ if (!state.slowStarts || !(at in state.slowStarts))
11809
+ return false;
11810
+ const { [at]: _, ...rest } = state.slowStarts;
11811
+ state.slowStarts = Object.keys(rest).length ? rest : undefined;
11812
+ return true;
11813
+ }
11814
+ var startsHeld = (d) => Boolean(d.runnerHold || d.startBackoff);
11815
+ var SLOW_START_RELEASES = 3;
11816
+ function slowStart(d, msg) {
11817
+ if (msg.includes(UNWATCHED))
11818
+ return null;
11819
+ if (isTimeout(msg))
11820
+ return "the runner start timed out";
11821
+ const hold = d.machine?.hold;
11822
+ if (hold)
11823
+ return `the runner start failed with the machine busy (${hold.signals.join(", ")})`;
11824
+ return null;
11825
+ }
11628
11826
  async function rereadAtOnce(d) {
11629
11827
  if (!d.rereadAtOnce)
11630
11828
  return;
11631
11829
  let next = null;
11632
11830
  try {
11633
- next = await d.rereadAtOnce();
11831
+ next = await d.rereadAtOnce(profileOf2(d));
11634
11832
  } catch {}
11635
11833
  if (next === null || next === d.atOnce)
11636
11834
  return;
@@ -11642,7 +11840,7 @@ async function rereadMachine(w, d) {
11642
11840
  return;
11643
11841
  let next = null;
11644
11842
  try {
11645
- next = await w.reread();
11843
+ next = await w.reread(profileOf2(d));
11646
11844
  } catch {}
11647
11845
  if (!next)
11648
11846
  return;
@@ -11700,7 +11898,9 @@ async function resume(d, held, claim, state, started) {
11700
11898
  d.log(`${at}: answered, but the parked session is gone — starting a new one`);
11701
11899
  return false;
11702
11900
  }
11703
- runner.send(held.handle, claim.answer);
11901
+ const note = runner.send(held.handle, claim.answer);
11902
+ if (note)
11903
+ d.log(`${at}: answered — ${note}`);
11704
11904
  } catch (e) {
11705
11905
  d.log(`${at}: answered, but the session could not be told (${message(e)}) — starting a new one`);
11706
11906
  stopOpen(d, held, runner);
@@ -11817,7 +12017,7 @@ async function roleLaunchFor(d, at, role) {
11817
12017
  };
11818
12018
  if (d.rereadRoles) {
11819
12019
  try {
11820
- const now = await d.rereadRoles();
12020
+ const now = await d.rereadRoles(profileOf2(d));
11821
12021
  if (now !== null)
11822
12022
  roles = now;
11823
12023
  } catch {}
@@ -12175,8 +12375,9 @@ function endDraft(d, run, runner) {
12175
12375
  function ciNudge(url, failing) {
12176
12376
  return `CI failed on your PR (${failing.join(", ")}): ${url}. Read the failing logs (gh pr checks ${url}), fix the cause, run the repo's gate, and push to the same branch.`;
12177
12377
  }
12378
+ var SELF_NUDGES = 2;
12178
12379
  function reviewNudge(url, count) {
12179
- return `New review feedback on your PR (${count} ${count === 1 ? "comment" : "comments"} from people so far): ${url}. Read all of it (gh pr view ${url} --comments, and the inline threads), answer each comment on its own thread with the fix or a reasoned reply, push, and leave the threads for the reviewer to resolve.`;
12380
+ return `New review feedback on your PR (${count} ${count === 1 ? "comment" : "comments"} from people so far): ${url}. Read all of it (gh pr view ${url} --comments, and the inline threads), answer each comment on its own thread with the fix or a reasoned reply, push, and leave the threads for the reviewer to resolve. End every comment and reply you post on the PR with the line ${ENGINE_MARK} so the engine knows it is yours and not the reviewer's.`;
12180
12381
  }
12181
12382
  async function follow(d, state, run, claim, runner, finished, attention, changes, decided) {
12182
12383
  const pr = run.pr;
@@ -12213,8 +12414,8 @@ async function follow(d, state, run, claim, runner, finished, attention, changes
12213
12414
  try {
12214
12415
  alive = runner.status(run.handle) === "running";
12215
12416
  if (alive) {
12216
- runner.send(run.handle, text);
12217
- d.log(`${ref(run)}: ${what} — told the session`);
12417
+ const note = runner.send(run.handle, text);
12418
+ d.log(`${ref(run)}: ${what} — told the session${note ? ` (${note})` : ""}`);
12218
12419
  return;
12219
12420
  }
12220
12421
  } catch {}
@@ -12231,16 +12432,38 @@ async function follow(d, state, run, claim, runner, finished, attention, changes
12231
12432
  pr.resolving = undefined;
12232
12433
  pr.conflicted = undefined;
12233
12434
  }
12234
- if (view.feedback > pr.feedback)
12235
- tell(reviewNudge(pr.url, view.feedback), `${view.feedback - pr.feedback} new review comment(s)`);
12435
+ if (view.feedback > pr.feedback) {
12436
+ const what = `${view.feedback - pr.feedback} new review comment(s)`;
12437
+ const authorBefore = pr.authorFeedback ?? 0;
12438
+ const othersGrew = view.feedback - view.authorFeedback > pr.feedback - authorBefore;
12439
+ const selfNudges = othersGrew ? 0 : (pr.selfNudges ?? 0) + 1;
12440
+ if (selfNudges > SELF_NUDGES) {
12441
+ d.log(`${ref(run)}: ${what}, only the PR author's own after ${SELF_NUDGES} nudges in a row — needs a person (the session may be answering itself): ${pr.url}`);
12442
+ attention.push({
12443
+ ...at,
12444
+ what: `${what} from the PR's author only, after ${SELF_NUDGES} nudges: is the session answering itself?`
12445
+ });
12446
+ } else {
12447
+ tell(reviewNudge(pr.url, view.feedback), what);
12448
+ pr.selfNudges = selfNudges || undefined;
12449
+ }
12450
+ }
12236
12451
  pr.failing = view.failing;
12237
12452
  pr.feedback = Math.max(pr.feedback, view.feedback);
12453
+ pr.authorFeedback = Math.max(pr.authorFeedback ?? 0, view.authorFeedback) || undefined;
12238
12454
  const clear = view.failing.length === 0 && view.pending === 0 && view.reviewDecision === "APPROVED";
12239
12455
  if (clear && !pr.clear) {
12240
12456
  d.log(`${ref(run)}: ${pr.url} is green and approved — ready for your merge call`);
12241
12457
  attention.push({ ...at, what: "green and approved: ready for a merge" });
12242
12458
  }
12243
12459
  pr.clear = clear;
12460
+ const prRead = d.source.prRead;
12461
+ const posted = `${ci}|${review}|${clear}`;
12462
+ if (prRead && posted !== pr.posted) {
12463
+ const read = { ci, ...review ? { review } : {}, clear };
12464
+ if (await prRead.call(d.source, run.item, run.node, read))
12465
+ pr.posted = posted;
12466
+ }
12244
12467
  return true;
12245
12468
  }
12246
12469
  function rippleRunnerWorktree(d, run, runner, head) {
@@ -12270,15 +12493,21 @@ async function ripple(d, state, run, claim, runner, url, finished, decided, head
12270
12493
  ...(state.merged ?? []).filter((m) => !(m.item === run.item && m.node === run.node)),
12271
12494
  { item: run.item, node: run.node, url, repoPath: run.repoPath }
12272
12495
  ];
12496
+ let landed = true;
12273
12497
  try {
12274
12498
  await d.source.report(claim, outcome, d.engine);
12275
12499
  } catch (e) {
12276
- d.log(`${ref(run)}: merged (${url}), but the report failed — retrying next tick: ${message(e)}`);
12277
- return false;
12500
+ if (!(e instanceof ReportRefusedError && e.final)) {
12501
+ d.log(`${ref(run)}: merged (${url}), but the report failed — retrying next tick: ${message(e)}`);
12502
+ return false;
12503
+ }
12504
+ d.log(`${ref(run)}: merged (${url}), but SLEDS refused the merged report (HTTP ${e.status}) — not retrying: ${message(e)}`);
12505
+ landed = false;
12278
12506
  }
12279
12507
  finished.push({ item: run.item, node: run.node, outcome });
12280
12508
  setLane(d, runner, run.handle, "done");
12281
- await openLanded(d, state, run, url, mergeCommit, decided);
12509
+ if (landed)
12510
+ await openLanded(d, state, run, url, mergeCommit, decided);
12282
12511
  try {
12283
12512
  if (runner.status(run.handle) === "running")
12284
12513
  runner.stop(run.handle);
@@ -12333,7 +12562,7 @@ function eventAnswerNudge(e, answer) {
12333
12562
 
12334
12563
  ${answer}
12335
12564
 
12336
- On your own branch, by a merge commit — never a rebase that needs a force-push — run the repo's gate, push, and comment on the PR how each file was resolved.`;
12565
+ On your own branch, by a merge commit — never a rebase that needs a force-push — run the repo's gate, push, and comment on the PR how each file was resolved, ending the comment with the line ${ENGINE_MARK}.`;
12337
12566
  if (e.kind === "conflict")
12338
12567
  return `The decider on your PR's conflict with ${e.sibling} (${e.pr}):
12339
12568
 
@@ -12563,9 +12792,13 @@ function deliver(d, state, dec, answer, attention, decided) {
12563
12792
  const url = dec.event.pr;
12564
12793
  if (kind === "conflict")
12565
12794
  conflictStep(run, "decidedAt", d.now());
12795
+ let status;
12566
12796
  try {
12567
- if (run && runner && runner.status(run.handle) === "running") {
12568
- runner.send(run.handle, eventAnswerNudge(dec.event, answer));
12797
+ status = run && runner ? runner.status(run.handle) : undefined;
12798
+ if (run && runner && status === "running") {
12799
+ const note = runner.send(run.handle, eventAnswerNudge(dec.event, answer));
12800
+ if (note)
12801
+ d.log(`${ref(run)}: ${KIND_SAYS[kind]} answered — ${note}`);
12569
12802
  if (run.pr && baseConflict(dec.event))
12570
12803
  run.pr.resolving = dec.event.baseSha;
12571
12804
  if (kind === "conflict")
@@ -12581,7 +12814,8 @@ function deliver(d, state, dec, answer, attention, decided) {
12581
12814
  }
12582
12815
  } catch {}
12583
12816
  const event = dec.event;
12584
- if (run && baseConflict(event) && startResolver(d, run, event, answer)) {
12817
+ const gone = status !== undefined && status !== "running";
12818
+ if (run && gone && baseConflict(event) && startResolver(d, run, event, answer)) {
12585
12819
  conflictStep(run, "fixingAt", d.now());
12586
12820
  decided.push({
12587
12821
  item: dec.item,
@@ -12670,6 +12904,9 @@ function startResolver(d, run, e, answer) {
12670
12904
  if (!runner || runner.makesWorktree || run.handle.cwd === run.repoPath)
12671
12905
  return false;
12672
12906
  const name = sessionName(run.item, `${run.node}-resolve`);
12907
+ try {
12908
+ runner.stop(run.handle);
12909
+ } catch {}
12673
12910
  try {
12674
12911
  const launch = nodeLaunchFor(d, ref(run));
12675
12912
  run.handle = runner.start({
@@ -12680,7 +12917,9 @@ function startResolver(d, run, e, answer) {
12680
12917
  base: e.base,
12681
12918
  promptFile: d.writePrompt(name, eventAnswerNudge(e, answer)),
12682
12919
  permissionMode: d.permissionMode,
12683
- ...launchOptions(launch)
12920
+ ...launchOptions(launch),
12921
+ ...run.mcpConfig ? { mcpConfig: run.mcpConfig, ...d.strictMcp ? { strictMcp: true } : {} } : {},
12922
+ ...run.group ? { group: run.group } : {}
12684
12923
  });
12685
12924
  } catch (err) {
12686
12925
  d.log(`${ref(run)}: could not start a resolver session: ${message(err)}`);
@@ -12834,7 +13073,7 @@ async function settleEvent(d, events, state, dec, attention, decided) {
12834
13073
  if (!dec.answerer && baseConflict(dec.event) && events.answerDecision)
12835
13074
  return answerBaseConflict(d, events, state, dec, attention, decided);
12836
13075
  if (!dec.answerer)
12837
- return d.runnerHold ? true : startEventAnswerer(d, events, state, dec, decided);
13076
+ return startsHeld(d) ? true : startEventAnswerer(d, events, state, dec, decided);
12838
13077
  const a = dec.answerer;
12839
13078
  const runner = runnerFor(d, a.handle);
12840
13079
  if (!runner) {
@@ -13542,17 +13781,17 @@ function flagsFromProfile(flags, given, profile, home) {
13542
13781
  }
13543
13782
  if (profile.holdCpu !== undefined && !has("hold-cpu")) {
13544
13783
  out.holdCpu = profile.holdCpu;
13545
- if (profile.holdCpu !== HOLD_DEFAULTS.holdCpu)
13784
+ if (profile.holdCpu !== HOLD_DEFAULTS2.holdCpu)
13546
13785
  used.push(`hold at cpu ${profile.holdCpu}%`);
13547
13786
  }
13548
13787
  if (profile.holdLoad !== undefined && !has("hold-load")) {
13549
13788
  out.holdLoad = profile.holdLoad;
13550
- if (profile.holdLoad !== HOLD_DEFAULTS.holdLoad)
13789
+ if (profile.holdLoad !== HOLD_DEFAULTS2.holdLoad)
13551
13790
  used.push(`hold at load ${profile.holdLoad}× cores`);
13552
13791
  }
13553
13792
  if (profile.holdPressure && !has("hold-pressure")) {
13554
13793
  out.holdPressure = profile.holdPressure;
13555
- if (profile.holdPressure !== HOLD_DEFAULTS.holdPressure)
13794
+ if (profile.holdPressure !== HOLD_DEFAULTS2.holdPressure)
13556
13795
  used.push(`hold at memory pressure ${profile.holdPressure}`);
13557
13796
  }
13558
13797
  if (profile.holdOff !== undefined && !has("hold")) {
@@ -13565,10 +13804,10 @@ function flagsFromProfile(flags, given, profile, home) {
13565
13804
  function machineSettings(flags) {
13566
13805
  return {
13567
13806
  thresholds: {
13568
- cpuPct: flags.holdCpu ?? HOLD_DEFAULTS.holdCpu,
13569
- loadPerCore: flags.holdLoad ?? HOLD_DEFAULTS.holdLoad
13807
+ cpuPct: flags.holdCpu ?? HOLD_DEFAULTS2.holdCpu,
13808
+ loadPerCore: flags.holdLoad ?? HOLD_DEFAULTS2.holdLoad
13570
13809
  },
13571
- pressure: flags.holdPressure ?? HOLD_DEFAULTS.holdPressure,
13810
+ pressure: flags.holdPressure ?? HOLD_DEFAULTS2.holdPressure,
13572
13811
  off: flags.holdOff === true
13573
13812
  };
13574
13813
  }
@@ -15184,17 +15423,17 @@ function flagsFromProfile2(flags, given, profile, home) {
15184
15423
  }
15185
15424
  if (profile.holdCpu !== undefined && !has("hold-cpu")) {
15186
15425
  out.holdCpu = profile.holdCpu;
15187
- if (profile.holdCpu !== HOLD_DEFAULTS.holdCpu)
15426
+ if (profile.holdCpu !== HOLD_DEFAULTS2.holdCpu)
15188
15427
  used.push(`hold at cpu ${profile.holdCpu}%`);
15189
15428
  }
15190
15429
  if (profile.holdLoad !== undefined && !has("hold-load")) {
15191
15430
  out.holdLoad = profile.holdLoad;
15192
- if (profile.holdLoad !== HOLD_DEFAULTS.holdLoad)
15431
+ if (profile.holdLoad !== HOLD_DEFAULTS2.holdLoad)
15193
15432
  used.push(`hold at load ${profile.holdLoad}× cores`);
15194
15433
  }
15195
15434
  if (profile.holdPressure && !has("hold-pressure")) {
15196
15435
  out.holdPressure = profile.holdPressure;
15197
- if (profile.holdPressure !== HOLD_DEFAULTS.holdPressure)
15436
+ if (profile.holdPressure !== HOLD_DEFAULTS2.holdPressure)
15198
15437
  used.push(`hold at memory pressure ${profile.holdPressure}`);
15199
15438
  }
15200
15439
  if (profile.holdOff !== undefined && !has("hold")) {
@@ -15207,15 +15446,15 @@ function flagsFromProfile2(flags, given, profile, home) {
15207
15446
  function machineSettings2(flags) {
15208
15447
  return {
15209
15448
  thresholds: {
15210
- cpuPct: flags.holdCpu ?? HOLD_DEFAULTS.holdCpu,
15211
- loadPerCore: flags.holdLoad ?? HOLD_DEFAULTS.holdLoad
15449
+ cpuPct: flags.holdCpu ?? HOLD_DEFAULTS2.holdCpu,
15450
+ loadPerCore: flags.holdLoad ?? HOLD_DEFAULTS2.holdLoad
15212
15451
  },
15213
- pressure: flags.holdPressure ?? HOLD_DEFAULTS.holdPressure,
15452
+ pressure: flags.holdPressure ?? HOLD_DEFAULTS2.holdPressure,
15214
15453
  off: flags.holdOff === true
15215
15454
  };
15216
15455
  }
15217
- async function rereadMachine2(client, flags, given) {
15218
- const profile = await readProfile(client);
15456
+ async function rereadMachine2(read, flags, given) {
15457
+ const profile = await read();
15219
15458
  return profile ? machineSettings2(flagsFromProfile2(flags, given, profile).flags) : null;
15220
15459
  }
15221
15460
  var EFFORT_SAYS2 = `${EFFORTS.slice(0, -1).join(", ")} or ${EFFORTS.at(-1)}`;
@@ -15230,8 +15469,8 @@ var ROLE_KEYS = [
15230
15469
  ["drafter-model", "drafterModel"],
15231
15470
  ["drafter-effort", "drafterEffort"]
15232
15471
  ];
15233
- async function rereadRoles(client, flags, given) {
15234
- const profile = ROLE_KEYS.every(([flag]) => given.includes(flag)) ? undefined : await readProfile(client);
15472
+ async function rereadRoles(read, flags, given) {
15473
+ const profile = ROLE_KEYS.every(([flag]) => given.includes(flag)) ? undefined : await read();
15235
15474
  if (profile === null)
15236
15475
  return null;
15237
15476
  const out = {};
@@ -15243,10 +15482,10 @@ async function rereadRoles(client, flags, given) {
15243
15482
  }
15244
15483
  return out;
15245
15484
  }
15246
- async function rereadAtOnce2(client, flags, given) {
15485
+ async function rereadAtOnce2(read, flags, given) {
15247
15486
  if (given.includes("at-once"))
15248
15487
  return flags.atOnce;
15249
- const profile = await readProfile(client);
15488
+ const profile = await read();
15250
15489
  return profile ? profile.atOnce ?? 1 : null;
15251
15490
  }
15252
15491
  var PROFILE_RETRY_MS2 = [2000, 5000];
@@ -15729,16 +15968,18 @@ var engineCmd = async (o) => {
15729
15968
  });
15730
15969
  }
15731
15970
  const deps = engineDeps2(o, flags, source, runner, `engine:${flags.name}`);
15732
- if (client && deps.machine) {
15971
+ if (client) {
15733
15972
  const c = client;
15973
+ deps.readProfile = () => readProfile(c);
15974
+ }
15975
+ if (client && deps.machine) {
15734
15976
  const started = flags;
15735
- deps.machine.reread = () => rereadMachine2(c, started, parsed.given);
15977
+ deps.machine.reread = (read) => rereadMachine2(read, started, parsed.given);
15736
15978
  }
15737
15979
  if (client) {
15738
- const c = client;
15739
15980
  const started = flags;
15740
- deps.rereadRoles = () => rereadRoles(c, started, parsed.given);
15741
- deps.rereadAtOnce = () => rereadAtOnce2(c, started, parsed.given);
15981
+ deps.rereadRoles = (read) => rereadRoles(read, started, parsed.given);
15982
+ deps.rereadAtOnce = (read) => rereadAtOnce2(read, started, parsed.given);
15742
15983
  }
15743
15984
  log(engineHeader2(terminalOf2(o).c, deps.engine, (source.name === "jira" ? `Jira epic ${flags.jira}` : pickup === "auto" ? "your sled, Pickup: Auto (it arms the next ready item)" : "the routes armed on your sled") + (flags.org ? ` · org ${flags.org}` : "") + (flags.seat ? ` · as ${flags.seat}` : ""), runner.name, flags));
15744
15985
  if (flags.strictMcp && flags.agent)
@@ -16878,8 +17119,8 @@ function withEstimated(rows, a) {
16878
17119
  function withActual(rows, a) {
16879
17120
  if (a.state === "started")
16880
17121
  return rows;
16881
- const key = sourceKey(a.source);
16882
- return rows.filter((r) => r.key !== key);
17122
+ const key = a.source ? sourceKey(a.source) : a.key;
17123
+ return key ? rows.filter((r) => r.key !== key) : rows;
16883
17124
  }
16884
17125
  function owedBySession(rows, sessionId) {
16885
17126
  return rows.filter((r) => r.sessionId === sessionId && r.askedAt === undefined);
@@ -16934,11 +17175,11 @@ async function usageTool2(deps) {
16934
17175
  }
16935
17176
  var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16936
17177
  function trackingClient(client, home, now) {
16937
- const after = (path, body, status) => {
16938
- if (status < 200 || status >= 300 || !isObj2(body.source))
17178
+ const after = (path, body, status, json) => {
17179
+ if (status < 200 || status >= 300)
16939
17180
  return;
16940
17181
  try {
16941
- if (path === "/proxy/estimate") {
17182
+ if (path === "/proxy/estimate" && isObj2(body.source)) {
16942
17183
  const spec = typeof body.spec === "string" ? body.spec : "";
16943
17184
  writeOwed(home, withEstimated(readOwed(home, now()), {
16944
17185
  source: body.source,
@@ -16947,9 +17188,12 @@ function trackingClient(client, home, now) {
16947
17188
  now: now(),
16948
17189
  sessionId: readSession(home)?.sessionId
16949
17190
  }));
16950
- } else if (path === "/proxy/estimate/actual") {
17191
+ } else if (path === "/proxy/estimate/actual" || path === "/proxy/land") {
17192
+ const key = typeof json?.key === "string" ? json.key : undefined;
17193
+ if (!isObj2(body.source) && !key)
17194
+ return;
16951
17195
  writeOwed(home, withActual(readOwed(home, now()), {
16952
- source: body.source,
17196
+ ...isObj2(body.source) ? { source: body.source } : { key },
16953
17197
  state: typeof body.state === "string" ? body.state : undefined
16954
17198
  }));
16955
17199
  }
@@ -16959,13 +17203,13 @@ function trackingClient(client, home, now) {
16959
17203
  async post(path, body) {
16960
17204
  const r = await client.post(path, body);
16961
17205
  if (r)
16962
- after(path, body, 200);
17206
+ after(path, body, 200, r);
16963
17207
  return r;
16964
17208
  },
16965
17209
  async postFull(path, body) {
16966
17210
  const r = await client.postFull(path, body);
16967
17211
  if (r)
16968
- after(path, body, r.status);
17212
+ after(path, body, r.status, r.json);
16969
17213
  return r;
16970
17214
  },
16971
17215
  get: (path) => client.get(path)
@@ -17402,23 +17646,53 @@ var hooksOrg2 = (home, env = process.env) => {
17402
17646
 
17403
17647
  // src/commands/init.ts
17404
17648
  var DEFAULT_PROXY_URL = "https://proxy.sleds.ai";
17405
- var promptStdin = (question, input = process.stdin, output = process.stderr) => new Promise((resolve) => {
17406
- const rl = createInterface({ input, output });
17649
+ var lineReaders = new WeakMap;
17650
+ var lineReader = (input, output) => {
17651
+ const cached = lineReaders.get(input);
17652
+ if (cached)
17653
+ return cached;
17654
+ const rl = createInterface({ input, output, terminal: false });
17407
17655
  const events = rl;
17408
- let answer;
17409
- events.once("line", (line) => {
17410
- answer = line;
17411
- rl.close();
17656
+ const queued = [];
17657
+ const waiting = [];
17658
+ let closed = false;
17659
+ events.on("line", (line) => {
17660
+ const take = waiting.shift();
17661
+ if (take)
17662
+ take(line);
17663
+ else
17664
+ queued.push(line);
17665
+ if (waiting.length === 0 && !closed)
17666
+ rl.pause();
17412
17667
  });
17413
17668
  events.once("close", () => {
17414
- if (answer === undefined)
17415
- output.write(`
17416
- (stdin closed before an answer; taking it as no answer)
17417
- `);
17418
- resolve((answer ?? "").trim());
17669
+ closed = true;
17670
+ for (const take of waiting.splice(0))
17671
+ take(undefined);
17419
17672
  });
17673
+ const reader = {
17674
+ next: () => new Promise((resolve) => {
17675
+ if (queued.length > 0)
17676
+ return resolve(queued.shift());
17677
+ if (closed)
17678
+ return resolve(undefined);
17679
+ waiting.push(resolve);
17680
+ rl.resume();
17681
+ })
17682
+ };
17683
+ lineReaders.set(input, reader);
17684
+ return reader;
17685
+ };
17686
+ var promptStdin = async (question, input = process.stdin, output = process.stderr) => {
17687
+ const reader = lineReader(input, output);
17420
17688
  output.write(question);
17421
- });
17689
+ const answer = await reader.next();
17690
+ if (answer === undefined)
17691
+ output.write(`
17692
+ (stdin closed before an answer; taking it as no answer)
17693
+ `);
17694
+ return (answer ?? "").trim();
17695
+ };
17422
17696
  async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch, code) {
17423
17697
  return signupRequest(proxyUrl, { email, apiKey: credential, ...code ? { code } : {} }, fetchFn);
17424
17698
  }
@@ -18320,6 +18594,7 @@ init_config();
18320
18594
  init_state();
18321
18595
  init_settings();
18322
18596
  var DEFAULT_PROXY_URL2 = "https://proxy.sleds.ai";
18597
+ var lineReaders2 = new WeakMap;
18323
18598
 
18324
18599
  // src/commands/on.ts
18325
18600
  var on = (o) => {
@@ -19012,7 +19287,8 @@ async function decisionCmd(o, argv) {
19012
19287
  return said(o, key, json, r, () => log(seen ? `${key}: marked seen` : overrule ? `${key}: overruled — your answer stands in the log` : answeredLine(key, r.json?.route ?? route, r.json?.held)));
19013
19288
  }
19014
19289
  var DECISIONS_USAGE = "usage: sleds route decisions [<key>] [--flagged] [--json]";
19015
- function decisionsLines(j) {
19290
+ function decisionsLines(raw) {
19291
+ const j = clean(raw);
19016
19292
  const rows = Array.isArray(j.decisions) ? j.decisions : [];
19017
19293
  if (!rows.length)
19018
19294
  return ["no decisions"];
@@ -20686,7 +20962,8 @@ function ago2(at, now) {
20686
20962
  const h = Math.floor(m / 60);
20687
20963
  return h < 24 ? `${h}h ago` : `${Math.floor(h / 24)}d ago`;
20688
20964
  }
20689
- function webhookLines(j, now) {
20965
+ function webhookLines(raw, now) {
20966
+ const j = clean(raw);
20690
20967
  const rows = Array.isArray(j.webhooks) ? j.webhooks : [];
20691
20968
  if (!rows.length)
20692
20969
  return ["no webhooks — add one in the app: Settings › Integrations › Webhooks"];
@@ -20703,7 +20980,8 @@ function webhookLines(j, now) {
20703
20980
  return `${off} ${str10(h.name)} ${host} ${events.includes("*") ? "all events" : events.join(",")} ${lastText}${why} [${str10(h.id)}]`;
20704
20981
  });
20705
20982
  }
20706
- function deliveryLines(j, now) {
20983
+ function deliveryLines(raw, now) {
20984
+ const j = clean(raw);
20707
20985
  const rows = Array.isArray(j.deliveries) ? j.deliveries : [];
20708
20986
  const name = str10(j.webhook?.name);
20709
20987
  if (!rows.length)
@@ -21422,7 +21700,7 @@ ${renderHelpText(argv[1])}`);
21422
21700
  case "version":
21423
21701
  case "--version":
21424
21702
  case "-v":
21425
- console.log("sleds 0.1.32");
21703
+ console.log("sleds 0.1.33");
21426
21704
  return 0;
21427
21705
  default:
21428
21706
  console.error(`unknown command: ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "SLEDS CLI — the cost and limits of AI agent work, decided at the wall. Point Claude Code at a SLEDS proxy; estimate before you run; hold, switch model, or run overnight when you hit a limit.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",