@sleds/cli 0.1.40 → 0.1.42

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 +146 -54
  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
@@ -345,11 +345,11 @@ var init_guides = __esm(() => {
345
345
  {
346
346
  do: "Open `Work` and read the tabs",
347
347
  where: "Work",
348
- note: "Four tabs, each with its count: Waiting for product · Ready · On sleds · Landed this week. The search at the top narrows the rows as you type; the chips beside the tabs narrow by source, worth, type and `filed by` (you first). Your own work, and what is in front of you, is on Sleds."
348
+ note: "Three tabs, each with its count: Ready · On sleds · Landed this week; what waits for product's yes is on Intake (a line above the tabs says how many). The search at the top narrows the rows as you type; the chips beside the tabs narrow by source, worth, type and `filed by` (you first). Your own work, and what is in front of you, is on Sleds."
349
349
  },
350
350
  {
351
351
  do: "Read a pool item: the band, the worth, the chip, the readiness",
352
- note: "`suggested` (dashed, with a star) names the sled the rules (and the model) suggest, with why on hover; `waiting for product` needs a yes; the five boxes say what the item has (why · done · worth · repo · route) and `N gaps` names what is missing before anyone could start; an amber note says a related or duplicate item exists. `Take it` puts it on your sled; the ⋯ menu can put it on the suggested sled."
352
+ note: "`suggested` (dashed, with a star) names the sled the rules (and the model) suggest, with why on hover; `waiting for product` needs a yes (on Intake, where a row opens a modal to decide it); the five boxes say what the item has (why · done · worth · repo · route) and `N gaps` names what is missing before anyone could start; an amber note says a related or duplicate item exists. `Take it` puts it on your sled; the ⋯ menu can put it on the suggested sled."
353
353
  },
354
354
  {
355
355
  do: "Read a sled card: the seats, the jobs, the band spent",
@@ -679,16 +679,16 @@ var init_guides = __esm(() => {
679
679
  note: "Emails, comma-separated. Blank means any owner or admin of the workspace."
680
680
  },
681
681
  {
682
- do: "Approvers decide from the board or the Overview",
683
- where: "Overview › `Waiting on product`, or the item on Work",
684
- note: "`Approve` opens the gate; `Defer` keeps it closed and carries the note back to the filer, who answers in the brief and it comes back. In chat the same is `approve {project, decision, note?}`."
682
+ do: "Approvers decide on Intake",
683
+ where: "Intake (the rail), or Overview › `Waiting on product`",
684
+ note: "A row opens the ask in a modal over the ledger: `Approve` opens the gate; `Defer` keeps it closed and carries the note back to the filer, who answers in the brief and it comes back; `Ask the filer` sends the one question. A, D and Q do the same from the keyboard and move to the next ask. In chat the same is `approve {project, decision, note?}`."
685
685
  },
686
686
  {
687
687
  do: "Plan after the yes",
688
688
  note: "An approved epic can be planned in chat (`plan`): its milestones are sized, filed under it, and inherit the yes — they do not queue again. The epic's cap is the sum."
689
689
  }
690
690
  ],
691
- check: "File an ask from chat (`intake`): its item shows waiting for product in the pool on Work, the approvers see it under Waiting on product, and `run` on it is refused until someone approves."
691
+ check: "File an ask from chat (`intake`): its item shows waiting for product on Intake, the approvers see it under Waiting on product, and `run` on it is refused until someone approves."
692
692
  }
693
693
  ];
694
694
  });
@@ -1346,6 +1346,9 @@ var init_related = __esm(() => {
1346
1346
  "via"
1347
1347
  ]);
1348
1348
  });
1349
+
1350
+ // ../../packages/shared/src/intake/rank.ts
1351
+ var init_rank = () => {};
1349
1352
  // ../../packages/shared/src/profile/profile.ts
1350
1353
  var LIGHT;
