@sleds/cli 0.1.44 → 0.1.45

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 (2) hide show
  1. package/dist/sleds.mjs +414 -46
  2. package/package.json +1 -1
package/dist/sleds.mjs CHANGED
@@ -802,6 +802,13 @@ var init_verbs = __esm(() => {
802
802
  app: "Work › the item › click a field",
803
803
  how: "The filer, an approver or an admin changes an item from chat or by clicking the field in the app; every change says who and through what (via Claude · Cowork, via ChatGPT, in the app) and the record keeps before and after. A change to the ask, done or smallest marks the band stale with a Re-size at hand — never re-sized silently. Worth is the person's word in their words ('high · Acme renewal', '$5k–20k'; null clears it); on an epic, `carry: true` puts it on each child that has none, each child's record signed the same way. A mirrored ticket is the tracker's word and is not changed here."
804
804
  },
805
+ {
806
+ id: "reopen",
807
+ group: "meter",
808
+ want: "Put back an item that landed by mistake",
809
+ mcp: "reopen {project, reason}",
810
+ how: "SL-1819. For an item a skipped node or a wrong actual landed. It goes back on the sled it was on — or to ready on no sled, when that sled is gone from the org — the false actual is cleared and the band stands. A reopen always says why; the why goes on the item's record. The filer, an approver or an admin. Only a landed item reopens."
811
+ },
805
812
  {
806
813
  id: "archive",
807
814
  group: "meter",
@@ -2079,7 +2086,7 @@ ${state}`)}, fillcolor="${fill}", color="${border}"${style}];`);
2079
2086
  `)}
2080
2087
  `;
2081
2088
  }
