@sleds/cli 0.1.57 → 0.1.58

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/sleds.mjs +410 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,7 +64,7 @@ sleds engine the long-running loop over every route Run
64
64
  sleds engine --jira EPIC-7 the same loop over a Jira epic's Blocks links, no SLEDS route needed
65
65
  ```
66
66
 
67
- For each ready node the engine makes a worktree, starts an agent session on the node's branch (default `route/{item}-{node}`), and follows its PR to the merge: CI failures and review feedback go back to the session; a merge lands the node, closes the session, removes the worktree and, on Jira, moves the issue to Done. Stopping the engine leaves its sessions running — the next start picks them up from its state file. `sleds route run` keeps its own state file, so it can run beside a `sleds engine` on the same box.
67
+ For each ready node the engine makes a worktree, starts an agent session on the node's branch (default `route/{item}-{node}`; an item synced from Jira or Linear carries its key after the SLEDS key — `route/sl-2091-WORK-123-a`, worktree `sleds-sl-2091-WORK-123-a` — and the session starts its commit subjects and PR title with it, so the ticket's Development panel shows the work), and follows its PR to the merge: CI failures and review feedback go back to the session; a merge lands the node, closes the session, removes the worktree and, on Jira, moves the issue to Done. Stopping the engine leaves its sessions running — the next start picks them up from its state file. `sleds route run` keeps its own state file, so it can run beside a `sleds engine` on the same box.
68
68
 
69
69
  Where each node runs is `--runner`:
70
70
 
package/dist/sleds.mjs CHANGED
@@ -7089,11 +7089,21 @@ function planTakes(o) {
7089
7089
  }
7090
7090
  return out;
7091
7091
  }
7092
- function nodeBranch(item, node) {
7093
- return `route/${slugify(`${item}-${node}`, 60) || "node"}`;
7092
+ function trackerTicket(item, ticket) {
7093
+ const t = ticket?.trim();
7094
+ if (!t || !/^[A-Za-z][A-Za-z0-9]*-\d+$/.test(t) || t.toUpperCase() === item.toUpperCase())
7095
+ return;
7096
+ return t.toUpperCase();
7097
+ }
7098
+ function nodeBranch(item, node, ticket) {
7099
+ const t = trackerTicket(item, ticket);
7100
+ if (!t)
7101
+ return `route/${slugify(`${item}-${node}`, 60) || "node"}`;
7102
+ const n = slugify(node, 60);
7103
+ return `route/${[slugify(item, 60), t, n === t.toLowerCase() ? "" : n].filter(Boolean).join("-")}`;
7094
7104
  }
7095
- function sessionName(item, node) {
7096
- return nodeBranch(item, node).replace(/^route\//, "sleds-");
7105
+ function sessionName(item, node, ticket) {
7106
+ return nodeBranch(item, node, ticket).replace(/^route\//, "sleds-");
7097
7107
  }
7098
7108
  function fill(template, values) {
7099
7109
  return template.replace(/\{([A-Za-z_]+)\}/g, (whole, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : whole);
@@ -7123,7 +7133,7 @@ function branchFor(template, claim) {
7123
7133
  slug: slugify(claim.title, 40),
7124
7134
  ticket: one(claim.ticket ?? "")
7125
7135
  }));
7126
- return name || nodeBranch(claim.item, claim.node);
7136
+ return name || nodeBranch(claim.item, claim.node, claim.ticket);
7127
7137
  }
7128
7138
  function prTitleFor(template, claim) {
7129
7139
  return fill(template, {
@@ -7197,11 +7207,16 @@ function nodePrompt(o) {
7197
7207
  const plan = planBlock(o.plan, o.upstream ?? []);
7198
7208
  const visual = visualSurface(claim.pack);
7199
7209
  const deviations = planLines(o.plan).length ? ", and where you departed from the plan and why" : "";
7210
+ const ticket = trackerTicket(claim.item, claim.ticket);
7211
+ const title = o.prTitle ? ` and title it exactly \`${o.prTitle}\` (the repo's CI checks it)` : ticket ? ` and start its title with \`${ticket}\`` : "";
7200
7212
  const lines = [
7201
7213
  `You are node ${claim.node} of ${claim.item}'s route — "${claim.title}" — started by ${o.engine}.`,
7202
7214
  "",
7203
7215
  `- Work on branch \`${o.branch}\` from the latest \`origin/${base}\` (create it if this checkout is not on it). One branch, one PR.`,
7204
- `- Open the PR against \`${base}\` with \`gh pr create${visual ? " --draft" : ""}\`${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.`,
7216
+ ...ticket ? [
7217
+ `- Start every commit subject with \`${ticket}\` — ${claim.item} is synced from its tracker, which shows the branch, commits and PR on the ticket by that key.`
7218
+ ] : [],
7219
+ `- Open the PR against \`${base}\` with \`gh pr create${visual ? " --draft" : ""}\`${title}. Its body carries \`SLEDS-Node: ${claim.node}\` on the line under \`SLEDS-Source:\` when the pack names a source.`,
7205
7220
  ...claim.pack.includes(PR_HEADER_OPEN) ? [
7206
7221
  `- The PR body starts with the pack's PR header block (\`${PR_HEADER_OPEN}\` … \`${PR_HEADER_CLOSE}\`, in its part 5), above everything else. The pack wrote it before your work existed: when you make a mock (a screenshot or an artifact) or write a doc, put its link on that part's line in place of \`None\` — not only lower in the body (SL-1746). When you edit the body later, keep it on top.`
7207
7222
  ] : [],
@@ -7559,7 +7574,8 @@ var init_engine = __esm(() => {
7559
7574
  };
7560
7575
  VERDICT_SHAPE = [
7561
7576
  "- **Verdict first.** The first line of `md` (and of `why`) is the verdict on its own, in plain words: what to do, or what is so. No file paths, line numbers or links in it.",
7562
- "- Then at most 3 short points, one line each, as a `-` list. Cites go in `cites`, not in the prose."
7577
+ "- Then at most 3 short points, one line each, as a `-` list. Cites go in `cites`, not in the prose.",
7578
+ "- **One cite per span, no prose in it.** Code is `code:<path>:<line>`, the path from the repo root (`code:apps/web/src/App.tsx:42`); cite each line on its own, the cards merge them by file."
7563
7579
  ];
7564
7580
  DECIDER_RULES = [
7565
7581
  "- **Text after the input prompt was never sent.** On a session's screen, whatever sits after its last input prompt (`>`, `❯`, `›`) is a draft in the input box: nobody sent it. It is not a person's words, an instruction or a decision — never read it, or cite it, as one.",
@@ -7567,6 +7583,9 @@ var init_engine = __esm(() => {
7567
7583
  "- **No third repeat.** When the decision log shows this node already asked this question (or one the same in substance) and it was answered twice, a third answer of the same kind will not move it. Escalate, saying it keeps coming back, or release it: tell the session to stop waiting and finish with what it has (open its PR, or land and say what is owed)."
7568
7584
  ];
7569
7585
  });
7586
+
7587
+ // ../../packages/shared/src/route/sources.ts
7588
+ var init_sources = () => {};
7570
7589
  // ../../packages/shared/src/cowork/otlp.ts
7571
7590
  var OTLP_KEEP;
7572
7591
  var init_otlp = __esm(() => {
@@ -8225,6 +8244,7 @@ var init_src = __esm(() => {
8225
8244
  init_doneChecks();
8226
8245
  init_routeFix();
8227
8246
  init_engine();
8247
+ init_sources();
8228
8248
  init_otlp();
8229
8249
  init_session();
8230
8250
  init_effort();
@@ -8627,6 +8647,7 @@ function parseRun(v) {
8627
8647
  ...typeof v.mcpConfig === "string" ? { mcpConfig: v.mcpConfig } : {},
8628
8648
  ...v.onRunToken === true ? { onRunToken: true } : {},
8629
8649
  ...isObject8(v.group) && typeof v.group.key === "string" && typeof v.group.title === "string" ? { group: { key: v.group.key, title: v.group.title } } : {},
8650
+ ...typeof v.ticket === "string" ? { ticket: v.ticket } : {},
8630
8651
  branch,
8631
8652
  repoPath,
8632
8653
  handle,
@@ -8676,7 +8697,14 @@ function parseRun(v) {
8676
8697
  ...typeof v.pr.resolving === "string" ? { resolving: v.pr.resolving } : {},
8677
8698
  ...conflicted ? { conflicted } : {},
8678
8699
  ...typeof v.pr.reviewed === "string" ? { reviewed: v.pr.reviewed } : {},
8679
- ...typeof v.pr.look === "string" ? { look: v.pr.look } : {}
8700
+ ...typeof v.pr.look === "string" ? { look: v.pr.look } : {},
8701
+ ...isObject8(v.pr.instructed) && Array.isArray(v.pr.instructed.ids) && (v.pr.instructed.how === "told" || v.pr.instructed.how === "restarted") ? {
8702
+ instructed: {
8703
+ ids: v.pr.instructed.ids.filter((x) => typeof x === "string"),
8704
+ how: v.pr.instructed.how
8705
+ }
8706
+ } : {},
8707
+ ...typeof v.pr.findings === "string" ? { findings: v.pr.findings } : {}
8680
8708
  }
8681
8709
  } : {}
8682
8710
  };
@@ -8971,6 +8999,7 @@ init_route();
8971
8999
  init_doneChecks();
8972
9000
  init_routeFix();
8973
9001
  init_engine();
9002
+ init_sources();
8974
9003
  init_otlp();
8975
9004
  init_session();
8976
9005
  init_effort();
@@ -11422,6 +11451,26 @@ function orcaRunner(exec, deps = {}) {
11422
11451
  return env;
11423
11452
  };
11424
11453
  const retries = new Map;
11454
+ const terminalIn = (cwd, o) => {
11455
+ const launcher = launcherPath2(o.promptFile);
11456
+ writeFile(launcher, orcaLauncher(o));
11457
+ const shown = orca([
11458
+ "terminal",
11459
+ "create",
11460
+ "--worktree",
11461
+ `path:${cwd}`,
11462
+ "--title",
11463
+ o.name,
11464
+ "--command",
11465
+ orcaCommand(launcher),
11466
+ "--json"
11467
+ ], `orca terminal create ${o.name} (worktree ${cwd})`);
11468
+ const terminal = parseOrcaTerminal(shown);
11469
+ if (!terminal) {
11470
+ throw new Error(`orca terminal create ${o.name} started in ${cwd} but returned no terminal handle`);
11471
+ }
11472
+ return terminal;
11473
+ };
11425
11474
  return {
11426
11475
  name: "orca",
11427
11476
  makesWorktree: true,
@@ -11443,27 +11492,14 @@ function orcaRunner(exec, deps = {}) {
11443
11492
  if (!cwd) {
11444
11493
  throw new Error(`orca worktree create ${o.name} returned no worktree path; the agent was not started`);
11445
11494
  }
11446
- const launcher = launcherPath2(o.promptFile);
11447
- writeFile(launcher, orcaLauncher(o));
11448
- const shown = orca([
11449
- "terminal",
11450
- "create",
11451
- "--worktree",
11452
- `path:${cwd}`,
11453
- "--title",
11454
- o.name,
11455
- "--command",
11456
- orcaCommand(launcher),
11457
- "--json"
11458
- ], `orca terminal create ${o.name} (worktree ${cwd})`);
11459
- const terminal = parseOrcaTerminal(shown);
11460
- if (!terminal) {
11461
- throw new Error(`orca terminal create ${o.name} started in ${cwd} but returned no terminal handle`);
11462
- }
11495
+ const terminal = terminalIn(cwd, o);
11463
11496
  const own = parseOrcaBranch(created);
11464
11497
  const h = { runner: "orca", id: terminal, cwd, ...own ? { own } : {} };
11465
11498
  return h;
11466
11499
  },
11500
+ reopen(h, o) {
11501
+ return { ...h, id: terminalIn(h.cwd, o) };
11502
+ },
11467
11503
  status(h) {
11468
11504
  const r = exec("orca", ["terminal", "show", "--terminal", h.id, "--json"]);
11469
11505
  return orcaTerminalLive(r.status, r.stdout) ? "running" : "exited";
@@ -11578,7 +11614,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
11578
11614
  name: flags.name,
11579
11615
  os: host.os ?? process.platform,
11580
11616
  runner: flags.runner,
11581
- version: host.version ?? "0.1.57" ?? "dev",
11617
+ version: host.version ?? "0.1.58" ?? "dev",
11582
11618
  running: live.length,
11583
11619
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
11584
11620
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -11870,7 +11906,7 @@ function noteEvent(line) {
11870
11906
 
11871
11907
  // src/engine/gh.ts
11872
11908
  init_src();
11873
- var isGhError = (r) => r !== null && ("error" in r);
11909
+ var isGhError = (r) => r !== null && typeof r === "object" && ("error" in r);
11874
11910
  function prForBranch(exec, repoPath, branch) {
11875
11911
  try {
11876
11912
  const r = exec("gh", [
@@ -11912,6 +11948,74 @@ function prForBranch(exec, repoPath, branch) {
11912
11948
  return { error: e instanceof Error ? e.message : String(e) };
11913
11949
  }
11914
11950
  }
11951
+ function carriesKey(text, key) {
11952
+ const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11953
+ return new RegExp(`(^|[^A-Za-z0-9])${k}(?![0-9])`, "i").test(text);
11954
+ }
11955
+ var isTrackerKey = (key) => /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(key);
11956
+ function otherWork(exec, repoPath, key, own) {
11957
+ const out = [];
11958
+ try {
11959
+ const r = exec("gh", [
11960
+ "pr",
11961
+ "list",
11962
+ "--state",
11963
+ "open",
11964
+ "--json",
11965
+ "url,title,headRefName,isCrossRepository,author",
11966
+ "--limit",
11967
+ "100"
11968
+ ], { cwd: repoPath });
11969
+ if (r.status !== 0)
11970
+ return { error: r.stderr.trim() || `gh pr list exited with status ${r.status}` };
11971
+ let rows;
11972
+ try {
11973
+ rows = JSON.parse(r.stdout);
11974
+ } catch {
11975
+ return { error: "gh pr list did not print JSON" };
11976
+ }
11977
+ if (!Array.isArray(rows))
11978
+ return { error: "gh pr list did not print a list" };
11979
+ for (const row of rows) {
11980
+ if (typeof row !== "object" || row === null)
11981
+ continue;
11982
+ const { url, title, headRefName, isCrossRepository, author } = row;
11983
+ if (typeof url !== "string" || !url)
11984
+ continue;
11985
+ const branch = typeof headRefName === "string" ? headRefName : "";
11986
+ const named = typeof title === "string" ? title : "";
11987
+ if (!carriesKey(branch, key) && !carriesKey(named, key))
11988
+ continue;
11989
+ if (isCrossRepository !== true && own(branch))
11990
+ continue;
11991
+ const login = typeof author === "object" && author !== null ? author.login : undefined;
11992
+ out.push({
11993
+ branch,
11994
+ pr: { url, title: named, ...typeof login === "string" && login ? { author: login } : {} }
11995
+ });
11996
+ }
11997
+ } catch (e) {
11998
+ return { error: e instanceof Error ? e.message : String(e) };
11999
+ }
12000
+ try {
12001
+ const r = exec("git", ["ls-remote", "--heads", "origin"], { cwd: repoPath });
12002
+ if (r.status === 0) {
12003
+ const listed = new Set(out.map((w) => w.branch));
12004
+ for (const line of r.stdout.split(`
12005
+ `)) {
12006
+ const branch = line.split("\t")[1]?.trim().replace(/^refs\/heads\//, "") ?? "";
12007
+ if (!branch || listed.has(branch) || own(branch) || !carriesKey(branch, key))
12008
+ continue;
12009
+ listed.add(branch);
12010
+ out.push({ branch });
12011
+ }
12012
+ }
12013
+ } catch {}
12014
+ return out;
12015
+ }
12016
+ function otherWorkLine(work) {
12017
+ return work.map((w) => w.pr ? `${w.pr.url}${w.pr.author ? ` by ${w.pr.author}` : ""} (${w.branch})` : `branch ${w.branch} (no PR yet)`).join("; ");
12018
+ }
11915
12019
  var URL_RE = /https:\/\/[^\s)>\]"']+/;
11916
12020
  var ARTIFACT_RE = /https:\/\/claude\.ai\/[^\s)>\]"']*artifact\/[^\s)>\]"']+/;
11917
12021
  function mockOf(body) {
@@ -11993,6 +12097,8 @@ function countFeedback(stdout, author) {
11993
12097
  const n = { all: 0, author: 0 };
11994
12098
  for (const line of stdout.split(`
11995
12099
  `)) {
12100
+ if (line.startsWith(BOT_LINE))
12101
+ continue;
11996
12102
  const [login = "", marked, hasBody] = line.trim().split("\t");
11997
12103
  if (!login || marked === "true")
11998
12104
  continue;
@@ -12004,12 +12110,14 @@ function countFeedback(stdout, author) {
12004
12110
  }
12005
12111
  return n;
12006
12112
  }
12007
- var FEEDBACK_JQ = `.[] | select(.user.type != "Bot") | [.user.login, ((.body // "") | contains("${ENGINE_MARK}") | tostring), ((.body // "") | length > 0 | tostring)] | @tsv`;
12113
+ var BOT_LINE = "bot\t";
12114
+ var FEEDBACK_JQ = `.[] | if .user.type == "Bot" then (select((.body // "") | length > 0) | "${BOT_LINE}" + ({by: .user.login, url: .html_url, md: .body, at: (.submitted_at // .created_at // "")} | tojson)) else ([.user.login, ((.body // "") | contains("${ENGINE_MARK}") | tostring), ((.body // "") | length > 0 | tostring)] | @tsv) end`;
12008
12115
  function prFeedback(exec, repoPath, url, author) {
12009
12116
  const ref = prRef(url);
12010
12117
  if (!ref)
12011
12118
  return null;
12012
12119
  const total = { all: 0, author: 0 };
12120
+ const bots = [];
12013
12121
  for (const path of [
12014
12122
  `repos/${ref.repo}/issues/${ref.number}/comments`,
12015
12123
  `repos/${ref.repo}/pulls/${ref.number}/reviews`,
@@ -12022,11 +12130,38 @@ function prFeedback(exec, repoPath, url, author) {
12022
12130
  const n = countFeedback(r.stdout, author);
12023
12131
  total.all += n.all;
12024
12132
  total.author += n.author;
12133
+ bots.push(...parseBotFindings(r.stdout));
12025
12134
  } catch {
12026
12135
  return null;
12027
12136
  }
12028
12137
  }
12029
- return total;
12138
+ const findings = bots.sort((a, b) => a.at.localeCompare(b.at)).slice(-FINDINGS_MAX).map(({ by, url, md }) => ({ by, url, md }));
12139
+ return { ...total, findings };
12140
+ }
12141
+ var FINDINGS_MAX = 20;
12142
+ var FINDING_MD_MAX = 2000;
12143
+ function parseBotFindings(stdout) {
12144
+ const out = [];
12145
+ for (const line of stdout.split(`
12146
+ `)) {
12147
+ if (!line.startsWith(BOT_LINE))
12148
+ continue;
12149
+ try {
12150
+ const o = JSON.parse(line.slice(BOT_LINE.length));
12151
+ if (typeof o.by !== "string" || typeof o.url !== "string" || typeof o.md !== "string")
12152
+ continue;
12153
+ const md = o.md.trim();
12154
+ if (!md || md.includes(ENGINE_MARK))
12155
+ continue;
12156
+ out.push({
12157
+ by: o.by,
12158
+ url: o.url,
12159
+ md: md.slice(0, FINDING_MD_MAX),
12160
+ at: typeof o.at === "string" ? o.at : ""
12161
+ });
12162
+ } catch {}
12163
+ }
12164
+ return out;
12030
12165
  }
12031
12166
  var MERGE_QUEUE_QUERY = "query($url: URI!) { resource(url: $url) { ... on PullRequest { mergeQueueEntry { state } } } }";
12032
12167
  function prInMergeQueue(exec, repoPath, url) {
@@ -12064,11 +12199,19 @@ function prView(exec, repoPath, url, last) {
12064
12199
  const ready = !parsed.queued && parsed.failing.length === 0 && parsed.pending === 0 && parsed.reviewDecision === "APPROVED";
12065
12200
  const view = ready && prInMergeQueue(exec, repoPath, url) === true ? { ...parsed, queued: true } : parsed;
12066
12201
  let read = view.updatedAt && last?.updatedAt === view.updatedAt ? last : undefined;
12202
+ let findings = null;
12067
12203
  if (!read) {
12068
12204
  const f = prFeedback(exec, repoPath, url, view.author);
12069
- read = f && { ...f, updatedAt: view.updatedAt };
12070
- }
12071
- return read ? { ...view, feedback: read.all, authorFeedback: read.author, read } : view;
12205
+ read = f && { all: f.all, author: f.author, updatedAt: view.updatedAt };
12206
+ findings = f?.findings ?? null;
12207
+ }
12208
+ return read ? {
12209
+ ...view,
12210
+ feedback: read.all,
12211
+ authorFeedback: read.author,
12212
+ read,
12213
+ ...findings ? { findings } : {}
12214
+ } : view;
12072
12215
  } catch {
12073
12216
  return null;
12074
12217
  }
@@ -12616,6 +12759,32 @@ function resumesOf(s) {
12616
12759
  const r = s;
12617
12760
  return r && typeof r.resumeParked === "function" ? s : null;
12618
12761
  }
12762
+ function instructionsOf(s) {
12763
+ const i = s;
12764
+ return i && typeof i.instructions === "function" ? s : null;
12765
+ }
12766
+ function instructionsFrom(waiting) {
12767
+ if (!Array.isArray(waiting))
12768
+ return [];
12769
+ return waiting.filter(isObject8).flatMap((w) => {
12770
+ const id = str6(w.id);
12771
+ const md = str6(w.md);
12772
+ if (!id || !md)
12773
+ return [];
12774
+ const f = isObject8(w.finding) ? w.finding : null;
12775
+ const kind = f?.kind === "review" || f?.kind === "bot" ? f.kind : null;
12776
+ const url = f ? str6(f.url) : undefined;
12777
+ const by = f ? str6(f.by) : undefined;
12778
+ return [
12779
+ {
12780
+ id,
12781
+ md,
12782
+ by: str6(w.by) ?? "a person",
12783
+ ...kind ? { finding: { kind, ...url ? { url } : {}, ...by ? { by } : {} } } : {}
12784
+ }
12785
+ ];
12786
+ });
12787
+ }
12619
12788
  var FAIL_LOG_MAX = 2000;
12620
12789
  var UNREACHABLE3 = "SLEDS unreachable";
12621
12790
  function keysIn(states, state) {
@@ -12861,6 +13030,10 @@ function nextPickup(r, o) {
12861
13030
  continue;
12862
13031
  if (isPrMirror(row.source))
12863
13032
  continue;
13033
+ if (route.packable === false)
13034
+ continue;
13035
+ if (isObject8(route.elsewhere))
13036
+ continue;
12864
13037
  const states = isObject8(route.states) ? route.states : {};
12865
13038
  if (statesLanded(states) || keysIn(states, "running").length > 0)
12866
13039
  continue;
@@ -12890,6 +13063,8 @@ function armedDrafts(r, engine) {
12890
13063
  return false;
12891
13064
  if (route.drafting === true || route.proposed === true || route.draftParked)
12892
13065
  return false;
13066
+ if (isObject8(route.elsewhere))
13067
+ return false;
12893
13068
  if (strings(route.needsRoute).length === 0 || isPrMirror(row.source))
12894
13069
  return false;
12895
13070
  const states = isObject8(route.states) ? route.states : {};
@@ -13094,7 +13269,8 @@ function sledsSource(client, o) {
13094
13269
  ...str6(hit.answer) ? { answer: str6(hit.answer) } : {},
13095
13270
  ...runToken ? { runToken } : {},
13096
13271
  ...group ? { group } : {},
13097
- ...run ? { run } : {}
13272
+ ...run ? { run } : {},
13273
+ ...str6(hit.ticket) ? { ticket: str6(hit.ticket) } : {}
13098
13274
  };
13099
13275
  return { ok: true, claim };
13100
13276
  },
@@ -13117,7 +13293,7 @@ function sledsSource(client, o) {
13117
13293
  return "running";
13118
13294
  if (claim.kind === "draft") {
13119
13295
  const filed = Array.isArray(route.nodes) && route.nodes.length > 0;
13120
- if (filed || isObject8(route.proposed))
13296
+ if (filed || isObject8(route.proposed) || isObject8(route.elsewhere))
13121
13297
  return "done";
13122
13298
  if (isObject8(route.draftParked))
13123
13299
  return "parked";
@@ -13317,6 +13493,23 @@ function sledsSource(client, o) {
13317
13493
  return false;
13318
13494
  }
13319
13495
  },
13496
+ async instructions(item, node, o) {
13497
+ try {
13498
+ const r = await client.postFull("/proxy/prread", {
13499
+ project: item,
13500
+ node,
13501
+ instructions: {
13502
+ ...o.findings ? { findings: o.findings } : {},
13503
+ ...o.delivered ? { delivered: o.delivered } : {}
13504
+ }
13505
+ });
13506
+ if (!r || r.status < 200 || r.status >= 300 || !r.json)
13507
+ return null;
13508
+ return r.json.ok === false ? [] : instructionsFrom(r.json.waiting);
13509
+ } catch {
13510
+ return null;
13511
+ }
13512
+ },
13320
13513
  async answerDecision(a, engine, answer) {
13321
13514
  const r = await client.postFull("/proxy/run", { runId: a.runId, engine, answer });
13322
13515
  if (!r)
@@ -14284,8 +14477,8 @@ async function tickOnce(d) {
14284
14477
  await report({ kind: "release", why: repoPath.error });
14285
14478
  continue;
14286
14479
  }
14287
- const branch = d.branchOf ? d.branchOf(claim) : nodeBranch(claim.item, claim.node);
14288
- const name = sessionName(claim.item, claim.node);
14480
+ const branch = d.branchOf ? d.branchOf(claim) : nodeBranch(claim.item, claim.node, claim.ticket);
14481
+ const name = sessionName(claim.item, claim.node, claim.ticket);
14289
14482
  const found = d.prForBranch(d.exec, repoPath, branch);
14290
14483
  if (isGhError(found))
14291
14484
  d.log(`${at}: could not ask GitHub for ${branch}'s PR: ${found.error}`);
@@ -14295,6 +14488,12 @@ async function tickOnce(d) {
14295
14488
  await report({ kind: existing.state === "MERGED" ? "merged" : "pr", ref: existing.url });
14296
14489
  continue;
14297
14490
  }
14491
+ const duplicate = duplicateQuestion(d, claim, repoPath, "run");
14492
+ if (duplicate) {
14493
+ d.log(`${at}: parked — ${duplicate}`);
14494
+ await report({ kind: "park", question: duplicate });
14495
+ continue;
14496
+ }
14298
14497
  let start;
14299
14498
  let mcpConfig;
14300
14499
  try {
@@ -14380,6 +14579,7 @@ async function tickOnce(d) {
14380
14579
  ...mcpConfig ? { mcpConfig, ...claim.runToken ? { onRunToken: true } : {} } : {},
14381
14580
  ...claim.group ? { group: claim.group } : {},
14382
14581
  ...packBandOf(claim.pack) ? { band: packBandOf(claim.pack) } : {},
14582
+ ...claim.ticket ? { ticket: claim.ticket } : {},
14383
14583
  branch,
14384
14584
  repoPath,
14385
14585
  handle,
@@ -14505,7 +14705,7 @@ async function settlePermission(d, runner, run, shown) {
14505
14705
  if (!perms || !run.runId)
14506
14706
  return false;
14507
14707
  const at = ref(run);
14508
- const markFile = d.permissionMark?.(sessionName(run.item, run.node));
14708
+ const markFile = d.permissionMark?.(sessionName(run.item, run.node, run.ticket));
14509
14709
  const dialog = runner.readsSession ? permissionDialog(shown) : null;
14510
14710
  const p = run.permission;
14511
14711
  if (p) {
@@ -15354,6 +15554,25 @@ var safeWatch = (runner, h) => {
15354
15554
  return h.id;
15355
15555
  }
15356
15556
  };
15557
+ function duplicateQuestion(d, claim, repoPath, verb) {
15558
+ if (!d.otherWork || claim.answer)
15559
+ return null;
15560
+ const key = claim.ticket ?? claim.item;
15561
+ if (!isTrackerKey(key))
15562
+ return null;
15563
+ const prefix = nodeBranch(claim.item, "x").slice(0, -1);
15564
+ const template = d.branchOf?.(claim);
15565
+ const own = (b) => b.startsWith(prefix) || b === template;
15566
+ const found = d.otherWork(d.exec, repoPath, key, own);
15567
+ if (isGhError(found)) {
15568
+ d.log(`${ref(claim)}: could not look for other work on ${key}: ${found.error}`);
15569
+ return null;
15570
+ }
15571
+ if (!found.length)
15572
+ return null;
15573
+ const what = verb === "plan" ? "plan its route" : `run ${claim.node}`;
15574
+ return `${key} already has work outside this engine: ${otherWorkLine(found)}. Answer to ${what} anyway; if that work is the one to keep, skip the node or close the item instead.`;
15575
+ }
15357
15576
  async function startDraft(d, claim, why, state, started, finished) {
15358
15577
  const at = ref(claim);
15359
15578
  const report = async (outcome) => {
@@ -15370,6 +15589,12 @@ async function startDraft(d, claim, why, state, started, finished) {
15370
15589
  await report({ kind: "park", question: repoPath.error });
15371
15590
  return;
15372
15591
  }
15592
+ const duplicate = duplicateQuestion(d, claim, repoPath, "plan");
15593
+ if (duplicate) {
15594
+ d.log(`${at}: parked — ${duplicate}`);
15595
+ await report({ kind: "park", question: duplicate });
15596
+ return;
15597
+ }
15373
15598
  const name = sessionName(claim.item, DRAFT_NODE);
15374
15599
  const branch = nodeBranch(claim.item, DRAFT_NODE);
15375
15600
  let handle;
@@ -15535,6 +15760,30 @@ function ciNudge(url, failing) {
15535
15760
  var SELF_NUDGES = 2;
15536
15761
  var FEEDBACK_REREAD_MS = 10 * 60000;
15537
15762
  var CONFLICT_RECHECK_MS = 10 * 60000;
15763
+ var INSTRUCTION_READ_MS = 60000;
15764
+ function instructionText(i) {
15765
+ const from = i.finding ? ` about ${i.finding.kind === "review" ? "SLEDS's review" : `${i.finding.by ?? "a bot reviewer"}'s finding`}${i.finding.url ? ` (${i.finding.url})` : ""}` : "";
15766
+ return `From ${i.by}${from}:
15767
+
15768
+ ${i.md.trim()}`;
15769
+ }
15770
+ function instructionNudge(url, items) {
15771
+ const replies = items.some((i) => i.finding?.url) ? " Where one came from a finding on the PR, reply on that finding's thread with the fix or why not, and leave the thread for the reviewer to resolve." : "";
15772
+ return `Instructions for your PR (${url}), written on its item in SLEDS:
15773
+
15774
+ ${items.map(instructionText).join(`
15775
+
15776
+ ---
15777
+
15778
+ `)}
15779
+
15780
+ Do them on your own branch, run the repo's gate, and push to the same PR.${replies} End every comment and reply you post on the PR with the line ${ENGINE_MARK}.`;
15781
+ }
15782
+ function instructionPrompt(run, url, items) {
15783
+ return `You are picking up node ${run.node} of ${run.item}. Its PR is open: ${url}, on the branch ${run.branch}, checked out in this worktree. The session that opened it has ended. Read the PR first (gh pr view ${url} --comments, and the inline threads), then follow the instructions below. Never force-push, never merge, never open a second PR.
15784
+
15785
+ ${instructionNudge(url, items)}`;
15786
+ }
15538
15787
  function reviewNudge(url, count) {
15539
15788
  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.`;
15540
15789
  }
@@ -15652,6 +15901,7 @@ async function follow(d, state, run, claim, runner, finished, attention, changes
15652
15901
  pr.selfNudges = selfNudges || undefined;
15653
15902
  }
15654
15903
  }
15904
+ await carryInstructions(d, run, runner, view, attention);
15655
15905
  pr.failing = view.failing;
15656
15906
  pr.feedback = Math.max(pr.feedback, view.feedback);
15657
15907
  pr.authorFeedback = Math.max(pr.authorFeedback ?? 0, view.authorFeedback) || undefined;
@@ -16173,22 +16423,37 @@ async function answerBaseConflict(d, events, state, dec, attention, decided) {
16173
16423
  return false;
16174
16424
  }
16175
16425
  function startResolver(d, run, e, answer) {
16426
+ const name = sessionName(run.item, `${run.node}-resolve`);
16427
+ const started = restartInWorktree(d, run, name, eventAnswerNudge(e, answer), e.base);
16428
+ if (started !== true) {
16429
+ if (started)
16430
+ d.log(`${ref(run)}: could not start a resolver session: ${started.error}`);
16431
+ return false;
16432
+ }
16176
16433
  const runner = runnerFor(d, run.handle);
16177
- if (!runner || runner.makesWorktree || run.handle.cwd === run.repoPath)
16434
+ if (run.pr)
16435
+ run.pr.resolving = e.baseSha;
16436
+ d.log(`${ref(run)}: a resolver session has the conflict brief — ${safeWatch(runner, run.handle)}`);
16437
+ return true;
16438
+ }
16439
+ function restartInWorktree(d, run, name, prompt, base) {
16440
+ const runner = runnerFor(d, run.handle);
16441
+ if (!runner || run.handle.cwd === run.repoPath)
16442
+ return false;
16443
+ if (runner.makesWorktree && !runner.reopen)
16178
16444
  return false;
16179
- const name = sessionName(run.item, `${run.node}-resolve`);
16180
16445
  try {
16181
16446
  runner.stop(run.handle);
16182
16447
  } catch {}
16183
16448
  try {
16184
16449
  const launch = nodeLaunchFor(d, ref(run));
16185
- run.handle = runner.start({
16450
+ const o = {
16186
16451
  name,
16187
16452
  repoPath: run.repoPath,
16188
16453
  worktree: run.handle.cwd,
16189
16454
  branch: run.branch,
16190
- base: e.base,
16191
- promptFile: d.writePrompt(name, eventAnswerNudge(e, answer)),
16455
+ ...base ? { base } : {},
16456
+ promptFile: d.writePrompt(name, prompt),
16192
16457
  permissionMode: d.permissionMode,
16193
16458
  ...launchOptions(launch),
16194
16459
  ...run.mcpConfig ? { mcpConfig: run.mcpConfig, ...d.strictMcp ? { strictMcp: true } : {} } : {},
@@ -16196,16 +16461,75 @@ function startResolver(d, run, e, answer) {
16196
16461
  ...hooksFor(d, name),
16197
16462
  ...mainCheckoutFor(d, run.handle.cwd),
16198
16463
  ...run.group ? { group: run.group } : {}
16199
- });
16464
+ };
16465
+ run.handle = runner.makesWorktree ? runner.reopen(run.handle, o) : runner.start(o);
16200
16466
  } catch (err) {
16201
- d.log(`${ref(run)}: could not start a resolver session: ${message(err)}`);
16202
- return false;
16467
+ return { error: message(err) };
16203
16468
  }
16204
- if (run.pr)
16205
- run.pr.resolving = e.baseSha;
16206
- d.log(`${ref(run)}: a resolver session has the conflict brief — ${safeWatch(runner, run.handle)}`);
16207
16469
  return true;
16208
16470
  }
16471
+ async function carryInstructions(d, run, runner, view, attention) {
16472
+ const source = instructionsOf(d.source);
16473
+ const pr = run.pr;
16474
+ if (!source)
16475
+ return;
16476
+ const findings = view.findings;
16477
+ const findingsKey = findings?.map((f) => f.url).join(" ");
16478
+ const newFindings = findings !== undefined && findingsKey !== pr.findings;
16479
+ const due = !pr.instructAt || d.now() - pr.instructAt >= INSTRUCTION_READ_MS;
16480
+ if (!due && !newFindings && !pr.instructed)
16481
+ return;
16482
+ const waiting = await source.instructions(run.item, run.node, {
16483
+ ...newFindings ? { findings } : {},
16484
+ ...pr.instructed ? { delivered: pr.instructed } : {}
16485
+ });
16486
+ if (!waiting)
16487
+ return;
16488
+ pr.instructAt = d.now();
16489
+ if (newFindings)
16490
+ pr.findings = findingsKey;
16491
+ const told = pr.instructed?.ids ?? [];
16492
+ pr.instructed = undefined;
16493
+ const fresh = waiting.filter((i) => !told.includes(i.id));
16494
+ if (fresh.length === 0)
16495
+ return;
16496
+ const at = { item: run.item, node: run.node, url: pr.url };
16497
+ const what = `${fresh.length} instruction${fresh.length === 1 ? "" : "s"} from the item`;
16498
+ let status;
16499
+ try {
16500
+ status = runner.status(run.handle);
16501
+ if (status === "running") {
16502
+ const note = runner.send(run.handle, instructionNudge(pr.url, fresh));
16503
+ d.log(`${ref(run)}: ${what} — told the session${note ? ` (${note})` : ""}`);
16504
+ pr.instructed = { ids: fresh.map((i) => i.id), how: "told" };
16505
+ }
16506
+ } catch {}
16507
+ if (!pr.instructed && status === "exited") {
16508
+ const name = sessionName(run.item, `${run.node}-instruct`);
16509
+ const started = restartInWorktree(d, run, name, instructionPrompt(run, pr.url, fresh));
16510
+ if (started === true) {
16511
+ d.log(`${ref(run)}: ${what} — its session had ended; a new one has them on ${run.branch}: ${safeWatch(runnerFor(d, run.handle) ?? d.runner, run.handle)}`);
16512
+ pr.instructed = { ids: fresh.map((i) => i.id), how: "restarted" };
16513
+ pr.selfNudges = undefined;
16514
+ } else if (started)
16515
+ d.log(`${ref(run)}: could not restart the session: ${started.error}`);
16516
+ }
16517
+ if (pr.instructed) {
16518
+ const after = await source.instructions(run.item, run.node, { delivered: pr.instructed });
16519
+ if (after)
16520
+ pr.instructed = undefined;
16521
+ return;
16522
+ }
16523
+ const said = `${ref(run)}:instruct:${fresh.map((i) => i.id).join(",")}`;
16524
+ if (d.refused?.get(said))
16525
+ return;
16526
+ d.refused?.set(said, "said");
16527
+ d.log(`${ref(run)}: ${what} could not reach a session (${status === "running" ? "the session takes no input" : "the runner cannot restart it"}) — needs a person: ${pr.url}`);
16528
+ attention.push({
16529
+ ...at,
16530
+ what: `${what} waiting — no session can take them; open the worktree ${run.handle.cwd}`
16531
+ });
16532
+ }
16209
16533
  async function startEventAnswerer(d, answers, state, dec, decided) {
16210
16534
  const at = ref(dec);
16211
16535
  const kind = dec.event.kind;
@@ -17355,7 +17679,7 @@ import {
17355
17679
  } from "node:fs";
17356
17680
 
17357
17681
  // ../../packages/shared/src/updates/releaseKey.ts
17358
- var RELEASE_PUBLIC_KEY = "";
17682
+ var RELEASE_PUBLIC_KEY = "MCowBQYDK2VwAyEADIZiQghghQlN6WoLDF9xLmqMeFSQL4crcCBT4wINNCc=";
17359
17683
 
17360
17684
  // ../../packages/shared/src/updates/verify.ts
17361
17685
  import { createPrivateKey, createPublicKey as createPublicKey2, sign, verify } from "node:crypto";
@@ -17586,7 +17910,7 @@ async function fetchVersion(o) {
17586
17910
  const platform = o.platform ?? process.platform;
17587
17911
  const arch = o.arch ?? process.arch;
17588
17912
  const publicKey = o.publicKey ?? RELEASE_PUBLIC_KEY;
17589
- const running = o.running ?? "0.1.57";
17913
+ const running = o.running ?? "0.1.58";
17590
17914
  if (!publicKey)
17591
17915
  return { ok: false, why: "no release key yet" };
17592
17916
  if (!VERSION_RE2.test(o.version))
@@ -18865,7 +19189,7 @@ function templateProblem(flag, template, jira = false) {
18865
19189
  return `--${flag} has ${unknown.map((f) => `{${f}}`).join(", ")}, which the engine does not fill — it knows ${known.map((f) => `{${f}}`).join(" ")}`;
18866
19190
  const perNode = jira ? ["node", "key", "ticket", "slug"] : ["node", "slug"];
18867
19191
  if (flag === "branch" && !used.some((f) => perNode.includes(f)))
18868
- return jira ? "--branch needs {node}, {key}, {ticket} or {slug}: without one every node of an item gets the same branch, and the second would find the first one's PR and stop" : "--branch needs {node} or {slug} on a SLEDS route: its nodes share the item's {key} and have no {ticket}, so without one every node gets the same branch and the second would find the first one's PR and stop";
19192
+ return jira ? "--branch needs {node}, {key}, {ticket} or {slug}: without one every node of an item gets the same branch, and the second would find the first one's PR and stop" : "--branch needs {node} or {slug} on a SLEDS route: its nodes share the item's {key} and {ticket}, so without one every node gets the same branch and the second would find the first one's PR and stop";
18869
19193
  return null;
18870
19194
  }
18871
19195
  function defaultEngineName(host) {
@@ -19171,7 +19495,7 @@ function engineClient(o, org, breaker) {
19171
19495
  ...src,
19172
19496
  ...breaker ? { fetchFn: breaker.fetch } : {},
19173
19497
  timeoutMs: ENGINE_TIMEOUT_MS,
19174
- version: "0.1.57"
19498
+ version: "0.1.58"
19175
19499
  });
19176
19500
  const asked = org?.trim() || env.SLEDS_ORG?.trim();
19177
19501
  if (asked && !selectOrg(readConfig(o.home), org, env).seat)
@@ -19348,6 +19672,7 @@ function engineDeps(o, flags, source, runner, stateName) {
19348
19672
  defaultBranch,
19349
19673
  runnerOf: (name) => name === runner.name ? runner : restoreRunner(name, exec),
19350
19674
  prForBranch,
19675
+ otherWork,
19351
19676
  prView,
19352
19677
  mergeTree,
19353
19678
  prFiles,
@@ -19462,7 +19787,7 @@ async function driveEngine(o, flags, deps, until = {}) {
19462
19787
  runner: deps.runner.name,
19463
19788
  atOnce: flags.atOnce,
19464
19789
  interval: flags.interval,
19465
- cliVersion: "0.1.57"
19790
+ cliVersion: "0.1.58"
19466
19791
  };
19467
19792
  const beat = (checkedAt) => {
19468
19793
  const m = deps.machine;
@@ -20971,7 +21296,7 @@ function templateProblem2(flag, template, jira = false) {
20971
21296
  return `--${flag} has ${unknown.map((f) => `{${f}}`).join(", ")}, which the engine does not fill — it knows ${known.map((f) => `{${f}}`).join(" ")}`;
20972
21297
  const perNode = jira ? ["node", "key", "ticket", "slug"] : ["node", "slug"];
20973
21298
  if (flag === "branch" && !used.some((f) => perNode.includes(f)))
20974
- return jira ? "--branch needs {node}, {key}, {ticket} or {slug}: without one every node of an item gets the same branch, and the second would find the first one's PR and stop" : "--branch needs {node} or {slug} on a SLEDS route: its nodes share the item's {key} and have no {ticket}, so without one every node gets the same branch and the second would find the first one's PR and stop";
21299
+ return jira ? "--branch needs {node}, {key}, {ticket} or {slug}: without one every node of an item gets the same branch, and the second would find the first one's PR and stop" : "--branch needs {node} or {slug} on a SLEDS route: its nodes share the item's {key} and {ticket}, so without one every node gets the same branch and the second would find the first one's PR and stop";
20975
21300
  return null;
20976
21301
  }
20977
21302
  function defaultEngineName2(host) {
@@ -21329,7 +21654,7 @@ function engineClient2(o, org, breaker) {
21329
21654
  ...src,
21330
21655
  ...breaker ? { fetchFn: breaker.fetch } : {},
21331
21656
  timeoutMs: ENGINE_TIMEOUT_MS2,
21332
- version: "0.1.57"
21657
+ version: "0.1.58"
21333
21658
  });
21334
21659
  const asked = org?.trim() || env.SLEDS_ORG?.trim();
21335
21660
  if (asked && !selectOrg(readConfig(o.home), org, env).seat)
@@ -21528,6 +21853,7 @@ function engineDeps2(o, flags, source, runner, stateName) {
21528
21853
  defaultBranch,
21529
21854
  runnerOf: (name) => name === runner.name ? runner : restoreRunner(name, exec),
21530
21855
  prForBranch,
21856
+ otherWork,
21531
21857
  prView,
21532
21858
  mergeTree,
21533
21859
  prFiles,
@@ -21655,7 +21981,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
21655
21981
  runner: deps.runner.name,
21656
21982
  atOnce: flags.atOnce,
21657
21983
  interval: flags.interval,
21658
- cliVersion: "0.1.57"
21984
+ cliVersion: "0.1.58"
21659
21985
  };
21660
21986
  const beat = (checkedAt) => {
21661
21987
  const m = deps.machine;
@@ -23758,7 +24084,7 @@ function contextFor(deps) {
23758
24084
  ...src,
23759
24085
  fetchFn: deps.fetchFn,
23760
24086
  timeoutMs: deps.timeoutMs,
23761
- version: "0.1.57"
24087
+ version: "0.1.58"
23762
24088
  }), deps.home, now) : null;
23763
24089
  return {
23764
24090
  channel: "code",
@@ -23872,6 +24198,8 @@ When the person asks what is on their sled, what they hold, or what to pick up n
23872
24198
 
23873
24199
  Offer the next step in one line: take one (\`sled\` with \`take: "<key>"\`, or \`"<key>/B"\` for one node of its route), or read one whole first (\`item\`).
23874
24200
 
24201
+ An item already being built outside the engine (by hand, in another app) is marked so no engine plans or runs it: \`sleds route elsewhere <key> --note "<why>"\`. It stays on the sled and lands from its PR.
24202
+
23875
24203
  A key SLEDS cannot find may live in another org: the connector reads the org of the seat it signed in with. Say which org that is, and that the item has to be read from a seat in its own org. Do not report it as missing or done.
23876
24204
 
23877
24205
  ## Finish — leave what you learned
@@ -25553,12 +25881,13 @@ init_src();
25553
25881
  import { readFileSync as readFileSync29 } from "node:fs";
25554
25882
  import { hostname as hostname12 } from "node:os";
25555
25883
  var ARM_USAGE = "usage: sleds route arm <key> [--off] [--json] [--name <engine>] [--at-once <n>] [--runner tmux|headless|command|orca|cmux] [--runner-cmd '<launcher>'] [--agent '<command>'] [--mode <permission-mode>] [--model <id>] [--effort <e>] [--branch '<template>'] [--pr-title '<template>'] [--repos-dir <dir>]";
25556
- var USAGE2 = "usage: sleds route <key> [--long | --dot | --json] | sleds route arm <key> [--off] [--json] | sleds route answer <key>/<node> --stdin | sleds route propose <key> --file <nodes.json> | sleds route approve <key> | sleds route redraft <key> | sleds route single <key> | sleds route decisions [<key>] [--flagged] | sleds route decision <key> <id> --seen | --answer --stdin [--route accept|keep|redraft] | --keep-route | --apply [--answer --stdin] | --overrule --stdin | --ready | sleds route run <key> [--at-once <n>] [--runner tmux|headless|command|orca|cmux] [--runner-cmd '<launcher>'] [--agent '<command>'] [--interval <s>] [--lease <min>] [--idle <min>] [--repo <path>] [--repos-dir <dir>] [--mode <permission-mode>] [--model <id>] [--branch '<template>'] [--pr-title '<template>'] [--once]";
25884
+ var USAGE2 = "usage: sleds route <key> [--long | --dot | --json] | sleds route arm <key> [--off] [--json] | sleds route answer <key>/<node> --stdin | sleds route propose <key> --file <nodes.json> | sleds route approve <key> | sleds route redraft <key> | sleds route single <key> | sleds route elsewhere <key> [--note <text>] | sleds route decisions [<key>] [--flagged] | sleds route decision <key> <id> --seen | --answer --stdin [--route accept|keep|redraft] | --keep-route | --apply [--answer --stdin] | --overrule --stdin | --ready | sleds route run <key> [--at-once <n>] [--runner tmux|headless|command|orca|cmux] [--runner-cmd '<launcher>'] [--agent '<command>'] [--interval <s>] [--lease <min>] [--idle <min>] [--repo <path>] [--repos-dir <dir>] [--mode <permission-mode>] [--model <id>] [--branch '<template>'] [--pr-title '<template>'] [--once]";
25557
25885
  var ANSWER_USAGE = "usage: sleds route answer <key>/<node> <answer…> | sleds route answer <key>/<node> --stdin [--json]";
25558
25886
  var PROPOSE_USAGE = "usage: sleds route propose <key> (--file <nodes.json> | --stdin) [--name <engine>] [--json]";
25559
25887
  var APPROVE_USAGE = "usage: sleds route approve <key> [--file <nodes.json>] [--json]";
25560
25888
  var REDRAFT_USAGE = "usage: sleds route redraft <key> [--json]";
25561
25889
  var SINGLE_USAGE = "usage: sleds route single <key> [--json]";
25890
+ var ELSEWHERE_USAGE = "usage: sleds route elsewhere <key> [--note <text>] [--json]";
25562
25891
  var DECISION_USAGE = "usage: sleds route decision <key> <id> --seen | sleds route decision <key> <id> (--answer | --overrule) (<answer…> | --stdin) | sleds route decision <key> <id> [--answer (<answer…> | --stdin)] --route accept|keep|redraft | sleds route decision <key> <id> --keep-route | sleds route decision <key> <id> --apply [--answer (<answer…> | --stdin)] | sleds route decision <key> <id> --ready [--json]";
25563
25892
  var ROUTE_VERDICTS = ["accept", "keep", "redraft"];
25564
25893
  function nodesFromJson(text) {
@@ -25611,6 +25940,8 @@ function parseVerbArgs(argv, allowed) {
25611
25940
  out.file = value;
25612
25941
  if (flag === "name")
25613
25942
  out.name = value;
25943
+ if (flag === "note")
25944
+ out.note = value;
25614
25945
  i++;
25615
25946
  } else if (a.startsWith("-"))
25616
25947
  return { error: `unknown flag ${a}` };
@@ -25843,10 +26174,13 @@ async function redraftCmd(o, argv) {
25843
26174
  async function singleCmd(o, argv) {
25844
26175
  return oneVerb(o, argv, SINGLE_USAGE, { single: true }, "nothing changed", (key) => singleLine(key));
25845
26176
  }
25846
- async function oneVerb(o, argv, usage, body, unreachable, line) {
26177
+ async function elsewhereCmd(o, argv) {
26178
+ return oneVerb(o, argv, ELSEWHERE_USAGE, (a) => ({ elsewhere: true, ...a.note ? { note: a.note } : {} }), "nothing marked", (key) => elsewhereLine(key), ["note"]);
26179
+ }
26180
+ async function oneVerb(o, argv, usage, body, unreachable, line, flags = []) {
25847
26181
  const log = o.log ?? console.log;
25848
26182
  const error = o.error ?? console.error;
25849
- const a = parseVerbArgs(argv, ["json"]);
26183
+ const a = parseVerbArgs(argv, ["json", ...flags]);
25850
26184
  if ("error" in a) {
25851
26185
  error(a.error);
25852
26186
  error(usage);
@@ -25857,7 +26191,10 @@ async function oneVerb(o, argv, usage, body, unreachable, line) {
25857
26191
  error(client.error);
25858
26192
  return 1;
25859
26193
  }
25860
- const r = await client.postFull("/proxy/route", { project: a.key, ...body });
26194
+ const r = await client.postFull("/proxy/route", {
26195
+ project: a.key,
26196
+ ...typeof body === "function" ? body(a) : body
26197
+ });
25861
26198
  if (!r) {
25862
26199
  error(`${a.key}: SLEDS unreachable — ${unreachable}`);
25863
26200
  return 1;
@@ -25871,6 +26208,9 @@ function redraftLine(key) {
25871
26208
  function singleLine(key) {
25872
26209
  return `${key} runs as one node — it is not planned again`;
25873
26210
  }
26211
+ function elsewhereLine(key) {
26212
+ return `${key} marked built elsewhere — no engine runs it`;
26213
+ }
25874
26214
  function answeredLine(key, route, held) {
25875
26215
  const nodes = Array.isArray(held) && held.length ? ` (${held.join(", ")})` : "";
25876
26216
  if (route === "accept")
@@ -26138,6 +26478,8 @@ var routeCmd = async (o) => {
26138
26478
  return redraftCmd(o, o.argv.slice(1));
26139
26479
  if (o.argv[0] === "single")
26140
26480
  return singleCmd(o, o.argv.slice(1));
26481
+ if (o.argv[0] === "elsewhere")
26482
+ return elsewhereCmd(o, o.argv.slice(1));
26141
26483
  if (o.argv[0] === "decision")
26142
26484
  return decisionCmd(o, o.argv.slice(1));
26143
26485
  if (o.argv[0] === "decisions")
@@ -27876,7 +28218,7 @@ var realPidAlive4 = (pid) => {
27876
28218
  };
27877
28219
  function machineClient(home, env) {
27878
28220
  const src = meterSource(home, undefined, { env });
27879
- return src ? fetchClient({ ...src, version: "0.1.57" }) : null;
28221
+ return src ? fetchClient({ ...src, version: "0.1.58" }) : null;
27880
28222
  }
27881
28223
  async function noteAnswer2(home, a, now) {
27882
28224
  try {
@@ -28526,7 +28868,10 @@ commands:
28526
28868
  approve <key> a person's yes to a planned route: files it and, under
28527
28869
  Pickup: Auto, arms it; --file <nodes.json> approves an
28528
28870
  edited set instead (--json)
28529
- run <key> run the engine for this one item until its route lands
28871
+ elsewhere <key> built outside the engine: no engine plans or runs it, it
28872
+ stays on your sled and lands from its PR; --note "<why>"
28873
+ goes on the thread (--json)
28874
+ run <key> run the engine for this one item until its route lands
28530
28875
  --at-once <n> sessions at once (default 1; one per seat \u2014 tick seats
28531
28876
  when you press Run to run more than one)
28532
28877
  --runner tmux|headless|command|orca where each node runs (default tmux, any
@@ -28736,7 +29081,7 @@ var main = async (argv) => {
28736
29081
  now: Date.now(),
28737
29082
  stderr: (line) => console.error(line),
28738
29083
  isTTY: !!process.stderr.isTTY,
28739
- version: "0.1.57"
29084
+ version: "0.1.58"
28740
29085
  });
28741
29086
  switch (cmd) {
28742
29087
  case "update":
@@ -28969,7 +29314,7 @@ ${renderHelpText(argv[1])}`);
28969
29314
  case "version":
28970
29315
  case "--version":
28971
29316
  case "-v":
28972
- console.log("sleds 0.1.57");
29317
+ console.log("sleds 0.1.58");
28973
29318
  return 0;
28974
29319
  default:
28975
29320
  console.error(`unknown command: ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
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",