@sleds/cli 0.1.40 → 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 +103 -33
  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
@@ -10422,7 +10422,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
10422
10422
  name: flags.name,
10423
10423
  os: host.os ?? process.platform,
10424
10424
  runner: flags.runner,
10425
- version: host.version ?? "0.1.40" ?? "dev",
10425
+ version: host.version ?? "0.1.41" ?? "dev",
10426
10426
  running: live.length,
10427
10427
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
10428
10428
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -11025,6 +11025,29 @@ function adfComment(text) {
11025
11025
  }
11026
11026
  };
11027
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
+ }
11028
11051
  function jiraSource(o) {
11029
11052
  const fetchFn = o.fetchFn ?? fetch;
11030
11053
  const base = o.baseUrl.replace(/\/+$/, "");
@@ -11065,23 +11088,35 @@ function jiraSource(o) {
11065
11088
  byNode.set(n.node, n);
11066
11089
  return [epicItem(epic, nodes)];
11067
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
+ };
11068
11107
  const transitionTo = async (issue, category) => {
11069
11108
  const t = await call(`/rest/api/3/issue/${issue}/transitions`);
11070
11109
  const list = Array.isArray(t.json?.transitions) ? t.json.transitions.filter(isObject8) : [];
11071
11110
  const hit = list.find((x) => isObject8(x.to) && categoryOf({ status: x.to }) === category && x.id !== undefined);
11072
11111
  if (!hit)
11073
- return false;
11074
- const r = await call(`/rest/api/3/issue/${issue}/transitions`, {
11075
- method: "POST",
11076
- body: { transition: { id: String(hit.id) } }
11077
- });
11078
- 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 } : {} };
11079
11115
  };
11080
11116
  const comment = async (issue, text) => {
11081
- const r = await call(`/rest/api/3/issue/${issue}/comment`, {
11082
- method: "POST",
11083
- body: adfComment(text)
11084
- });
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}`);
11085
11120
  if (!r.ok)
11086
11121
  throw new Error(`Jira: could not comment on ${issue} — ${errorOf(r)}`);
11087
11122
  };
@@ -11107,8 +11142,16 @@ function jiraSource(o) {
11107
11142
  const pack = `# ${n.issue} · ${n.summary}
11108
11143
 
11109
11144
  ${body}`.trimEnd();
11110
- const moved = await transitionTo(n.issue, "indeterminate").catch(() => false);
11111
- 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)
11112
11155
  return {
11113
11156
  ok: false,
11114
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`
@@ -11135,8 +11178,8 @@ ${body}`.trimEnd();
11135
11178
  if (outcome.kind === "pr")
11136
11179
  return comment(issue, `PR opened by ${by}: ${outcome.ref}`);
11137
11180
  if (outcome.kind === "merged") {
11138
- const done = await transitionTo(issue, "done").catch(() => false);
11139
- 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.`);
11140
11183
  }
11141
11184
  if (outcome.kind === "park")
11142
11185
  return comment(issue, outcome.question);
@@ -11148,20 +11191,21 @@ ${outcome.log.slice(-JIRA_LOG_MAX)}`);
11148
11191
  }
11149
11192
  };
11150
11193
  }
11151
- function jiraFromEnv(epic, env = process.env) {
11152
- const baseUrl = env.JIRA_BASE_URL?.trim();
11153
- const email = env.JIRA_EMAIL?.trim();
11154
- const token = env.JIRA_TOKEN?.trim();
11155
- const missing = [
11156
- baseUrl ? null : "JIRA_BASE_URL",
11157
- email ? null : "JIRA_EMAIL",
11158
- token ? null : "JIRA_TOKEN"
11159
- ].filter((m) => m !== null);
11160
- if (missing.length > 0 || !baseUrl || !email || !token)
11161
- return {
11162
- error: `Jira source needs ${missing.join(", ")} (e.g. JIRA_BASE_URL=https://qwick.atlassian.net, an API token in JIRA_TOKEN)`
11163
- };
11164
- 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
+ });
11165
11209
  }
11166
11210
 
11167
11211
  // src/engine/loop.ts
@@ -16270,7 +16314,7 @@ async function driveEngine(o, flags, deps, until = {}) {
16270
16314
  runner: deps.runner.name,
16271
16315
  atOnce: flags.atOnce,
16272
16316
  interval: flags.interval,
16273
- cliVersion: "0.1.40"
16317
+ cliVersion: "0.1.41"
16274
16318
  };
16275
16319
  const beat = (checkedAt) => {
16276
16320
  const m = deps.machine;
@@ -18087,6 +18131,15 @@ function orgClient2(o, flag) {
18087
18131
  const org = engineOrg2(o, flag);
18088
18132
  return engineClient2(o, org.seated ? org.slug ?? undefined : flag);
18089
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
+ }
18090
18143
  function realPidAlive2(pid) {
18091
18144
  try {
18092
18145
  process.kill(pid, 0);
@@ -18323,7 +18376,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
18323
18376
  runner: deps.runner.name,
18324
18377
  atOnce: flags.atOnce,
18325
18378
  interval: flags.interval,
18326
- cliVersion: "0.1.40"
18379
+ cliVersion: "0.1.41"
18327
18380
  };
18328
18381
  const beat = (checkedAt) => {
18329
18382
  const m = deps.machine;
@@ -18574,7 +18627,24 @@ var engineCmd = async (o) => {
18574
18627
  if (o.source)
18575
18628
  source = o.source;
18576
18629
  else if (flags.jira) {
18577
- 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);
18578
18648
  if ("error" in j) {
18579
18649
  error(j.error);
18580
18650
  return 1;
@@ -24783,7 +24853,7 @@ ${renderHelpText(argv[1])}`);
24783
24853
  case "version":
24784
24854
  case "--version":
24785
24855
  case "-v":
24786
- console.log("sleds 0.1.40");
24856
+ console.log("sleds 0.1.41");
24787
24857
  return 0;
24788
24858
  default:
24789
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.40",
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",