1351
1354
  var init_profile = __esm(() => {
@@ -5543,6 +5546,34 @@ var init_fieldMap = __esm(() => {
5543
5546
  var SEPT_30 = "/whats-new/2026-09-30", WHATS_NEW;
5544
5547
  var init_whatsNew = __esm(() => {
5545
5548
  WHATS_NEW = [
5549
+ {
5550
+ date: "2026-10-20",
5551
+ title: "Intake: the gate gets its own page",
5552
+ lede: "What waits for product's yes was the first tab on Work. It is a page now, Intake, between Work and Sleds on the rail: the same rows, with the policy an approver decides under on one line, the counts over them, and a modal that reads one ask fast and decides it without leaving the list. Work keeps what is ready and who is on it, and opens on Ready.",
5553
+ sections: [
5554
+ {
5555
+ title: "One ask at a time, over the ledger",
5556
+ where: "Intake › a row",
5557
+ body: "A row on Intake opens a centred modal, not the item's page: what SLEDS says and why, the six steps from filed to landed with this one's place on them, the brief, and — folded, each with its answer on the fold — the ticket, the split and who takes it. Approve, Defer with a note and Ask the filer are at the foot, with Open the full item for anything deeper. ‹ › and the arrow keys walk the ledger in its order; A, D and Q decide and move on; Decide in order starts at the top. Esc goes back to the ledger as it was."
5558
+ },
5559
+ {
5560
+ title: "The policy on the page",
5561
+ where: "Intake, above the rows",
5562
+ body: "Above the rows, one dashed line says whose yes is needed and for which asks, who the approvers are, and what a yes writes to Jira — read from Policy, with Change in Policy beside it. Under it, four counts: open, need a worth, waiting on a filer, and became work this week (which opens the Record)."
5563
+ },
5564
+ {
5565
+ title: "Where things went on Work",
5566
+ where: "Work",
5567
+ body: "Work's tabs are Ready, On sleds and Landed. A line above them says how many wait in Intake and links there; the Board layout says the same in place of its Waiting column. The rail's count under Work is what is ready now; Intake's is what waits. Old links — Approvals, a waiting lane, a search hit on a waiting item — land on Intake."
5568
+ }
5569
+ ],
5570
+ also: [
5571
+ {
5572
+ label: "Next",
5573
+ text: "Worth from the modal and from Jira's Business Value, the return of each ask against its time, SLEDS's own suggested verdict, and auto-approve in shadow mode first — spec 41's later slices."
5574
+ }
5575
+ ]
5576
+ },
5546
5577
  {
5547
5578
  date: "2026-10-10",
5548
5579
  title: "Docs beside the work, and webhooks for your own tools",
@@ -7523,6 +7554,7 @@ var init_src = __esm(() => {
7523
7554
  init_verbs();
7524
7555
  init_brief();
7525
7556
  init_related();
7557
+ init_rank();
7526
7558
  init_markdown2();
7527
7559
  init_plan3();
7528
7560
  init_recommend();
@@ -8222,6 +8254,7 @@ init_guides();
8222
8254
  init_verbs();
8223
8255
  init_brief();
8224
8256
  init_related();
8257
+ init_rank();
8225
8258
  init_markdown2();
8226
8259
  init_plan3();
8227
8260
  init_recommend();
@@ -10267,28 +10300,17 @@ function cmuxRunner(exec, deps = {}) {
10267
10300
  }
10268
10301
  let id = parseCmuxWorkspace(r.stdout);
10269
10302
  const ref = id ? null : parseCmuxRef(r.stdout);
10270
- let closed = false;
10271
10303
  if (ref) {
10272
- let listed = null;
10273
- for (let i = 0;i < CMUX_LIST_TRIES; i++) {
10304
+ for (let i = 0;i < CMUX_LIST_TRIES && !id; i++) {
10274
10305
  if (i > 0)
10275
10306
  sleep(CMUX_LIST_WAIT_MS);
10276
10307
  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);
10286
- if (stray)
10287
- closed = closeWorkspace(stray);
10308
+ if (l.status === 0)
10309
+ id = cmuxIdForRef(l.stdout, ref, o.name);
10288
10310
  }
10289
10311
  }
10290
10312
  if (!id) {
10291
- throw new Error(ref ? `cmux workspace create ${o.name} gave ${ref} but no workspace id for it; ${closed ? "that workspace was closed" : UNWATCHED}` : `cmux workspace create ${o.name} returned no workspace (${r.stdout.trim() || "nothing"}); ${UNWATCHED}`);
10313
+ throw new Error(ref ? `cmux workspace create ${o.name} gave ${ref} but no workspace id for it; ${UNWATCHED}` : `cmux workspace create ${o.name} returned no workspace (${r.stdout.trim() || "nothing"}); ${UNWATCHED}`);
10292
10314
  }
10293
10315
  if (group && listed && !joined) {
10294
10316
  const c = cmux([
@@ -10422,7 +10444,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
10422
10444
  name: flags.name,
10423
10445
  os: host.os ?? process.platform,
10424
10446
  runner: flags.runner,
10425
- version: host.version ?? "0.1.40" ?? "dev",
10447
+ version: host.version ?? "0.1.42" ?? "dev",
10426
10448
  running: live.length,
10427
10449
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
10428
10450
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -11025,6 +11047,29 @@ function adfComment(text) {
11025
11047
  }
11026
11048
  };
11027
11049
  }
11050
+ function localWritesOff(env) {
11051
+ return env.JIRA_WRITES?.trim().toLowerCase() === "off";
11052
+ }
11053
+ function sledsJiraGate(client, env = process.env) {
11054
+ return {
11055
+ async ask(w) {
11056
+ if (localWritesOff(env))
11057
+ return "JIRA_WRITES=off is set here";
11058
+ const r = await client.postFull("/proxy/jira/write", { ...w }).catch(() => null);
11059
+ if (!r)
11060
+ return "SLEDS could not be reached to say whether writes to Jira are on";
11061
+ if (r.status !== 200 || !r.json)
11062
+ return `SLEDS would not say whether writes to Jira are on (HTTP ${r.status})`;
11063
+ const refusal = r.json.refusal;
11064
+ if (refusal === null)
11065
+ return null;
11066
+ return str6(r.json.why) ?? (typeof refusal === "string" ? refusal : "SLEDS refused the write");
11067
+ },
11068
+ async told(w, outcome) {
11069
+ await client.postFull("/proxy/jira/write", { ...w, outcome }).catch(() => null);
11070
+ }
11071
+ };
11072
+ }
11028
11073
  function jiraSource(o) {
11029
11074
  const fetchFn = o.fetchFn ?? fetch;
11030
11075
  const base = o.baseUrl.replace(/\/+$/, "");
@@ -11065,23 +11110,35 @@ function jiraSource(o) {
11065
11110
  byNode.set(n.node, n);
11066
11111
  return [epicItem(epic, nodes)];
11067
11112
  };
11113
+ const write = async (issue, sw, what, body) => {
11114
+ const w = {
11115
+ issueKey: issue,
11116
+ switch: sw,
11117
+ method: "POST",
11118
+ endpoint: `/issue/{key}/${what}`
11119
+ };
11120
+ const refused = await o.gate.ask(w).catch((e) => `SLEDS could not be reached (${e instanceof Error ? e.message : String(e)})`);
11121
+ if (refused)
11122
+ return { ok: false, status: 0, json: null, refused };
11123
+ const r = await call(`/rest/api/3/issue/${issue}/${what}`, { method: "POST", body });
11124
+ await o.gate.told(w, r.ok ? "sent" : `failed:${r.status}`).catch(() => {
11125
+ return;
11126
+ });
11127
+ return r;
11128
+ };
11068
11129
  const transitionTo = async (issue, category) => {
11069
11130
  const t = await call(`/rest/api/3/issue/${issue}/transitions`);
11070
11131
  const list = Array.isArray(t.json?.transitions) ? t.json.transitions.filter(isObject8) : [];
11071
11132
  const hit = list.find((x) => isObject8(x.to) && categoryOf({ status: x.to }) === category && x.id !== undefined);
11072
11133
  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;
11134
+ return { ok: false };
11135
+ const r = await write(issue, "status", "transitions", { transition: { id: String(hit.id) } });
11136
+ return { ok: r.ok, ...r.refused ? { refused: r.refused } : {} };
11079
11137
  };
11080
11138
  const comment = async (issue, text) => {
11081
- const r = await call(`/rest/api/3/issue/${issue}/comment`, {
11082
- method: "POST",
11083
- body: adfComment(text)
11084
- });
11139
+ const r = await write(issue, "comments", "comment", adfComment(text));
11140
+ if (r.refused)
11141
+ throw new Error(`Jira: not written to ${issue} — ${r.refused}`);
11085
11142
  if (!r.ok)
11086
11143
  throw new Error(`Jira: could not comment on ${issue} — ${errorOf(r)}`);
11087
11144
  };
@@ -11107,8 +11164,16 @@ function jiraSource(o) {
11107
11164
  const pack = `# ${n.issue} · ${n.summary}
11108
11165
 
11109
11166
  ${body}`.trimEnd();
11110
- const moved = await transitionTo(n.issue, "indeterminate").catch(() => false);
11111
- if (!moved)
11167
+ const moved = await transitionTo(n.issue, "indeterminate").catch(() => ({
11168
+ ok: false,
11169
+ refused: undefined
11170
+ }));
11171
+ if (moved.refused)
11172
+ return {
11173
+ ok: false,
11174
+ error: `not taken: not written to Jira (${n.issue}) — ${moved.refused}`
11175
+ };
11176
+ if (!moved.ok)
11112
11177
  return {
11113
11178
  ok: false,
11114
11179
  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 +11200,8 @@ ${body}`.trimEnd();
11135
11200
  if (outcome.kind === "pr")
11136
11201
  return comment(issue, `PR opened by ${by}: ${outcome.ref}`);
11137
11202
  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.`);
11203
+ const done = await transitionTo(issue, "done").catch(() => ({ ok: false }));
11204
+ 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
11205
  }
11141
11206
  if (outcome.kind === "park")
11142
11207
  return comment(issue, outcome.question);
@@ -11148,20 +11213,21 @@ ${outcome.log.slice(-JIRA_LOG_MAX)}`);
11148
11213
  }
11149
11214
  };
11150
11215
  }
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 });
11216
+ function jiraEnvError(env) {
11217
+ const missing = ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_TOKEN"].filter((k) => !env[k]?.trim());
11218
+ 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;
11219
+ }
11220
+ function jiraFromEnv(epic, gate, env = process.env) {
11221
+ const error = jiraEnvError(env);
11222
+ if (error)
11223
+ return { error };
11224
+ return jiraSource({
11225
+ epic,
11226
+ baseUrl: String(env.JIRA_BASE_URL).trim(),
11227
+ email: String(env.JIRA_EMAIL).trim(),
11228
+ token: String(env.JIRA_TOKEN).trim(),
11229
+ gate
11230
+ });
11165
11231
  }
11166
11232
 
11167
11233
  // src/engine/loop.ts
@@ -16270,7 +16336,7 @@ async function driveEngine(o, flags, deps, until = {}) {
16270
16336
  runner: deps.runner.name,
16271
16337
  atOnce: flags.atOnce,
16272
16338
  interval: flags.interval,
16273
- cliVersion: "0.1.40"
16339
+ cliVersion: "0.1.42"
16274
16340
  };
16275
16341
  const beat = (checkedAt) => {
16276
16342
  const m = deps.machine;
@@ -18087,6 +18153,15 @@ function orgClient2(o, flag) {
18087
18153
  const org = engineOrg2(o, flag);
18088
18154
  return engineClient2(o, org.seated ? org.slug ?? undefined : flag);
18089
18155
  }
18156
+ async function jiraWritesAtStart(gate, epic) {
18157
+ const no = await gate.ask({
18158
+ issueKey: epic.trim().toUpperCase(),
18159
+ switch: "status",
18160
+ method: "POST",
18161
+ endpoint: "/issue/{key}/transitions"
18162
+ });
18163
+ 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;
18164
+ }
18090
18165
  function realPidAlive2(pid) {
18091
18166
  try {
18092
18167
  process.kill(pid, 0);
@@ -18323,7 +18398,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
18323
18398
  runner: deps.runner.name,
18324
18399
  atOnce: flags.atOnce,
18325
18400
  interval: flags.interval,
18326
- cliVersion: "0.1.40"
18401
+ cliVersion: "0.1.42"
18327
18402
  };
18328
18403
  const beat = (checkedAt) => {
18329
18404
  const m = deps.machine;
@@ -18574,7 +18649,24 @@ var engineCmd = async (o) => {
18574
18649
  if (o.source)
18575
18650
  source = o.source;
18576
18651
  else if (flags.jira) {
18577
- const j = jiraFromEnv(flags.jira, o.env ?? process.env);
18652
+ const env = o.env ?? process.env;
18653
+ const missing = jiraEnvError(env);
18654
+ if (missing) {
18655
+ error(missing);
18656
+ return 1;
18657
+ }
18658
+ const c = o.client ?? orgClient2(o, given);
18659
+ if ("error" in c) {
18660
+ error(`--jira asks SLEDS before every write to Jira (spec 39), so it needs a seat: ${c.error}`);
18661
+ return 1;
18662
+ }
18663
+ const gate = sledsJiraGate(c, env);
18664
+ const no = await jiraWritesAtStart(gate, flags.jira);
18665
+ if (no) {
18666
+ error(no);
18667
+ return 1;
18668
+ }
18669
+ const j = jiraFromEnv(flags.jira, gate, env);
18578
18670
  if ("error" in j) {
18579
18671
  error(j.error);
18580
18672
  return 1;
@@ -24783,7 +24875,7 @@ ${renderHelpText(argv[1])}`);
24783
24875
  case "version":
24784
24876
  case "--version":
24785
24877
  case "-v":
24786
- console.log("sleds 0.1.40");
24878
+ console.log("sleds 0.1.42");
24787
24879
  return 0;
24788
24880
  default:
24789
24881
  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.42",
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",