@sleds/cli 0.1.39 → 0.1.41

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 +129 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -87,7 +87,7 @@ The flags `sleds route run` and `sleds engine` share:
87
87
  --repo <path> | --repos-dir <dir> where the node's repo is checked out
88
88
  ```
89
89
 
90
- `sleds engine` also takes `--name <n>` (default the hostname; it signs as `engine:<n>`), `--interval <s>`, `--lease <min>`, `--idle <min>` (default 20: a session showing nothing new and no PR is parked for a person, its last message the question, and the answer is typed back into it) and `--once` for a single tick. `--jira` reads `JIRA_BASE_URL`, `JIRA_EMAIL` and `JIRA_TOKEN`. `sleds help` has the full list.
90
+ `sleds engine` also takes `--name <n>` (default the hostname; it signs as `engine:<n>`), `--interval <s>`, `--lease <min>`, `--idle <min>` (default 20: a session showing nothing new and no PR is parked for a person, its last message the question, and the answer is typed back into it) and `--once` for a single tick. `--jira` reads `JIRA_BASE_URL`, `JIRA_EMAIL` and `JIRA_TOKEN`, and still needs a seat: before every write to Jira (a status move, a comment) it asks SLEDS, and writes nothing when the org runs Jira read only, the switch is off, the issue's project is not watched, SLEDS cannot be reached, or `JIRA_WRITES=off` is set in its environment (spec 39 rule 4). It will not start when SLEDS says no writes for the epic's project. `sleds help` has the full list.
91
91
 
92
92
  `sleds engine status [--name <n>] [--json]` says what that engine holds now: the nodes it runs (with where to watch each), the PRs it follows and their checks, the next check, and whether it is alive. It reads the engine's state file and the lock the engine keeps beside it (`<name>.lock`: its pid, written each tick, removed on a clean stop), not SLEDS, so it answers offline. `--json` is the contract a front end reads; both it and the state file carry a version, `v` (spec 28 §4.2). For a `sleds route run` engine, pass `--name <n>-<KEY>`.
93
93
 
package/dist/sleds.mjs CHANGED
@@ -10160,6 +10160,10 @@ var cmuxProbeReason = (r) => {
10160
10160
  };
10161
10161
  var cmuxAuthRefused = (r) => r.status !== 0 && /\b(auth_required|unauthorized|authentication required|invalid password)\b/i.test(`${r.stderr}
