@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.
- package/README.md +1 -1
- package/dist/sleds.mjs +129 -39
- 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
|
|
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 ?
|
|
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
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
11055
|
-
|
|
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
|
|
11062
|
-
|
|
11063
|
-
|
|
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(() =>
|
|
11091
|
-
|
|
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
|
|
11132
|
-
const
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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",
|