2082
- var ROUTE_APPROACH_MAX = 2000, PLAN_APPROACH_MAX = 1200, PLAN_LIST_MAX = 8, PLAN_LINE_MAX = 240, CONTROL, MAX_NODES = 24, DEPTH_WARN = 4, norm2 = (s) => s.trim().toUpperCase(), AREA_ROOTS, NOT_AREAS, sameRepo = (a, b) => a.length === 0 || b.length === 0 || a.some((r) => b.includes(r)), DOT_COLORS, ROUTE_DRAFT_RULE = "Draft the route: PR-sized nodes, file-disjoint where they run in parallel, each with a title, the files or areas it touches, the repo, and the nodes it waits on (by key: A, B, C…). Prefer wide waves over chains deeper than four. Make the tough calls once, so each node's session follows them: an `approach` paragraph for the whole route, and on each node a `plan` — its approach, what it `provides` that later nodes rely on (names, shapes, files), what it `expects` from the nodes it waits on, and the `doneChecks` that say it landed. Show the graph and the plans as text and ask for a yes; file nothing before it. Then `route {q, approach, nodes: [{key, title, touches, repos, blockedBy, plan: {approach, provides, expects, doneChecks}}], confirm: true}`.";
2089
+ var ROUTE_APPROACH_MAX = 2000, PLAN_APPROACH_MAX = 1200, PLAN_LIST_MAX = 8, PLAN_LINE_MAX = 240, CONTROL, MAX_NODES = 24, DEPTH_WARN = 4, norm2 = (s) => s.trim().toUpperCase(), AREA_ROOTS, NOT_AREAS, sameRepo = (a, b) => a.length === 0 || b.length === 0 || a.some((r) => b.includes(r)), DOT_COLORS, ROUTE_DRAFT_RULE = "Draft the route: PR-sized nodes, file-disjoint where they run in parallel, each with a title, the files or areas it touches, the repo, and the nodes it waits on (by key: A, B, C…). Prefer wide waves over chains deeper than four. Make the tough calls once, so each node's session follows them: an `approach` paragraph for the whole route, and on each node a `plan` — its approach, what it `provides` that later nodes rely on (names, shapes, files), what it `expects` from the nodes it waits on, and the `doneChecks` that say it landed. Show the graph and the plans as text and ask for a yes; file nothing before it. An item with children takes no nodes: its children are the work, and their order lives in their blockedBy. Then `route {q, approach, nodes: [{key, title, touches, repos, blockedBy, plan: {approach, provides, expects, doneChecks}}], confirm: true}`.";
2083
2090
  var init_route = __esm(() => {
2084
2091
  CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
2085
2092
  AREA_ROOTS = new Set(["apps", "packages", "services", "libs", "crates", "modules"]);
@@ -2151,6 +2158,7 @@ var init_annotations = __esm(() => {
2151
2158
  finish: add("Land the work"),
2152
2159
  buddy: add("Pair with a buddy"),
2153
2160
  pickup: add("Pick up a hand-off"),
2161
+ reopen: add("Reopen an item", true),
2154
2162
  update: change("Change an item", true),
2155
2163
  archive: change("Archive an item", true),
2156
2164
  withdraw: change("Withdraw a hand-off", true)
@@ -3223,6 +3231,27 @@ async function updateTool(ctx, args) {
3223
3231
  text: `${key} updated: ${changed.join(", ")}.${carried}${stale ? " The band was sized before this change — say so, and offer to re-size (estimate with the same source)." : ""}`
3224
3232
  };
3225
3233
  }
3234
+ async function reopenTool(ctx, args) {
3235
+ if (!ctx.client)
3236
+ return NOT_SET_UP;
3237
+ const project = str5(args.project)?.trim();
3238
+ if (!project)
3239
+ return { text: "Which item? Pass project: its key (QW-142).", isError: true };
3240
+ const reason = str5(args.reason)?.trim();
3241
+ if (!reason)
3242
+ return {
3243
+ text: "Say why it is reopened — pass reason, one line. It goes on the item's record.",
3244
+ isError: true
3245
+ };
3246
+ const res = await postSaying(ctx, "/proxy/reopen", { project, reason }, "Could not reopen the item.");
3247
+ if ("fail" in res)
3248
+ return res.fail;
3249
+ const key = str5(res.json.key) ?? project;
3250
+ const sled = str5(res.json.sled);
3251
+ return {
3252
+ text: `Reopened ${key} — ${sled ? `back on ${sled}'s sled` : "back to ready, on no sled"}. The false actual is cleared; the band stands.`
3253
+ };
3254
+ }
3226
3255
  function carriedLine(c) {
3227
3256
  if (!isObject4(c))
3228
3257
  return "";
@@ -3531,9 +3560,21 @@ async function itemTool(ctx, args) {
3531
3560
  text: `Nothing on the sled answers to "${q}". Try the key (QW-142) or a word from the title with status.`,
3532
3561
  isError: true
3533
3562
  };
3534
- const md = str5(r.markdown) ?? "";
3535
3563
  if (asFile)
3536
- return { text: md, structured: { key: str5(r.key) ?? q, export: true } };
3564
+ return { text: str5(r.markdown) ?? "", structured: { key: str5(r.key) ?? q, export: true } };
3565
+ const envs = isObject4(r.whereLive) && Array.isArray(r.whereLive.envs) ? r.whereLive.envs : [];
3566
+ const live = envs.filter(isObject4).filter((e) => {
3567
+ const s = isObject4(e.state) ? e.state : null;
3568
+ return !(s && (s.kind === "no_code" || s.state === "no_code"));
3569
+ }).map((e) => `- ${str5(e.name) ?? str5(e.slug) ?? "?"}: ${str5(e.words) ?? ""}`);
3570
+ const body = str5(r.markdown) ?? "";
3571
+ const section = live.length ? `## Where it's live
3572
+ ${live.join(`
3573
+ `)}
3574
+
3575
+ ` : "";
3576
+ const title = body.match(/^# [^\n]*\n+/)?.[0] ?? "";
3577
+ const md = `${title}${section}${body.slice(title.length)}`;
3537
3578
  const readiness = isObject4(r.readiness) ? r.readiness : null;
3538
3579
  const line = readiness ? str5(readiness.line) : undefined;
3539
3580
  const state = readiness ? str5(readiness.state) : undefined;
@@ -4274,6 +4315,8 @@ async function callTool(name, args, ctx) {
4274
4315
  return attachTool(ctx, args);
4275
4316
  case "update":
4276
4317
  return updateTool(ctx, args);
4318
+ case "reopen":
4319
+ return reopenTool(ctx, args);
4277
4320
  case "archive":
4278
4321
  return archiveTool(ctx, args);
4279
4322
  case "comment":
@@ -4679,7 +4722,7 @@ var init_server = __esm(() => {
4679
4722
  },
4680
4723
  {
4681
4724
  name: "route",
4682
- description: "The route — an item's internal order as PR-sized nodes with blockers (spec 26). With only `q`: the route as filed (each node's state, what it waits on, the ready set, where each run can be watched), or, when none is filed, the pack and the drafting rule so you draft one. Drafting: PR-sized nodes, file-disjoint where they run in parallel, each with a title, the files or areas it touches, the repo, and the nodes it waits on by key (A, B, C…); prefer wide waves over chains deeper than four. Make the tough calls once: an `approach` for the route and a `plan` on each node (its approach, what it provides to later nodes, what it expects from the nodes it waits on, its done-checks), so each node's session follows them instead of re-planning. Show the graph and the plans as text and ask for a yes; file nothing before it. Then call again with `nodes` and `confirm: true`. Re-drafting shows what changed; a node with a run or a PR is never removed. Ready is computed from landed blockers, never set. With nobody to ask (a drafting session an engine started, spec 26 §5.1): call with `nodes` and `propose: true` instead — the route is held on the item for a person's yes and nothing is filed; then stop. A read of an item with a proposal shows it as drafted, not filed.",
4725
+ description: "The route — an item's internal order as PR-sized nodes with blockers (spec 26). With only `q`: the route as filed (each node's state, what it waits on, the ready set, where each run can be watched), or, when none is filed, the pack and the drafting rule so you draft one. Drafting: PR-sized nodes, file-disjoint where they run in parallel, each with a title, the files or areas it touches, the repo, and the nodes it waits on by key (A, B, C…); prefer wide waves over chains deeper than four. Make the tough calls once: an `approach` for the route and a `plan` on each node (its approach, what it provides to later nodes, what it expects from the nodes it waits on, its done-checks), so each node's session follows them instead of re-planning. Show the graph and the plans as text and ask for a yes; file nothing before it. Then call again with `nodes` and `confirm: true`. An item with children is refused nodes — its children are the work: set their order with blockedBy on them. The item lands only when every child has landed. Re-drafting shows what changed; a node with a run or a PR is never removed. Ready is computed from landed blockers, never set. With nobody to ask (a drafting session an engine started, spec 26 §5.1): call with `nodes` and `propose: true` instead — the route is held on the item for a person's yes and nothing is filed; then stop. A read of an item with a proposal shows it as drafted, not filed.",
4683
4726
  inputSchema: {
4684
4727
  type: "object",
4685
4728
  properties: {
@@ -4967,6 +5010,21 @@ var init_server = __esm(() => {
4967
5010
  required: ["project"]
4968
5011
  }
4969
5012
  },
5013
+ {
5014
+ name: "reopen",
5015
+ description: "Put back an item that landed by mistake — a skipped node closed its route, or an actual was sent against the wrong item. It goes back on the sled it was on (or to ready, when that sled is gone), the false actual is cleared and the band stands. Pass the item's key and why: a reopen always says why, and the why goes on the item's record. The filer, an approver or an admin may. Only a landed item can be reopened.",
5016
+ inputSchema: {
5017
+ type: "object",
5018
+ properties: {
5019
+ project: { type: "string", description: "The item's key — QW-142 — or its project key." },
5020
+ reason: {
5021
+ type: "string",
5022
+ description: "Why it is reopened, one line — e.g. 'a skipped node landed it; six children are open'."
5023
+ }
5024
+ },
5025
+ required: ["project", "reason"]
5026
+ }
5027
+ },
4970
5028
  {
4971
5029
  name: "item",
4972
5030
  description: "Read one item whole, as one markdown document — the pack: everything an engineer or an agent needs to finish the work from this document alone. Eight parts: the ask, where the code is, how to run it (the repo's setup profile), how to know it is done (the gate), how to ship it (the playbook), what the org expects, what is known already (the Field Book — notes from earlier runs on this item, its feature and what landed beside it, the questions, the thread), and the rules of the road. Carries its readiness and whether it is packable. Pass the item's key (QW-142) or its title. Call this before building from an item, and before `check`.",
@@ -6416,7 +6474,54 @@ var init_pack2 = __esm(() => {
6416
6474
  });
6417
6475
 
6418
6476
  // ../../packages/shared/src/deploys/deploys.ts
6419
- var init_deploys = () => {};
6477
+ function parseDeployBody(json) {
6478
+ if (!json || typeof json !== "object" || Array.isArray(json))
6479
+ return bad("body must be a JSON object");
6480
+ const j = json;
6481
+ const repo = typeof j.repo === "string" ? j.repo.trim().toLowerCase() : "";
6482
+ if (!REPO_RE.test(repo))
6483
+ return bad("repo must be owner/name");
6484
+ const sha = typeof j.sha === "string" ? j.sha.trim().toLowerCase() : "";
6485
+ if (!SHA_RE.test(sha))
6486
+ return bad("sha must be 7–40 hex characters");
6487
+ const environment = typeof j.environment === "string" ? j.environment.trim() : "";
6488
+ if (!ENV_SLUG_RE.test(environment))
6489
+ return bad("environment must be a slug: lower-case letters, digits, - and _");
6490
+ const body = { repo, sha, environment };
6491
+ if (j.service !== undefined && j.service !== null) {
6492
+ const service = typeof j.service === "string" ? j.service.trim() : "";
6493
+ if (!SERVICE_RE.test(service))
6494
+ return bad("service must be a short name");
6495
+ body.service = service;
6496
+ }
6497
+ if (j.at !== undefined && j.at !== null) {
6498
+ const at = typeof j.at === "string" ? Date.parse(j.at) : typeof j.at === "number" ? j.at : Number.NaN;
6499
+ if (!Number.isFinite(at))
6500
+ return bad("at must be an ISO time");
6501
+ body.at = at;
6502
+ }
6503
+ if (j.url !== undefined && j.url !== null) {
6504
+ let ok = false;
6505
+ if (typeof j.url === "string") {
6506
+ try {
6507
+ ok = new URL(j.url).protocol === "https:";
6508
+ } catch {
6509
+ ok = false;
6510
+ }
6511
+ }
6512
+ if (!ok)
6513
+ return bad("url must be an https link");
6514
+ body.url = j.url;
6515
+ }
6516
+ return { ok: true, body };
6517
+ }
6518
+ var REPO_RE, SHA_RE, ENV_SLUG_RE, SERVICE_RE, bad = (error) => ({ ok: false, error });
6519
+ var init_deploys = __esm(() => {
6520
+ REPO_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
6521
+ SHA_RE = /^[0-9a-f]{7,40}$/;
6522
+ ENV_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,39}$/;
6523
+ SERVICE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/;
6524
+ });
6420
6525
 
6421
6526
  // ../../packages/shared/src/newrelic/newrelic.ts
6422
6527
  var init_newrelic = () => {};
@@ -6817,7 +6922,7 @@ function nodePrompt(o) {
6817
6922
  ...claim.pack.includes(PR_HEADER_OPEN) ? [
6818
6923
  `- 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.`
6819
6924
  ] : [],
6820
- "- Run the repo's own gate before you push. Never merge: a person (or the musher, when the attachment says so) does.",
6925
+ o.gate ? `- Run the repo's gate before you push: exactly \`${o.gate}\`, as given. Never wrap it — no \`$?\`, \`$(...)\`, \`bash -c\` or \`; echo\` around it — or it stops at a permission dialog; read its result from the exit and the output. Never merge: a person (or the musher, when the attachment says so) does.` : "- Run the repo's own gate before you push. Never merge: a person (or the musher, when the attachment says so) does.",
6821
6926
  "- Stay inside this node's scope. Work another node needs goes in the PR description, not in this branch.",
6822
6927
  `- End every comment and reply you post on the PR with the line \`${ENGINE_MARK}\`, so the engine knows it is yours and not a reviewer's.`,
6823
6928
  "- The approved brief and this route are the go-ahead: a person said yes to them before this session started. Build, commit, push and open the PR; a park is for a question the record does not settle, not for a go-ahead to build or push.",
@@ -9693,9 +9798,127 @@ var defer = async (o) => {
9693
9798
  return 0;
9694
9799
  };
9695
9800
 
9801
+ // src/commands/deploy.ts
9802
+ init_src();
9803
+ import { execFileSync as execFileSync2 } from "node:child_process";
9804
+ var DEPLOYS_URL = "https://hooks.sleds.ai/deploys";
9805
+ var USAGE = "usage: sleds deploy --env <slug> [--service <name>] [--sha <sha>] [--repo <owner/name>]";
9806
+ var FLAGS = ["env", "service", "sha", "repo"];
9807
+ var set = (x) => x?.trim() ? x.trim() : undefined;
9808
+ function repoFromRemote(remote) {
9809
+ const m = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?[^/:]+[:/](.+?)(?:\.git)?\/?$/i.exec(remote.trim());
9810
+ const parts = m?.[1]?.split("/").filter(Boolean) ?? [];
9811
+ if (parts.length < 2)
9812
+ return null;
9813
+ return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`.toLowerCase();
9814
+ }
9815
+ function resolveDeploy(argv, env, git) {
9816
+ const flags = {};
9817
+ for (let i = 0;i < argv.length; i++) {
9818
+ const a = argv[i] ?? "";
9819
+ const m = /^--([a-z]+)(?:=(.*))?$/.exec(a);
9820
+ const name = m?.[1];
9821
+ if (!m || !name || !FLAGS.includes(name))
9822
+ return { ok: false, error: `unknown argument ${a}` };
9823
+ const value = m[2] ?? argv[++i];
9824
+ if (!set(value))
9825
+ return { ok: false, error: `--${name} needs a value` };
9826
+ flags[name] = value?.trim();
9827
+ }
9828
+ if (!flags.env)
9829
+ return { ok: false, error: "--env <slug> is required" };
9830
+ const vercelRepo = set(env.VERCEL_GIT_REPO_OWNER) && set(env.VERCEL_GIT_REPO_SLUG) ? `${set(env.VERCEL_GIT_REPO_OWNER)}/${set(env.VERCEL_GIT_REPO_SLUG)}` : undefined;
9831
+ const remote = () => {
9832
+ const url = git(["remote", "get-url", "origin"]);
9833
+ return url && repoFromRemote(url) || undefined;
9834
+ };
9835
+ const repo = flags.repo ?? set(env.GITHUB_REPOSITORY) ?? vercelRepo ?? remote();
9836
+ if (!repo)
9837
+ return { ok: false, error: "no repo: pass --repo owner/name (no origin remote)" };
9838
+ const sha = flags.sha ?? set(env.SOURCE_VERSION) ?? set(env.HEROKU_SLUG_COMMIT) ?? set(env.VERCEL_GIT_COMMIT_SHA) ?? set(env.GITHUB_SHA) ?? set(git(["rev-parse", "HEAD"]) ?? undefined);
9839
+ if (!sha)
9840
+ return { ok: false, error: "no sha: pass --sha (not a git checkout)" };
9841
+ return parseDeployBody({
9842
+ repo,
9843
+ sha,
9844
+ environment: flags.env,
9845
+ ...flags.service ? { service: flags.service } : {}
9846
+ });
9847
+ }
9848
+ var defaultGit = (args) => {
9849
+ try {
9850
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
9851
+ } catch {
9852
+ return null;
9853
+ }
9854
+ };
9855
+ var defaultPost = async (url, token, body) => {
9856
+ try {
9857
+ const res = await fetch(url, {
9858
+ method: "POST",
9859
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
9860
+ body: JSON.stringify(body),
9861
+ signal: AbortSignal.timeout(15000)
9862
+ });
9863
+ let json = null;
9864
+ try {
9865
+ json = await res.json();
9866
+ } catch {}
9867
+ return { status: res.status, json };
9868
+ } catch (e) {
9869
+ return { error: e instanceof Error ? e.message : String(e) };
9870
+ }
9871
+ };
9872
+ function deployLine(body, json) {
9873
+ const where = `${body.repo}${body.service ? ` (${body.service})` : ""} → ${body.environment}`;
9874
+ if (typeof json.ignored === "string")
9875
+ return `ignored: ${json.ignored}`;
9876
+ if (json.duplicate)
9877
+ return `${where}: already at ${body.sha.slice(0, 7)}`;
9878
+ const prs = Array.isArray(json.newlyDeployed) ? json.newlyDeployed : [];
9879
+ const list = prs.map((p) => `#${p.number}`).join(", ");
9880
+ if (json.rollback)
9881
+ return `${where}: rollback to ${body.sha.slice(0, 7)}${list ? ` (carries ${list} again)` : ""}`;
9882
+ return prs.length ? `${where}: newly deployed ${list}` : `${where}: ${body.sha.slice(0, 7)} recorded, no newly deployed PRs`;
9883
+ }
9884
+ var deployCmd = async (o) => {
9885
+ const log = o.log ?? console.log;
9886
+ const error = o.error ?? console.error;
9887
+ const env = o.env ?? process.env;
9888
+ const token = set(env.SLEDS_DEPLOY_TOKEN);
9889
+ if (!token) {
9890
+ error("SLEDS_DEPLOY_TOKEN is not set — mint one in Settings › Deploys");
9891
+ return 2;
9892
+ }
9893
+ const resolved = resolveDeploy(o.argv, env, o.git ?? defaultGit);
9894
+ if (!resolved.ok) {
9895
+ error(resolved.error);
9896
+ error(USAGE);
9897
+ return 2;
9898
+ }
9899
+ const body = resolved.body;
9900
+ const url = set(env.SLEDS_DEPLOYS_URL) ?? DEPLOYS_URL;
9901
+ const r = await (o.post ?? defaultPost)(url, token, body);
9902
+ if ("error" in r) {
9903
+ error(`warning: SLEDS unreachable, deploy not recorded — ${r.error}`);
9904
+ return 0;
9905
+ }
9906
+ if (r.status === 200 && r.json) {
9907
+ log(deployLine(body, r.json));
9908
+ return 0;
9909
+ }
9910
+ const why = typeof r.json?.error === "string" ? r.json.error : `HTTP ${r.status}`;
9911
+ if (r.status >= 400 && r.status < 500) {
9912
+ error(r.status === 401 ? `SLEDS refused the deploy token (${why})` : why);
9913
+ return 1;
9914
+ }
9915
+ error(`warning: SLEDS answered ${why}, deploy not recorded`);
9916
+ return 0;
9917
+ };
9918
+
9696
9919
  // src/commands/doctor.ts
9697
9920
  init_src();
9698
- import { execFileSync as execFileSync3, spawnSync as spawnSync5 } from "node:child_process";
9921
+ import { execFileSync as execFileSync4, spawnSync as spawnSync5 } from "node:child_process";
9699
9922
  import { randomBytes } from "node:crypto";
9700
9923
  import {
9701
9924
  existsSync as existsSync17,
@@ -9710,7 +9933,7 @@ import { tmpdir } from "node:os";
9710
9933
  import { join as join20, resolve as resolve9 } from "node:path";
9711
9934
 
9712
9935
  // src/agentMcp.ts
9713
- import { execFileSync as execFileSync2 } from "node:child_process";
9936
+ import { execFileSync as execFileSync3 } from "node:child_process";
9714
9937
  import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
9715
9938
  import { dirname as dirname8, join as join10, resolve as resolve6 } from "node:path";
9716
9939
  init_settings();
@@ -9778,7 +10001,7 @@ function applyCodexMcp2(toml, home, env = process.env) {
9778
10001
  `;
9779
10002
  }
9780
10003
  var defaultRunClaude2 = (args) => {
9781
- execFileSync2("claude", args, { stdio: ["ignore", "ignore", "pipe"], timeout: 20000 });
10004
+ execFileSync3("claude", args, { stdio: ["ignore", "ignore", "pipe"], timeout: 20000 });
9782
10005
  };
9783
10006
  async function offerMcp2(o) {
9784
10007
  const env = o.env ?? process.env;
@@ -10233,6 +10456,7 @@ var agentShellCommand = (o) => {
10233
10456
  const effort = o.effort ? ` --effort ${shq2(o.effort)}` : "";
10234
10457
  return `${sandboxedAgent("claude", o.sandbox, o.mainCheckout)}${hookSettingsFlag(o)}${mcpConfigFlag(o)} --permission-mode ${shq2(o.permissionMode)}${model}${effort} ${prompt}`;
10235
10458
  };
10459
+ var execLine = (cmd) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(cmd) ? `exec env ${cmd}` : `exec ${cmd}`;
10236
10460
  var sandboxedAgent = (agent, sandbox, mainCheckout) => {
10237
10461
  const kind = sandboxAgentKind(agent);
10238
10462
  const line = kind === "claude" ? `${agent}${claudeAddDirFlag(mainCheckout)}` : agent;
@@ -10705,7 +10929,7 @@ var parseOrcaTerminal = (env) => {
10705
10929
  };
10706
10930
  var orcaLauncher = (o) => `#!/bin/sh
10707
10931
  # sleds engine: ${o.name}
10708
- exec ${agentShellCommand(o)}
10932
+ ${execLine(agentShellCommand(o))}
10709
10933
  `;
10710
10934
  var orcaCommand = (launcher) => `sh ${shq2(launcher)}; exit`;
10711
10935
  var GONE = /exit|clos|gone|dead|stopp|kill/i;
@@ -10907,7 +11131,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
10907
11131
  name: flags.name,
10908
11132
  os: host.os ?? process.platform,
10909
11133
  runner: flags.runner,
10910
- version: host.version ?? "0.1.44" ?? "dev",
11134
+ version: host.version ?? "0.1.45" ?? "dev",
10911
11135
  running: live.length,
10912
11136
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
10913
11137
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -11900,7 +12124,7 @@ function keysIn(states, state) {
11900
12124
  }
11901
12125
  function statesLanded(states) {
11902
12126
  const all = Object.values(states);
11903
- return all.length > 0 && all.every((s) => s === "landed" || s === "skipped");
12127
+ return all.length > 0 && all.every((s) => s === "landed" || s === "skipped") && all.some((s) => s === "landed");
11904
12128
  }
11905
12129
  function armedTerms(armed) {
11906
12130
  if (!isObject8(armed))
@@ -12865,6 +13089,37 @@ function packInstallOf(pack, repo) {
12865
13089
  }
12866
13090
  return first;
12867
13091
  }
13092
+ function packGateOf(pack, repo) {
13093
+ let inDone = false;
13094
+ let first;
13095
+ const want = repo?.trim().toLowerCase();
13096
+ for (const line of pack.split(`
13097
+ `)) {
13098
+ if (/^##\s/.test(line)) {
13099
+ inDone = /^##\s+4\./.test(line);
13100
+ continue;
13101
+ }
13102
+ if (!inDone)
13103
+ continue;
13104
+ const m = line.match(/^\s*-\s*([^:`]+?):\s*`(.+)`\s*$/);
13105
+ if (!m?.[1] || !m[2])
13106
+ continue;
13107
+ const name = m[1].trim().toLowerCase();
13108
+ if (name === "done means" || name.startsWith("try it"))
13109
+ continue;
13110
+ if (want && name === want)
13111
+ return m[2];
13112
+ first ??= m[2];
13113
+ }
13114
+ return first;
13115
+ }
13116
+ function gateAllowRules(gate) {
13117
+ const whole = gate?.trim();
13118
+ if (!whole)
13119
+ return [];
13120
+ const parts = whole.split(/&&|\|\||;/).map((p) => p.trim()).filter(Boolean);
13121
+ return [...new Set([whole, ...parts])].map((c) => `Bash(${c})`);
13122
+ }
12868
13123
  function ensureInstalled(exec, path, fs, o = {}) {
12869
13124
  if (!fs.exists(join15(path, "package.json")) || fs.exists(join15(path, "node_modules")))
12870
13125
  return null;
@@ -12957,17 +13212,17 @@ function fingerprint(text) {
12957
13212
  }
12958
13213
  return (h >>> 0).toString(16);
12959
13214
  }
12960
- function sandboxFor(d, name, cwd) {
13215
+ function sandboxFor(d, name, cwd, allow) {
12961
13216
  if (!d.sandbox)
12962
13217
  return {};
12963
13218
  if (!d.writeSandboxSettings)
12964
13219
  throw new Error("sessions are sandboxed, but nothing writes their settings — not started");
12965
- return { sandbox: d.writeSandboxSettings(name, cwd) };
13220
+ return { sandbox: d.writeSandboxSettings(name, cwd, allow) };
12966
13221
  }
12967
- function hooksFor(d, name) {
13222
+ function hooksFor(d, name, allow) {
12968
13223
  if (d.sandbox || !d.writeHookSettings)
12969
13224
  return {};
12970
- return { settingsFile: d.writeHookSettings(name) };
13225
+ return { settingsFile: d.writeHookSettings(name, allow) };
12971
13226
  }
12972
13227
  function mainCheckoutFor(d, cwd) {
12973
13228
  const main = d.mainCheckoutOf?.(cwd);
@@ -13425,6 +13680,8 @@ async function tickOnce(d) {
13425
13680
  const worktree = d.runner.makesWorktree ? repoPath : d.ensureWorktree(d.exec, repoPath, branch, worktreePath(repoPath, name), base, packInstallOf(claim.pack, claim.repos[0])) ?? worktreePath(repoPath, name);
13426
13681
  const prTitle = d.prTitleOf?.(claim);
13427
13682
  mcpConfig = engineMcpConfigFor(d, name, claim.runToken, at, worktree);
13683
+ const gate = packGateOf(claim.pack, claim.repos[0]);
13684
+ const allow = gateAllowRules(gate);
13428
13685
  const prompt = nodePrompt({
13429
13686
  claim,
13430
13687
  branch,
@@ -13433,6 +13690,7 @@ async function tickOnce(d) {
13433
13690
  sledsTools: d.source.name === "sleds",
13434
13691
  engineMcp: Boolean(mcpConfig),
13435
13692
  ...prTitle ? { prTitle } : {},
13693
+ ...gate ? { gate } : {},
13436
13694
  ...nodePlanContext(await briefsOf(claim.item), claim.node)
13437
13695
  });
13438
13696
  const promptFile = d.writePrompt(name, prompt);
@@ -13449,8 +13707,8 @@ async function tickOnce(d) {
13449
13707
  ...launch.effort && !launch.agent ? { effort: launch.effort } : {},
13450
13708
  ...launch.agent ? { agent: launch.agent } : {},
13451
13709
  ...mcpConfig ? { mcpConfig, ...d.strictMcp ? { strictMcp: true } : {} } : {},
13452
- ...sandboxFor(d, name, worktree),
13453
- ...hooksFor(d, name),
13710
+ ...sandboxFor(d, name, worktree, allow),
13711
+ ...hooksFor(d, name, allow),
13454
13712
  ...mainCheckoutFor(d, worktree),
13455
13713
  ...claim.group ? { group: claim.group } : {}
13456
13714
  };
@@ -13708,6 +13966,24 @@ function pressAnswer(d, runner, run, options, answer) {
13708
13966
  return false;
13709
13967
  }
13710
13968
  }
13969
+ function readFollowed(d, runner, run) {
13970
+ if (!runner.readsSession)
13971
+ return "";
13972
+ let shown = "";
13973
+ try {
13974
+ shown = runner.read(run.handle);
13975
+ } catch {
13976
+ shown = "";
13977
+ }
13978
+ const print = fingerprint(shown);
13979
+ if (run.seen !== print) {
13980
+ run.seen = print;
13981
+ run.seenAt = d.now();
13982
+ run.seenTicks = 0;
13983
+ } else
13984
+ run.seenTicks = (run.seenTicks ?? 0) + 1;
13985
+ return shown;
13986
+ }
13711
13987
  async function dropPermission(d, run, why) {
13712
13988
  const perms = permissionsOf(d.source);
13713
13989
  if (!run.permission || !perms || !run.runId)
@@ -14532,11 +14808,13 @@ async function follow(d, state, run, claim, runner, finished, attention, changes
14532
14808
  return true;
14533
14809
  await judgeLook(d, run, view);
14534
14810
  if (view.state === "MERGED") {
14811
+ await dropPermission(d, run, "the PR merged");
14535
14812
  const landed = await ripple(d, state, run, claim, runner, pr.url, finished, decided, view.head, view.mergeCommit);
14536
14813
  return !landed;
14537
14814
  }
14538
14815
  if (view.state === "CLOSED") {
14539
14816
  d.log(`${ref(run)}: ${pr.url} was closed without a merge — let go, its session stopped`);
14817
+ await dropPermission(d, run, "the PR closed without a merge");
14540
14818
  stopOpen(d, run, runner);
14541
14819
  return false;
14542
14820
  }
@@ -14556,6 +14834,8 @@ async function follow(d, state, run, claim, runner, finished, attention, changes
14556
14834
  kind: "review",
14557
14835
  what: `${view.feedback - pr.feedback} new comment${view.feedback - pr.feedback === 1 ? "" : "s"}`
14558
14836
  });
14837
+ if (await settlePermission(d, runner, run, readFollowed(d, runner, run)))
14838
+ return true;
14559
14839
  const tell = (text, what) => {
14560
14840
  let alive = false;
14561
14841
  try {
@@ -15650,7 +15930,7 @@ var headlessScript = (o, log) => {
15650
15930
  ...o.model ? [`--model ${shq2(o.model)}`] : [],
15651
15931
  ...o.effort ? [`--effort ${shq2(o.effort)}`] : []
15652
15932
  ].join(" ");
15653
- return `exec ${cmd} > ${shq2(log)} 2>&1`;
15933
+ return `${execLine(cmd)} > ${shq2(log)} 2>&1`;
15654
15934
  };
15655
15935
  var transcriptTail = (log) => {
15656
15936
  const lines = log.trimEnd().split(`
@@ -16567,7 +16847,7 @@ function engineDeps(o, flags, source, runner, stateName) {
16567
16847
  ...flags.strictMcp ? { strictMcp: true } : {},
16568
16848
  ...flags.sandbox ? {
16569
16849
  sandbox: true,
16570
- writeSandboxSettings: o.writeSandboxSettings ?? ((name, cwd) => {
16850
+ writeSandboxSettings: o.writeSandboxSettings ?? ((name, cwd, allow) => {
16571
16851
  const gitCommonDir = gitCommonDirOf(exec, cwd);
16572
16852
  const dir = join16(stateDir, "sandbox");
16573
16853
  mkdirSync12(dir, { recursive: true, mode: 448 });
@@ -16576,6 +16856,9 @@ function engineDeps(o, flags, source, runner, stateName) {
16576
16856
  ...SANDBOX_DEFAULT_ALLOW,
16577
16857
  ...flags.sandboxAllow ?? []
16578
16858
  ]);
16859
+ walls.permissions.allow = [
16860
+ ...new Set([...walls.permissions.allow, ...allow ?? []])
16861
+ ];
16579
16862
  const settings = withHook(name, { ...walls });
16580
16863
  writeFileSync13(p, `${JSON.stringify(settings, null, 2)}
16581
16864
  `, { mode: 384 });
@@ -16583,10 +16866,11 @@ function engineDeps(o, flags, source, runner, stateName) {
16583
16866
  return { settingsFile: p, gitCommonDir };
16584
16867
  })
16585
16868
  } : {},
16586
- writeHookSettings: (name) => {
16869
+ writeHookSettings: (name, allow) => {
16587
16870
  mkdirSync12(hooksDir, { recursive: true, mode: 448 });
16588
16871
  const p = join16(hooksDir, `${name}.json`);
16589
- writeFileSync13(p, `${JSON.stringify(withHook(name, {}), null, 2)}
16872
+ const base = allow?.length ? { permissions: { allow: [...allow] } } : {};
16873
+ writeFileSync13(p, `${JSON.stringify(withHook(name, base), null, 2)}
16590
16874
  `, { mode: 384 });
16591
16875
  chmodSync4(p, 384);
16592
16876
  return p;
@@ -16717,7 +17001,7 @@ async function driveEngine(o, flags, deps, until = {}) {
16717
17001
  runner: deps.runner.name,
16718
17002
  atOnce: flags.atOnce,
16719
17003
  interval: flags.interval,
16720
- cliVersion: "0.1.44"
17004
+ cliVersion: "0.1.45"
16721
17005
  };
16722
17006
  const beat = (checkedAt) => {
16723
17007
  const m = deps.machine;
@@ -17619,7 +17903,7 @@ function formatDoctor(checks) {
17619
17903
  }
17620
17904
  function defaultWhich2(cmd) {
17621
17905
  try {
17622
- return execFileSync3("which", ["-a", cmd], {
17906
+ return execFileSync4("which", ["-a", cmd], {
17623
17907
  encoding: "utf8",
17624
17908
  stdio: ["ignore", "pipe", "ignore"]
17625
17909
  }).split(`
@@ -18156,7 +18440,7 @@ function parsePmset(text) {
18156
18440
  }
18157
18441
  var defaultPmset = () => {
18158
18442
  try {
18159
- return parsePmset(execFileSync3("pmset", ["-g", "custom"], { encoding: "utf8", timeout: 2000 }));
18443
+ return parsePmset(execFileSync4("pmset", ["-g", "custom"], { encoding: "utf8", timeout: 2000 }));
18160
18444
  } catch {
18161
18445
  return null;
18162
18446
  }
@@ -18691,7 +18975,7 @@ function engineDeps2(o, flags, source, runner, stateName) {
18691
18975
  ...flags.strictMcp ? { strictMcp: true } : {},
18692
18976
  ...flags.sandbox ? {
18693
18977
  sandbox: true,
18694
- writeSandboxSettings: o.writeSandboxSettings ?? ((name, cwd) => {
18978
+ writeSandboxSettings: o.writeSandboxSettings ?? ((name, cwd, allow) => {
18695
18979
  const gitCommonDir = gitCommonDirOf2(exec, cwd);
18696
18980
  const dir = join21(stateDir, "sandbox");
18697
18981
  mkdirSync16(dir, { recursive: true, mode: 448 });
@@ -18700,6 +18984,9 @@ function engineDeps2(o, flags, source, runner, stateName) {
18700
18984
  ...SANDBOX_DEFAULT_ALLOW,
18701
18985
  ...flags.sandboxAllow ?? []
18702
18986
  ]);
18987
+ walls.permissions.allow = [
18988
+ ...new Set([...walls.permissions.allow, ...allow ?? []])
18989
+ ];
18703
18990
  const settings = withHook(name, { ...walls });
18704
18991
  writeFileSync18(p, `${JSON.stringify(settings, null, 2)}
18705
18992
  `, { mode: 384 });
@@ -18707,10 +18994,11 @@ function engineDeps2(o, flags, source, runner, stateName) {
18707
18994
  return { settingsFile: p, gitCommonDir };
18708
18995
  })
18709
18996
  } : {},
18710
- writeHookSettings: (name) => {
18997
+ writeHookSettings: (name, allow) => {
18711
18998
  mkdirSync16(hooksDir, { recursive: true, mode: 448 });
18712
18999
  const p = join21(hooksDir, `${name}.json`);
18713
- writeFileSync18(p, `${JSON.stringify(withHook(name, {}), null, 2)}
19000
+ const base = allow?.length ? { permissions: { allow: [...allow] } } : {};
19001
+ writeFileSync18(p, `${JSON.stringify(withHook(name, base), null, 2)}
18714
19002
  `, { mode: 384 });
18715
19003
  chmodSync5(p, 384);
18716
19004
  return p;
@@ -18850,7 +19138,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
18850
19138
  runner: deps.runner.name,
18851
19139
  atOnce: flags.atOnce,
18852
19140
  interval: flags.interval,
18853
- cliVersion: "0.1.44"
19141
+ cliVersion: "0.1.45"
18854
19142
  };
18855
19143
  const beat = (checkedAt) => {
18856
19144
  const m = deps.machine;
@@ -22498,12 +22786,81 @@ var promptEvent = async (o) => {
22498
22786
  return code;
22499
22787
  };
22500
22788
 
22789
+ // src/commands/reopen.ts
22790
+ var REOPEN_USAGE = 'usage: sleds reopen <key> --reason "<why it is reopened>" [--json]';
22791
+ function parseReopenArgs(argv) {
22792
+ let key;
22793
+ let reason = "";
22794
+ let json = false;
22795
+ for (let i = 0;i < argv.length; i++) {
22796
+ const a = argv[i];
22797
+ if (a === "--json")
22798
+ json = true;
22799
+ else if (a === "--reason") {
22800
+ const value = argv[i + 1];
22801
+ if (value === undefined || value.startsWith("--"))
22802
+ return { error: "--reason needs a value" };
22803
+ reason = value;
22804
+ i++;
22805
+ } else if (a.startsWith("--"))
22806
+ return { error: `unknown flag ${a}` };
22807
+ else if (key === undefined)
22808
+ key = a;
22809
+ else
22810
+ return { error: `one item at a time — "${a}" is extra` };
22811
+ }
22812
+ if (!key)
22813
+ return { error: "which item?" };
22814
+ return { key, reason: reason.trim(), json };
22815
+ }
22816
+ function reopenedLine(key, j) {
22817
+ const shown = typeof j.key === "string" ? j.key : key;
22818
+ return typeof j.sled === "string" && j.sled ? `${shown} reopened — back on ${j.sled}'s sled` : `${shown} reopened — back to ready, on no sled`;
22819
+ }
22820
+ async function reopenCmd(o) {
22821
+ const log = o.log ?? console.log;
22822
+ const error = o.error ?? console.error;
22823
+ const a = parseReopenArgs(o.argv);
22824
+ if ("error" in a) {
22825
+ error(a.error);
22826
+ error(REOPEN_USAGE);
22827
+ return 2;
22828
+ }
22829
+ if (!a.reason) {
22830
+ error(`${a.key}: say why it is reopened — it goes on the item's thread`);
22831
+ error(REOPEN_USAGE);
22832
+ return 1;
22833
+ }
22834
+ const client = engineClient({ home: o.home, ...o.client ? { client: o.client } : {} });
22835
+ if ("error" in client) {
22836
+ error(client.error);
22837
+ return 1;
22838
+ }
22839
+ const r = await client.postFull("/proxy/reopen", { project: a.key, reason: a.reason });
22840
+ if (!r) {
22841
+ error(`${a.key}: SLEDS unreachable — nothing reopened`);
22842
+ return 1;
22843
+ }
22844
+ const ok = r.status < 300 && r.json?.ok !== false;
22845
+ if (a.json)
22846
+ log(JSON.stringify(r.json ?? { ok: false, error: `HTTP ${r.status}` }));
22847
+ if (!ok) {
22848
+ const why = typeof r.json?.error === "string" ? r.json.error : `HTTP ${r.status}`;
22849
+ if (!a.json)
22850
+ error(r.status === 404 && !r.json ? `${a.key}: SLEDS does not know this seat — run \`sleds init\`` : `${a.key}: ${why}`);
22851
+ return 1;
22852
+ }
22853
+ if (!a.json)
22854
+ log(reopenedLine(a.key, r.json ?? {}));
22855
+ return 0;
22856
+ }
22857
+
22501
22858
  // src/commands/route.ts
22502
22859
  init_src();
22503
22860
  import { readFileSync as readFileSync27 } from "node:fs";
22504
22861
  import { hostname as hostname12 } from "node:os";
22505
22862
  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>]";
22506
- var USAGE = "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]";
22863
+ 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]";
22507
22864
  var ANSWER_USAGE = "usage: sleds route answer <key>/<node> <answer…> | sleds route answer <key>/<node> --stdin [--json]";
22508
22865
  var PROPOSE_USAGE = "usage: sleds route propose <key> (--file <nodes.json> | --stdin) [--name <engine>] [--json]";
22509
22866
  var APPROVE_USAGE = "usage: sleds route approve <key> [--file <nodes.json>] [--json]";
@@ -23058,14 +23415,14 @@ var routeCmd = async (o) => {
23058
23415
  const parsed = parseEngineArgs(o.argv.filter((a) => a !== "--dot" && a !== "--long" && a !== "--json"), o.host ?? hostname12());
23059
23416
  if ("error" in parsed) {
23060
23417
  error(parsed.error);
23061
- error(USAGE);
23418
+ error(USAGE2);
23062
23419
  return 2;
23063
23420
  }
23064
23421
  const { positional } = parsed;
23065
23422
  const isRun = positional[0] === "run";
23066
23423
  const key = isRun ? positional[1] : positional[0];
23067
23424
  if (!key) {
23068
- error(USAGE);
23425
+ error(USAGE2);
23069
23426
  return 2;
23070
23427
  }
23071
23428
  if (asJson) {
@@ -23321,7 +23678,7 @@ var seatEvent = async (o) => {
23321
23678
  };
23322
23679
 
23323
23680
  // src/commands/seatSync.ts
23324
- import { execFileSync as execFileSync4 } from "node:child_process";
23681
+ import { execFileSync as execFileSync5 } from "node:child_process";
23325
23682
  import { existsSync as existsSync26, mkdirSync as mkdirSync22, readFileSync as readFileSync28, readdirSync as readdirSync5, rmSync as rmSync16, writeFileSync as writeFileSync25 } from "node:fs";
23326
23683
  import { platform as platform2 } from "node:os";
23327
23684
  import { dirname as dirname17, join as join26 } from "node:path";
@@ -23689,7 +24046,7 @@ async function readCookieRowsBun(home, source) {
23689
24046
  }
23690
24047
  function keychainKey(service) {
23691
24048
  try {
23692
- return execFileSync4("security", ["find-generic-password", "-w", "-s", service], {
24049
+ return execFileSync5("security", ["find-generic-password", "-w", "-s", service], {
23693
24050
  encoding: "utf8"
23694
24051
  }).trim();
23695
24052
  } catch {
@@ -23701,7 +24058,7 @@ function decryptJar(rows, safeStoragePw) {
23701
24058
  }
23702
24059
  var curlUsage = (org, cookie) => {
23703
24060
  try {
23704
- const out = execFileSync4("curl", usageCurlArgs(), {
24061
+ const out = execFileSync5("curl", usageCurlArgs(), {
23705
24062
  input: usageCurlConfig(org, cookie),
23706
24063
  encoding: "utf8",
23707
24064
  maxBuffer: 8000000
@@ -23766,16 +24123,16 @@ function install(home, argv, source, log) {
23766
24123
  }
23767
24124
  writeFileSync25(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
23768
24125
  try {
23769
- execFileSync4("launchctl", ["unload", path], { stdio: "ignore" });
24126
+ execFileSync5("launchctl", ["unload", path], { stdio: "ignore" });
23770
24127
  } catch {}
23771
- execFileSync4("launchctl", ["load", "-w", path]);
24128
+ execFileSync5("launchctl", ["load", "-w", path]);
23772
24129
  log(`seat-sync timer installed (every ${SYNC_INTERVAL_S / 60} min) → ${path}`);
23773
24130
  return 0;
23774
24131
  }
23775
24132
  function uninstall(home, source, log) {
23776
24133
  const path = plistPath2(home, source);
23777
24134
  try {
23778
- execFileSync4("launchctl", ["unload", "-w", path], { stdio: "ignore" });
24135
+ execFileSync5("launchctl", ["unload", "-w", path], { stdio: "ignore" });
23779
24136
  } catch {}
23780
24137
  rmSync16(seatTokenPath(home, source), { force: true });
23781
24138
  log(existsSync26(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
@@ -24647,7 +25004,7 @@ var verify = async (o) => {
24647
25004
  };
24648
25005
 
24649
25006
  // src/commands/webhooks.ts
24650
- var USAGE2 = "usage: sleds webhooks [--json] | sleds webhooks deliveries <id or name> [--json]";
25007
+ var USAGE3 = "usage: sleds webhooks [--json] | sleds webhooks deliveries <id or name> [--json]";
24651
25008
  var str14 = (x) => typeof x === "string" ? x : "";
24652
25009
  function ago3(at, now) {
24653
25010
  const m = Math.max(0, Math.floor((now - at) / 60000));
@@ -24698,7 +25055,7 @@ var webhooksCmd = async (o) => {
24698
25055
  const now = (o.now ?? Date.now)();
24699
25056
  const sub = words[0];
24700
25057
  if (sub && (sub !== "deliveries" || words.length !== 2)) {
24701
- error(USAGE2);
25058
+ error(USAGE3);
24702
25059
  return 2;
24703
25060
  }
24704
25061
  const path = sub === "deliveries" ? `/proxy/webhooks/deliveries?hook=${encodeURIComponent(words[1] ?? "")}` : "/proxy/webhooks";
@@ -24958,7 +25315,7 @@ var isSledsStopHook2 = isSledsHookFor2("stop-event");
24958
25315
  var isSledsPermissionHook2 = isSledsHookFor2("permission-event");
24959
25316
 
24960
25317
  // src/index.ts
24961
- var USAGE3 = `usage: sleds <command> [options]
25318
+ var USAGE4 = `usage: sleds <command> [options]
24962
25319
 
24963
25320
  commands:
24964
25321
  init Sign in through the browser, make this machine a seat, install the /sled skill
@@ -25013,6 +25370,10 @@ commands:
25013
25370
  --run start Claude Code with the pack as the first prompt
25014
25371
  webhooks The org's outbound webhooks (admins); deliveries <id or name> lists one hook's
25015
25372
  last hundred deliveries. Registering and Redeliver stay in the app.
25373
+ deploy A release step's one line: tell SLEDS this commit is now running in an environment.
25374
+ --env <slug> the environment (required); --service <name> for a monorepo
25375
+ --sha / --repo else the CI's own variables, then git
25376
+ Token from SLEDS_DEPLOY_TOKEN (Settings \u203A Deploys). SLEDS down never fails a release.
25016
25377
  sled --json What is on your sled (mine with each route, proposed, sent), for a front end.
25017
25378
  sleds sled top KEY put KEY first on your sled; the engine picks it next
25018
25379
  route <key> Print an item's route: its nodes, what each waits on, what is ready, where each
@@ -25040,6 +25401,9 @@ commands:
25040
25401
  --mode <permission-mode> Claude Code's (default auto; acceptEdits stops an unattended
25041
25402
  session at git push; an account without auto passes acceptEdits)
25042
25403
  --model <id> Claude Code's model
25404
+ reopen <key> Put back an item that landed by mistake (a skipped node closed its route): its
25405
+ landing is cleared and it goes back on the sled it came off, or to ready.
25406
+ --reason "<text>" why \u2014 required; it goes on the item's thread (--json)
25043
25407
  engine The long-running loop over the routes Run armed on your sled: takes each ready
25044
25408
  node, starts a session on its own branch, follows its PR to the merge (CI failures
25045
25409
  and review feedback go back to the session; a merge lands the node, closes the
@@ -25161,11 +25525,11 @@ var wantsHelp = (argv) => {
25161
25525
  return false;
25162
25526
  };
25163
25527
  var commandUsage = (cmd) => {
25164
- const lines = USAGE3.split(`
25528
+ const lines = USAGE4.split(`
25165
25529
  `);
25166
25530
  const start = lines.findIndex((l) => l === ` ${cmd}` || l.startsWith(` ${cmd} `));
25167
25531
  if (start === -1)
25168
- return USAGE3;
25532
+ return USAGE4;
25169
25533
  const section = [lines[start]];
25170
25534
  for (const l of lines.slice(start + 1)) {
25171
25535
  if (!/^ {3,}\S/.test(l))
@@ -25221,8 +25585,12 @@ var main = async (argv) => {
25221
25585
  return packCmd({ home, argv: argv.slice(1) });
25222
25586
  case "webhooks":
25223
25587
  return webhooksCmd({ home, argv: argv.slice(1) });
25588
+ case "deploy":
25589
+ return deployCmd({ argv: argv.slice(1) });
25224
25590
  case "sled":
25225
25591
  return sledCmd({ home, argv: argv.slice(1) });
25592
+ case "reopen":
25593
+ return reopenCmd({ home, argv: argv.slice(1) });
25226
25594
  case "engine":
25227
25595
  case "route": {
25228
25596
  if (cmd === "engine" && argv[1] === "status")
@@ -25415,19 +25783,19 @@ var main = async (argv) => {
25415
25783
  case "help":
25416
25784
  case "--help":
25417
25785
  case "-h":
25418
- console.log(USAGE3);
25786
+ console.log(USAGE4);
25419
25787
  console.log(`
25420
25788
  ${renderHelpText(argv[1])}`);
25421
25789
  return 0;
25422
25790
  case "version":
25423
25791
  case "--version":
25424
25792
  case "-v":
25425
- console.log("sleds 0.1.44");
25793
+ console.log("sleds 0.1.45");
25426
25794
  return 0;
25427
25795
  default:
25428
25796
  console.error(`unknown command: ${cmd}
25429
25797
  `);
25430
- console.error(USAGE3);
25798
+ console.error(USAGE4);
25431
25799
  return 1;
25432
25800
  }
25433
25801
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
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",