10162
10162
  ${r.stdout}`);
10163
+ var cmuxNeedsForce = (r) => r.status !== 0 && /\bconfirmation_required\b|--force\b/i.test(`${r.stderr}
10164
+ ${r.stdout}`);
10165
+ var CMUX_LIST_TRIES = 20;
10166
+ var CMUX_LIST_WAIT_MS = 250;
10163
10167
  var CMUX_SETTLE_MS = 300;
10164
10168
  var CMUX_ENTER_RETRIES = 2;
10165
10169
  var PROMPT_LINE = /^[\s│|]*[>❯›]\s?(.*)$/;
@@ -10213,6 +10217,12 @@ function cmuxRunner(exec, deps = {}) {
10213
10217
  return pw ? run(args) : r;
10214
10218
  };
10215
10219
  const failed = cmuxFailure;
10220
+ const closeWorkspace = (id) => {
10221
+ const c = cmux(["close-workspace", "--workspace", id]);
10222
+ if (!cmuxNeedsForce(c))
10223
+ return c.status === 0;
10224
+ return cmux(["close-workspace", "--workspace", id, "--force"]).status === 0;
10225
+ };
10216
10226
  return {
10217
10227
  name: "cmux",
10218
10228
  makesWorktree: false,
@@ -10252,19 +10262,29 @@ function cmuxRunner(exec, deps = {}) {
10252
10262
  const why = failed(r);
10253
10263
  const l = cmux(["--id-format", "both", "list-workspaces"]);
10254
10264
  const stray = l.status === 0 ? cmuxIdForName(l.stdout, o.name) : null;
10255
- const left = l.status !== 0 || (stray ? cmux(["close-workspace", "--workspace", stray]).status !== 0 : namedLines(l.stdout, o.name).length > 0);
10265
+ const left = l.status !== 0 || (stray ? !closeWorkspace(stray) : namedLines(l.stdout, o.name).length > 0);
10256
10266
  throw new Error(`cmux workspace create ${o.name} failed: ${why}${left ? `; ${UNWATCHED}` : ""}`);
10257
10267
  }
10258
10268
  let id = parseCmuxWorkspace(r.stdout);
10259
10269
  const ref = id ? null : parseCmuxRef(r.stdout);
10260
10270
  let closed = false;
10261
10271
  if (ref) {
10262
- const l = cmux(["--id-format", "both", "list-workspaces"]);
10263
- if (l.status === 0) {
10264
- id = cmuxIdForRef(l.stdout, ref, o.name);
10265
- const stray = id ? null : cmuxIdForName(l.stdout, o.name);
10272
+ let listed = null;
10273
+ for (let i = 0;i < CMUX_LIST_TRIES; i++) {
10274
+ if (i > 0)
10275
+ sleep(CMUX_LIST_WAIT_MS);
10276
+ const l = cmux(["--id-format", "both", "list-workspaces"]);
10277
+ if (l.status !== 0)
10278
+ continue;
10279
+ listed = l.stdout;
10280
+ if (namedLines(l.stdout, o.name).length > 0)
10281
+ break;
10282
+ }
10283
+ if (listed !== null) {
10284
+ id = cmuxIdForRef(listed, ref, o.name);
10285
+ const stray = id ? null : cmuxIdForName(listed, o.name);
10266
10286
  if (stray)
10267
- closed = cmux(["close-workspace", "--workspace", stray]).status === 0;
10287
+ closed = closeWorkspace(stray);
10268
10288
  }
10269
10289
  }
10270
10290
  if (!id) {
@@ -10402,7 +10422,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
10402
10422
  name: flags.name,
10403
10423
  os: host.os ?? process.platform,
10404
10424
  runner: flags.runner,
10405
- version: host.version ?? "0.1.39" ?? "dev",
10425
+ version: host.version ?? "0.1.41" ?? "dev",
10406
10426
  running: live.length,
10407
10427
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
10408
10428
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -11005,6 +11025,29 @@ function adfComment(text) {
11005
11025
  }
11006
11026
  };
11007
11027
  }
11028
+ function localWritesOff(env) {
11029
+ return env.JIRA_WRITES?.trim().toLowerCase() === "off";
11030
+ }
11031
+ function sledsJiraGate(client, env = process.env) {
11032
+ return {
11033
+ async ask(w) {
11034
+ if (localWritesOff(env))
11035
+ return "JIRA_WRITES=off is set here";
11036
+ const r = await client.postFull("/proxy/jira/write", { ...w }).catch(() => null);
11037
+ if (!r)
11038
+ return "SLEDS could not be reached to say whether writes to Jira are on";
11039
+ if (r.status !== 200 || !r.json)
11040
+ return `SLEDS would not say whether writes to Jira are on (HTTP ${r.status})`;
11041
+ const refusal = r.json.refusal;
11042
+ if (refusal === null)
11043
+ return null;
11044
+ return str6(r.json.why) ?? (typeof refusal === "string" ? refusal : "SLEDS refused the write");
11045
+ },
11046
+ async told(w, outcome) {
11047
+ await client.postFull("/proxy/jira/write", { ...w, outcome }).catch(() => null);
11048
+ }
11049
+ };
11050
+ }
11008
11051
  function jiraSource(o) {
11009
11052
  const fetchFn = o.fetchFn ?? fetch;
11010
11053
  const base = o.baseUrl.replace(/\/+$/, "");
@@ -11045,23 +11088,35 @@ function jiraSource(o) {
11045
11088
  byNode.set(n.node, n);
11046
11089
  return [epicItem(epic, nodes)];
11047
11090
  };
11091
+ const write = async (issue, sw, what, body) => {
11092
+ const w = {
11093
+ issueKey: issue,
11094
+ switch: sw,
11095
+ method: "POST",
11096
+ endpoint: `/issue/{key}/${what}`
11097
+ };
11098
+ const refused = await o.gate.ask(w).catch((e) => `SLEDS could not be reached (${e instanceof Error ? e.message : String(e)})`);
11099
+ if (refused)
11100
+ return { ok: false, status: 0, json: null, refused };
11101
+ const r = await call(`/rest/api/3/issue/${issue}/${what}`, { method: "POST", body });
11102
+ await o.gate.told(w, r.ok ? "sent" : `failed:${r.status}`).catch(() => {
11103
+ return;
11104
+ });
11105
+ return r;
11106
+ };
11048
11107
  const transitionTo = async (issue, category) => {
11049
11108
  const t = await call(`/rest/api/3/issue/${issue}/transitions`);
11050
11109
  const list = Array.isArray(t.json?.transitions) ? t.json.transitions.filter(isObject8) : [];
11051
11110
  const hit = list.find((x) => isObject8(x.to) && categoryOf({ status: x.to }) === category && x.id !== undefined);
11052
11111
  if (!hit)
11053
- return false;
11054
- const r = await call(`/rest/api/3/issue/${issue}/transitions`, {
11055
- method: "POST",
11056
- body: { transition: { id: String(hit.id) } }
11057
- });
11058
- return r.ok;
11112
+ return { ok: false };
11113
+ const r = await write(issue, "status", "transitions", { transition: { id: String(hit.id) } });
11114
+ return { ok: r.ok, ...r.refused ? { refused: r.refused } : {} };
11059
11115
  };
11060
11116
  const comment = async (issue, text) => {
11061
- const r = await call(`/rest/api/3/issue/${issue}/comment`, {
11062
- method: "POST",
11063
- body: adfComment(text)
11064
- });
11117
+ const r = await write(issue, "comments", "comment", adfComment(text));
11118
+ if (r.refused)
11119
+ throw new Error(`Jira: not written to ${issue} — ${r.refused}`);
11065
11120
  if (!r.ok)
11066
11121
  throw new Error(`Jira: could not comment on ${issue} — ${errorOf(r)}`);
11067
11122
  };
@@ -11087,8 +11142,16 @@ function jiraSource(o) {
11087
11142
  const pack = `# ${n.issue} · ${n.summary}
11088
11143
 
11089
11144
  ${body}`.trimEnd();
11090
- const moved = await transitionTo(n.issue, "indeterminate").catch(() => false);
11091
- if (!moved)
11145
+ const moved = await transitionTo(n.issue, "indeterminate").catch(() => ({
11146
+ ok: false,
11147
+ refused: undefined
11148
+ }));
11149
+ if (moved.refused)
11150
+ return {
11151
+ ok: false,
11152
+ error: `not taken: not written to Jira (${n.issue}) — ${moved.refused}`
11153
+ };
11154
+ if (!moved.ok)
11092
11155
  return {
11093
11156
  ok: false,
11094
11157
  error: `Jira would not move ${n.issue} to In Progress (no such transition, or no permission) — the engine needs it to mark the node taken`
@@ -11115,8 +11178,8 @@ ${body}`.trimEnd();
11115
11178
  if (outcome.kind === "pr")
11116
11179
  return comment(issue, `PR opened by ${by}: ${outcome.ref}`);
11117
11180
  if (outcome.kind === "merged") {
11118
- const done = await transitionTo(issue, "done").catch(() => false);
11119
- return comment(issue, done ? `PR merged by ${by}'s engine: ${outcome.ref}` : `PR merged: ${outcome.ref}. Jira would not move this to Done (no such transition, or no permission) — move it so the nodes it blocks can start.`);
11181
+ const done = await transitionTo(issue, "done").catch(() => ({ ok: false }));
11182
+ return comment(issue, done.ok ? `PR merged by ${by}'s engine: ${outcome.ref}` : `PR merged: ${outcome.ref}. Jira would not move this to Done (no such transition, or no permission) — move it so the nodes it blocks can start.`);
11120
11183
  }
11121
11184
  if (outcome.kind === "park")
11122
11185
  return comment(issue, outcome.question);
@@ -11128,20 +11191,21 @@ ${outcome.log.slice(-JIRA_LOG_MAX)}`);
11128
11191
  }
11129
11192
  };
11130
11193
  }
11131
- function jiraFromEnv(epic, env = process.env) {
11132
- const baseUrl = env.JIRA_BASE_URL?.trim();
11133
- const email = env.JIRA_EMAIL?.trim();
11134
- const token = env.JIRA_TOKEN?.trim();
11135
- const missing = [
11136
- baseUrl ? null : "JIRA_BASE_URL",
11137
- email ? null : "JIRA_EMAIL",
11138
- token ? null : "JIRA_TOKEN"
11139
- ].filter((m) => m !== null);
11140
- if (missing.length > 0 || !baseUrl || !email || !token)
11141
- return {
11142
- error: `Jira source needs ${missing.join(", ")} (e.g. JIRA_BASE_URL=https://qwick.atlassian.net, an API token in JIRA_TOKEN)`
11143
- };
11144
- return jiraSource({ epic, baseUrl, email, token });
11194
+ function jiraEnvError(env) {
11195
+ const missing = ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_TOKEN"].filter((k) => !env[k]?.trim());
11196
+ return missing.length > 0 ? `Jira source needs ${missing.join(", ")} (e.g. JIRA_BASE_URL=https://qwick.atlassian.net, an API token in JIRA_TOKEN)` : null;
11197
+ }
11198
+ function jiraFromEnv(epic, gate, env = process.env) {
11199
+ const error = jiraEnvError(env);
11200
+ if (error)
11201
+ return { error };
11202
+ return jiraSource({
11203
+ epic,
11204
+ baseUrl: String(env.JIRA_BASE_URL).trim(),
11205
+ email: String(env.JIRA_EMAIL).trim(),
11206
+ token: String(env.JIRA_TOKEN).trim(),
11207
+ gate
11208
+ });
11145
11209
  }
11146
11210
 
11147
11211
  // src/engine/loop.ts
@@ -16250,7 +16314,7 @@ async function driveEngine(o, flags, deps, until = {}) {
16250
16314
  runner: deps.runner.name,
16251
16315
  atOnce: flags.atOnce,
16252
16316
  interval: flags.interval,
16253
- cliVersion: "0.1.39"
16317
+ cliVersion: "0.1.41"
16254
16318
  };
16255
16319
  const beat = (checkedAt) => {
16256
16320
  const m = deps.machine;
@@ -18067,6 +18131,15 @@ function orgClient2(o, flag) {
18067
18131
  const org = engineOrg2(o, flag);
18068
18132
  return engineClient2(o, org.seated ? org.slug ?? undefined : flag);
18069
18133
  }
18134
+ async function jiraWritesAtStart(gate, epic) {
18135
+ const no = await gate.ask({
18136
+ issueKey: epic.trim().toUpperCase(),
18137
+ switch: "status",
18138
+ method: "POST",
18139
+ endpoint: "/issue/{key}/transitions"
18140
+ });
18141
+ return no ? `--jira ${epic}: not starting — SLEDS says no writes to Jira: ${no}. A take moves the issue to In Progress, so the engine cannot take a node without one.` : null;
18142
+ }
18070
18143
  function realPidAlive2(pid) {
18071
18144
  try {
18072
18145
  process.kill(pid, 0);
@@ -18303,7 +18376,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
18303
18376
  runner: deps.runner.name,
18304
18377
  atOnce: flags.atOnce,
18305
18378
  interval: flags.interval,
18306
- cliVersion: "0.1.39"
18379
+ cliVersion: "0.1.41"
18307
18380
  };
18308
18381
  const beat = (checkedAt) => {
18309
18382
  const m = deps.machine;
@@ -18554,7 +18627,24 @@ var engineCmd = async (o) => {
18554
18627
  if (o.source)
18555
18628
  source = o.source;
18556
18629
  else if (flags.jira) {
18557
- const j = jiraFromEnv(flags.jira, o.env ?? process.env);
18630
+ const env = o.env ?? process.env;
18631
+ const missing = jiraEnvError(env);
18632
+ if (missing) {
18633
+ error(missing);
18634
+ return 1;
18635
+ }
18636
+ const c = o.client ?? orgClient2(o, given);
18637
+ if ("error" in c) {
18638
+ error(`--jira asks SLEDS before every write to Jira (spec 39), so it needs a seat: ${c.error}`);
18639
+ return 1;
18640
+ }
18641
+ const gate = sledsJiraGate(c, env);
18642
+ const no = await jiraWritesAtStart(gate, flags.jira);
18643
+ if (no) {
18644
+ error(no);
18645
+ return 1;
18646
+ }
18647
+ const j = jiraFromEnv(flags.jira, gate, env);
18558
18648
  if ("error" in j) {
18559
18649
  error(j.error);
18560
18650
  return 1;
@@ -24763,7 +24853,7 @@ ${renderHelpText(argv[1])}`);
24763
24853
  case "version":
24764
24854
  case "--version":
24765
24855
  case "-v":
24766
- console.log("sleds 0.1.39");
24856
+ console.log("sleds 0.1.41");
24767
24857
  return 0;
24768
24858
  default:
24769
24859
  console.error(`unknown command: ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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",