@sleds/cli 0.1.22 → 0.1.23

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 +527 -140
  2. package/package.json +1 -1
package/dist/sleds.mjs CHANGED
@@ -305,7 +305,26 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
305
305
  if (!list.some(isSledsPromptHook))
306
306
  return { ...settings };
307
307
  return withEventHooks(settings, PROMPT_EVENT, list.filter((e) => !isSledsPromptHook(e)));
308
- }, hasPromptHook = (settings) => eventHooks(settings, PROMPT_EVENT).some(isSledsPromptHook), isSeatToken = (token) => /^sled_[A-Za-z0-9_-]+$/.test(token), maskToken = (token) => token.length <= 9 ? `${token.slice(0, 5)}…` : `${token.slice(0, 9)}…`, serialize = (settings) => `${JSON.stringify(settings, null, 2)}
308
+ }, hasPromptHook = (settings) => eventHooks(settings, PROMPT_EVENT).some(isSledsPromptHook), SESSION_EVENT = "SessionStart", SESSION_EVENT_TIMEOUT_S = 3, STOP_EVENT = "Stop", STOP_EVENT_TIMEOUT_S = 3, isSledsHookFor = (sub) => (entry) => {
309
+ if (!isObject5(entry) || !Array.isArray(entry.hooks))
310
+ return false;
311
+ const re = new RegExp(`\\b${sub}\\b`);
312
+ return entry.hooks.some((h) => isObject5(h) && h.type === "command" && typeof h.command === "string" && re.test(h.command));
313
+ }, isSledsSessionHook, isSledsStopHook, sessionHookEntry = (command) => ({
314
+ hooks: [{ type: "command", command, timeout: SESSION_EVENT_TIMEOUT_S }]
315
+ }), stopHookEntry = (command) => ({
316
+ hooks: [{ type: "command", command, timeout: STOP_EVENT_TIMEOUT_S }]
317
+ }), applyOnce = (settings, event, ours, entry) => {
318
+ const list = eventHooks(settings, event);
319
+ const idx = list.findIndex(ours);
320
+ const next = idx === -1 ? [...list, entry] : list.map((e, i) => i === idx ? entry : e);
321
+ return withEventHooks(settings, event, next);
322
+ }, removeOurs = (settings, event, ours) => {
323
+ const list = eventHooks(settings, event);
324
+ if (!list.some(ours))
325
+ return { ...settings };
326
+ return withEventHooks(settings, event, list.filter((e) => !ours(e)));
327
+ }, applySessionHook = (settings, command) => applyOnce(settings, SESSION_EVENT, isSledsSessionHook, sessionHookEntry(command)), removeSessionHook = (settings) => removeOurs(settings, SESSION_EVENT, isSledsSessionHook), hasSessionHook = (settings) => eventHooks(settings, SESSION_EVENT).some(isSledsSessionHook), applyStopHook = (settings, command) => applyOnce(settings, STOP_EVENT, isSledsStopHook, stopHookEntry(command)), removeStopHook = (settings) => removeOurs(settings, STOP_EVENT, isSledsStopHook), hasStopHook = (settings) => eventHooks(settings, STOP_EVENT).some(isSledsStopHook), isSeatToken = (token) => /^sled_[A-Za-z0-9_-]+$/.test(token), maskToken = (token) => token.length <= 9 ? `${token.slice(0, 5)}…` : `${token.slice(0, 9)}…`, serialize = (settings) => `${JSON.stringify(settings, null, 2)}
309
328
  `, hasClaudeCode = (home) => existsSync(claudeDir(home)), readSettings = (home) => {
310
329
  const p2 = settingsPath(home);
311
330
  if (!existsSync(p2))
@@ -338,7 +357,10 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
338
357
  writeFileSync(tokenPath(home), `${token}
339
358
  `, { mode: 384 });
340
359
  };
341
- var init_settings = () => {};
360
+ var init_settings = __esm(() => {
361
+ isSledsSessionHook = isSledsHookFor("session-event");
362
+ isSledsStopHook = isSledsHookFor("stop-event");
363
+ });
342
364
 
343
365
  // src/config.ts
344
366
  import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -565,17 +587,17 @@ __export(exports_seat, {
565
587
  });
566
588
  import {
567
589
  closeSync as closeSync2,
568
- existsSync as existsSync16,
569
- mkdirSync as mkdirSync13,
590
+ existsSync as existsSync17,
591
+ mkdirSync as mkdirSync14,
570
592
  openSync as openSync2,
571
- readFileSync as readFileSync13,
593
+ readFileSync as readFileSync14,
572
594
  readSync as readSync2,
573
- renameSync as renameSync6,
595
+ renameSync as renameSync7,
574
596
  statSync as statSync3,
575
- writeFileSync as writeFileSync13
597
+ writeFileSync as writeFileSync14
576
598
  } from "node:fs";
577
- import { join as join14 } from "node:path";
578
- var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject13 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num7 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
599
+ import { join as join15 } from "node:path";
600
+ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject14 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num7 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
579
601
  const n = num7(v);
580
602
  if (n !== undefined) {
581
603
  if (n <= 0)
@@ -588,7 +610,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
588
610
  }
589
611
  return;
590
612
  }, parseBar2 = (v) => {
591
- if (!isObject13(v))
613
+ if (!isObject14(v))
592
614
  return;
593
615
  const usedPct = num7(v.used_percentage) ?? num7(v.usedPct);
594
616
  const resetsAt = parseResetsAt2(v.resets_at ?? v.resetsAt);
@@ -596,14 +618,14 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
596
618
  return;
597
619
  return { usedPct, resetsAt };
598
620
  }, parseSeatWindow2 = (raw, capturedAt) => {
599
- if (!isObject13(raw))
621
+ if (!isObject14(raw))
600
622
  return null;
601
- const limits = isObject13(raw.rate_limits) ? raw.rate_limits : raw;
623
+ const limits = isObject14(raw.rate_limits) ? raw.rate_limits : raw;
602
624
  const fiveHour = parseBar2(limits.five_hour ?? limits.fiveHour);
603
625
  const sevenDay = parseBar2(limits.seven_day ?? limits.sevenDay);
604
626
  const spendLimit = parseBar2(limits.spend_limit ?? limits.spendLimit);
605
627
  const models = {};
606
- if (isObject13(limits.models))
628
+ if (isObject14(limits.models))
607
629
  for (const [k, v] of Object.entries(limits.models)) {
608
630
  const bar = parseBar2(v);
609
631
  if (bar)
@@ -658,15 +680,15 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
658
680
  return `⚠ seat ${hot[0]} ${verb}${SEP}resets ${reset}${SEP}/sled handoff · sleds defer`;
659
681
  }
660
682
  return `seat ${parts.join(SEP)}`;
661
- }, APPROACHING_PCT2 = 90, seatStatePath2 = (home) => join14(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
662
- if (!isObject13(v))
683
+ }, APPROACHING_PCT2 = 90, seatStatePath2 = (home) => join15(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
684
+ if (!isObject14(v))
663
685
  return;
664
686
  const capturedAt = num7(v.capturedAt);
665
687
  if (capturedAt === undefined)
666
688
  return;
667
689
  return parseSeatWindow2(v, capturedAt) ?? undefined;
668
690
  }, parseSeatState2 = (raw) => {
669
- if (!isObject13(raw))
691
+ if (!isObject14(raw))
670
692
  return {};
671
693
  const out = {};
672
694
  const window = parseWindowStored2(raw.window);
@@ -675,7 +697,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
675
697
  const lastPostedAt = num7(raw.lastPostedAt);
676
698
  if (lastPostedAt !== undefined)
677
699
  out.lastPostedAt = lastPostedAt;
678
- if (isObject13(raw.lastPosted)) {
700
+ if (isObject14(raw.lastPosted)) {
679
701
  const lp = {};
680
702
  const f = num7(raw.lastPosted.fiveHour);
681
703
  const s = num7(raw.lastPosted.sevenDay);
@@ -691,11 +713,11 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
691
713
  const lastWallResetsAt = num7(raw.lastWallResetsAt);
692
714
  if (lastWallResetsAt !== undefined)
693
715
  out.lastWallResetsAt = lastWallResetsAt;
694
- if (isObject13(raw.warned)) {
716
+ if (isObject14(raw.warned)) {
695
717
  const warned = {};
696
718
  for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
697
719
  const w = raw.warned[bar];
698
- if (!isObject13(w))
720
+ if (!isObject14(w))
699
721
  continue;
700
722
  const entry = {};
701
723
  const ap = num7(w.approaching);
@@ -708,11 +730,11 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
708
730
  }
709
731
  out.warned = warned;
710
732
  }
711
- if (isObject13(raw.told)) {
733
+ if (isObject14(raw.told)) {
712
734
  const told = {};
713
735
  for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
714
736
  const w = raw.told[bar];
715
- if (!isObject13(w))
737
+ if (!isObject14(w))
716
738
  continue;
717
739
  const entry = {};
718
740
  const ap = num7(w.approaching);
@@ -725,7 +747,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
725
747
  }
726
748
  out.told = told;
727
749
  }
728
- if (isObject13(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
750
+ if (isObject14(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
729
751
  const offset = num7(raw.transcriptScan.offset);
730
752
  const hitAt = num7(raw.transcriptScan.hitAt);
731
753
  out.transcriptScan = {
@@ -737,9 +759,9 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
737
759
  const session = parseSeatSession2(raw.session);
738
760
  if (session)
739
761
  out.session = session;
740
- if (isObject13(raw.cockpit) && raw.cockpit.kind === "cmux") {
762
+ if (isObject14(raw.cockpit) && raw.cockpit.kind === "cmux") {
741
763
  const c = { kind: "cmux" };
742
- if (isObject13(raw.cockpit.lines)) {
764
+ if (isObject14(raw.cockpit.lines)) {
743
765
  const lines = {};
744
766
  for (const [k, v] of Object.entries(raw.cockpit.lines))
745
767
  if (typeof v === "string")
@@ -749,9 +771,9 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
749
771
  if (raw.cockpit.progress === true)
750
772
  c.progress = true;
751
773
  const f = raw.cockpit.feed;
752
- if (isObject13(f) && num7(f.at) !== undefined) {
774
+ if (isObject14(f) && num7(f.at) !== undefined) {
753
775
  const strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
754
- const j = isObject13(f.jobs) ? f.jobs : {};
776
+ const j = isObject14(f.jobs) ? f.jobs : {};
755
777
  c.feed = {
756
778
  at: num7(f.at),
757
779
  handoffs: strs(f.handoffs),
@@ -767,7 +789,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
767
789
  }
768
790
  out.cockpit = c;
769
791
  }
770
- if (isObject13(raw.estimate)) {
792
+ if (isObject14(raw.estimate)) {
771
793
  const e = raw.estimate;
772
794
  const project = typeof e.project === "string" ? e.project : undefined;
773
795
  const [lo, hi, lm, hm, at] = [e.lowUsd, e.highUsd, e.lowMin, e.highMin, e.at].map(num7);
@@ -776,7 +798,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
776
798
  }
777
799
  return out;
778
800
  }, parseSeatSession2 = (v) => {
779
- if (!isObject13(v) || typeof v.id !== "string" || !v.id)
801
+ if (!isObject14(v) || typeof v.id !== "string" || !v.id)
780
802
  return;
781
803
  const at = num7(v.at);
782
804
  if (at === undefined)
@@ -793,12 +815,12 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
793
815
  return null;
794
816
  try {
795
817
  const raw = JSON.parse(trimmed);
796
- if (!isObject13(raw) || typeof raw.session_id !== "string" || !raw.session_id)
818
+ if (!isObject14(raw) || typeof raw.session_id !== "string" || !raw.session_id)
797
819
  return null;
798
820
  const out = { id: raw.session_id, at: now };
799
821
  if (typeof raw.cwd === "string" && raw.cwd)
800
822
  out.cwd = raw.cwd;
801
- const model = isObject13(raw.model) ? raw.model.id : raw.model;
823
+ const model = isObject14(raw.model) ? raw.model.id : raw.model;
802
824
  if (typeof model === "string" && model)
803
825
  out.model = model;
804
826
  return out;
@@ -819,21 +841,21 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
819
841
  return pick.length ? Math.min(...pick) : undefined;
820
842
  }, readSeatState2 = (home) => {
821
843
  const p2 = seatStatePath2(home);
822
- if (!existsSync16(p2))
844
+ if (!existsSync17(p2))
823
845
  return {};
824
846
  try {
825
- return parseSeatState2(JSON.parse(readFileSync13(p2, "utf8")));
847
+ return parseSeatState2(JSON.parse(readFileSync14(p2, "utf8")));
826
848
  } catch {
827
849
  return {};
828
850
  }
829
851
  }, writeSeatState2 = (home, state) => {
830
852
  const dir = configDir(home);
831
- mkdirSync13(dir, { recursive: true, mode: 448 });
853
+ mkdirSync14(dir, { recursive: true, mode: 448 });
832
854
  const target = seatStatePath2(home);
833
855
  const tmp = `${target}.${process.pid}.tmp`;
834
- writeFileSync13(tmp, `${JSON.stringify(state)}
856
+ writeFileSync14(tmp, `${JSON.stringify(state)}
835
857
  `, { mode: 384 });
836
- renameSync6(tmp, target);
858
+ renameSync7(tmp, target);
837
859
  }, pctOf2 = (b) => b?.usedPct, shouldPostSeat2 = (state, next, now) => {
838
860
  if (state.lastPostedAt === undefined || !state.lastPosted)
839
861
  return true;
@@ -900,7 +922,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
900
922
  }, CREDITS_PATTERN2, creditsHitIn2 = (text) => CREDITS_PATTERN2.test(text), transcriptPathFromStdin2 = (text) => {
901
923
  try {
902
924
  const raw = JSON.parse(text.trim());
903
- return isObject13(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
925
+ return isObject14(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
904
926
  } catch {
905
927
  return null;
906
928
  }
@@ -935,6 +957,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
935
957
  }
936
958
  }, seatWindowBody2 = (w) => ({
937
959
  capturedAt: w.capturedAt,
960
+ source: "claude",
938
961
  ...w.fiveHour ? { fiveHour: w.fiveHour } : {},
939
962
  ...w.sevenDay ? { sevenDay: w.sevenDay } : {},
940
963
  ...w.spendLimit ? { spendLimit: w.spendLimit } : {}
@@ -951,7 +974,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
951
974
  } catch {
952
975
  return false;
953
976
  }
954
- }, postSeatWindow2 = (o2, w) => postJson2(o2, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o2, input) => postJson2(o2, "/proxy/seat-wall", input), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.", SEAT_WALL_LINE_NO_KEY2 = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.", seatWallLine2 = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE2 : SEAT_WALL_LINE_NO_KEY2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve6) => {
977
+ }, postSeatWindow2 = (o2, w) => postJson2(o2, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o2, input) => postJson2(o2, "/proxy/seat-wall", { source: "claude", ...input }), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.", SEAT_WALL_LINE_NO_KEY2 = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.", seatWallLine2 = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE2 : SEAT_WALL_LINE_NO_KEY2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve6) => {
955
978
  if (stdin.isTTY) {
956
979
  resolve6("");
957
980
  return;
@@ -1178,7 +1201,7 @@ var GUIDES = [
1178
1201
  },
1179
1202
  {
1180
1203
  do: "Mint a seat for each person",
1181
- where: "Settings › Seats & tokens › `Mint`",
1204
+ where: "Settings › People › `Mint a seat` under the person",
1182
1205
  note: "A seat is one person's token at the proxy. It shows once; hand it to the person, never paste it in chat."
1183
1206
  },
1184
1207
  {
@@ -1259,7 +1282,7 @@ var GUIDES = [
1259
1282
  {
1260
1283
  do: "To keep every PR in sync: add the ledger workflow to each repo",
1261
1284
  where: "`.github/workflows/sleds-ledger.yml` from the SLEDS repo, with a repo secret `SLEDS_TOKEN`",
1262
- note: "Mint a seat for CI (Settings › Seats & tokens) and store its token as the secret. Then every PR is sized when it opens and closed on merge with the `SLEDS-Actual:` line from its body — the est → actual column on the Sled. It comments on the PR; leave it out while strictly read-only."
1285
+ note: "Mint a seat for CI (Settings › People, under a service person) and store its token as the secret. Then every PR is sized when it opens and closed on merge with the `SLEDS-Actual:` line from its body — the est → actual column on the Sled. It comments on the PR; leave it out while strictly read-only."
1263
1286
  },
1264
1287
  {
1265
1288
  do: "When you are ready to write: add a `sled` label to the repos",
@@ -2076,7 +2099,25 @@ function fetchClient(o2) {
2076
2099
  };
2077
2100
  }
2078
2101
  var PROTOCOL_VERSION = "2025-06-18";
2079
- var SERVER_INFO = { name: "sleds", version: "0.0.1" };
2102
+ var SERVER_INFO = {
2103
+ name: "sleds",
2104
+ title: "SLEDS",
2105
+ version: "0.1.23",
2106
+ description: "The cost and limits of AI agent work — estimate, hand off, and track what a task takes.",
2107
+ websiteUrl: "https://sleds.ai",
2108
+ icons: [
2109
+ { src: "https://agent.sleds.ai/icon.png", mimeType: "image/png", sizes: ["192x192"] },
2110
+ { src: "https://agent.sleds.ai/icon.svg", mimeType: "image/svg+xml", sizes: ["any"] }
2111
+ ]
2112
+ };
2113
+ var SERVER_INSTRUCTIONS = [
2114
+ "SLEDS is the ledger of engineering work: every piece is sized before it runs (a cost and time band, never a point figure) and closed with a measured actual after. You are the head; SLEDS holds the record. The person may be a PM or designer describing something they want built, or an engineer about to build — read which from the conversation.",
2115
+ 'When someone describes work for engineering (a feature, a fix, a spec, a PRD, "can we…"): offer once to put it on the sled. If they say yes, use `intake` — it drafts the brief, asks only what it cannot answer (who it is for and why now, what done looks like, the smallest version, the worth) two questions at a time, and writes nothing until they confirm. Never guess worth: leave it blank and say so. Afterwards tell them in plain words that it is on the sled, the band, and whether it waits for product\'s yes. They never need to learn a verb.',
2116
+ "When you are about to build something that will take more than a few minutes: `status` first if a row may already exist for it; otherwise `estimate` with a stable `source` (kind · project · doc · item) before the first line of work, and say the band. When it lands: `actual` with the same `source`, the state, where the output lives (`ref`), the model, and your session link (`sessionId`). Do not pass `minutes` unless a person stated them — SLEDS measures the time itself. A follow-up to something already landed is a new `item` under the same doc, never an overwrite. An `actual` with no estimate still records — the row is created and shown as landed unsized — but the band you skipped is the point, so estimate first.",
2117
+ "If SLEDS says there is no estimate for the source, size it first and send the actual again. If it says unreachable, try once more; if that fails, move on and tell the person the row is still owed. Never invent a number to get past an error."
2118
+ ].join(`
2119
+
2120
+ `);
2080
2121
  var MCP_TIMEOUT_MS = 5000;
2081
2122
  var sourceSchema = {
2082
2123
  type: "object",
@@ -2116,12 +2157,12 @@ var TOOLS = [
2116
2157
  },
2117
2158
  {
2118
2159
  name: "estimate",
2119
- description: "Size a task before running it: the org estimator's cost and time band with confidence and a spend cap. Recorded, so its actuals calibrate the next estimate. Never a point figure.",
2160
+ description: "Call this BEFORE starting any build that will take more than a few minutes — before the first line of work, not after. Sizes the task: the org estimator's cost and time band with confidence and a spend cap, never a point figure. Pass a stable `source` (kind · project · doc · item) so the row has a home on the sled and the later `actual` finds it. Recorded, so actuals calibrate the next estimate.",
2120
2161
  inputSchema: specSchema
2121
2162
  },
2122
2163
  {
2123
2164
  name: "intake",
2124
- description: "Send an ask to engineering from this conversation (spec 13). Pass what the person sent — a paragraph, a spec, a PRD — plus any links. Without `confirm` nothing is written: you get the brief drafted from it and the questions still open (who it is for and why now, what done looks like, the smallest version, the worth, what it touches) — ask at most two at a time, in your own words, and skip any the person already answered. Put their answers in `answers`, show them the brief, and when they say yes call again with `confirm: true`: the row is filed on the sled, sized, with their name on it. A brief without a why and a done is a draft and will not file. Worth is never guessed.",
2165
+ description: 'Use this when a person describes work they want engineering to do — a feature, a fix, a spec, a PRD, "can we…" — and has said yes to putting it on the sled. Sends the ask to engineering from this conversation (spec 13). Pass what the person sent — a paragraph, a spec, a PRD — plus any links. Without `confirm` nothing is written: you get the brief drafted from it and the questions still open (who it is for and why now, what done looks like, the smallest version, the worth, what it touches) — ask at most two at a time, in your own words, and skip any the person already answered. Put their answers in `answers`, show them the brief, and when they say yes call again with `confirm: true`: the row is filed on the sled, sized, with their name on it. A brief without a why and a done is a draft and will not file. Worth is never guessed.',
2125
2166
  inputSchema: {
2126
2167
  type: "object",
2127
2168
  properties: {
@@ -2243,7 +2284,7 @@ var TOOLS = [
2243
2284
  },
2244
2285
  {
2245
2286
  name: "status",
2246
- description: "Where is it — one paragraph per piece of work: its stage on the sled (waiting for product · deferred · ready · on the sled · landed), who has it, the size band, the worth, the tree when it has milestones, and the PR when it landed. Pass q (a tracker key like ENG-482, or a word from the title); with nothing, the rows you filed through intake, newest first.",
2287
+ description: "Where is it — call this first when you may be picking up work that already has a row (a ticket key, a spec section, a hand-off), so you estimate nothing twice. One paragraph per piece of work: its stage on the sled (waiting for product · deferred · ready · on the sled · landed), who has it, the size band, the worth, the tree when it has milestones, and the PR when it landed. Pass q (a tracker key like ENG-482, or a word from the title); with nothing, the rows you filed through intake, newest first.",
2247
2288
  inputSchema: {
2248
2289
  type: "object",
2249
2290
  properties: {
@@ -2298,7 +2339,7 @@ var TOOLS = [
2298
2339
  },
2299
2340
  {
2300
2341
  name: "actual",
2301
- description: "Close the loop on an estimated item: record what actually happened — minutes, dollars, tokens, model, who did it, when it started and finished, where the output lives (PR url), time by phase (discuss/build/deploy/test), and its state (started · done · merged · shipped). Same `source` as the estimate; Projects shows estimate vs actual and calibrates future bands.",
2342
+ description: "Call this WHEN THE WORK LANDS (merged, shipped, done) — the last thing you do on a build, every time, with the same `source` the estimate was filed under. Records what happened: state (started · done · merged · shipped), where the output lives (`ref`: PR url, commit, doc), the model, who did it, dollars and tokens when something counted them, time by phase. Minutes are MEASURED by SLEDS from the pickup (or the estimate) to now — do not invent a number; pass `minutes` only when a person stated it. Pass `sessionId` (your session link) so the session's token spend joins the row. If SLEDS answers that no estimate is on file, call `estimate` with this source first, then send this again.",
2302
2343
  inputSchema: {
2303
2344
  type: "object",
2304
2345
  properties: {
@@ -2317,7 +2358,14 @@ var TOOLS = [
2317
2358
  },
2318
2359
  required: ["kind", "project"]
2319
2360
  },
2320
- minutes: { type: "number", description: "Total wall-clock minutes of work." },
2361
+ minutes: {
2362
+ type: "number",
2363
+ description: "Only when a person stated the minutes or you timed them with a clock. Otherwise leave it out: SLEDS measures from the pickup (or the estimate) to now and records that; a number you pass is kept beside it as 'said'. On a row that already landed, a number here corrects the record."
2364
+ },
2365
+ sessionId: {
2366
+ type: "string",
2367
+ description: "This session's id or link (the `Claude-Session:` line you are told to sign with, e.g. https://claude.ai/code/session_…). Lets the session's token spend join the row."
2368
+ },
2321
2369
  usd: {
2322
2370
  type: ["number", "null"],
2323
2371
  description: "Dollars spent on the platform key, when known. Pass null to clear a figure recorded by mistake (seat work carries seatCreditsUsd, never usd)."
@@ -2406,6 +2454,30 @@ var isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2406
2454
  var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
2407
2455
  var str4 = (v) => typeof v === "string" ? v : undefined;
2408
2456
  var errorMessage = (j) => isObject4(j?.error) ? str4(j.error.message) : undefined;
2457
+ async function postSaying(ctx, path, body, nothing) {
2458
+ const r = await ctx.client.postFull(path, body);
2459
+ if (!r)
2460
+ return { fail: { text: `${nothing} ${UNREACHABLE.text}`, isError: true } };
2461
+ const j = r.json ?? {};
2462
+ if (r.status >= 200 && r.status < 300)
2463
+ return { json: j };
2464
+ if (r.status >= 500)
2465
+ return {
2466
+ fail: {
2467
+ text: `${nothing} ${errorMessage(j) ?? "Control plane unreachable"} (HTTP ${r.status}) → try once more in a moment, then move on and say so.`,
2468
+ isError: true
2469
+ }
2470
+ };
2471
+ const why = errorMessage(j) ?? str4(j.reason) ?? `HTTP ${r.status}`;
2472
+ return { fail: { text: `${nothing} ${why}.`, isError: true } };
2473
+ }
2474
+ function sourceText(source) {
2475
+ return [
2476
+ `${str4(source.kind) ?? "?"} ${str4(source.project) ?? "?"}`,
2477
+ str4(source.doc),
2478
+ str4(source.item)
2479
+ ].filter(Boolean).join(" › ");
2480
+ }
2409
2481
  async function usageTool(ctx) {
2410
2482
  if (ctx.usage)
2411
2483
  return ctx.usage();
@@ -2440,13 +2512,10 @@ async function estimateTool(ctx, args) {
2440
2512
  if (!spec)
2441
2513
  return { text: "Nothing to estimate. Pass spec: what the job should do.", isError: true };
2442
2514
  const source = isObject4(args.source) && str4(args.source.kind) && str4(args.source.project) ? args.source : undefined;
2443
- const r = await ctx.client.post("/proxy/estimate", {
2444
- spec,
2445
- model: str4(args.model),
2446
- ...source ? { source } : {}
2447
- });
2448
- if (!r)
2449
- return UNREACHABLE;
2515
+ const res = await postSaying(ctx, "/proxy/estimate", { spec, model: str4(args.model), ...source ? { source } : {} }, "Nothing was estimated.");
2516
+ if ("fail" in res)
2517
+ return res.fail;
2518
+ const r = res.json;
2450
2519
  const band = isObject4(r.band) ? r.band : {};
2451
2520
  const line = bandLine(band) ?? "band unreadable";
2452
2521
  const method = str4(band.method) ?? "plan";
@@ -2590,9 +2659,10 @@ async function overnightTool(ctx, args) {
2590
2659
  const spec = str4(args.spec)?.trim();
2591
2660
  if (!spec)
2592
2661
  return { text: "Nothing to queue. Pass spec: what the job should do.", isError: true };
2593
- const r = await ctx.client.post("/proxy/overnight", { spec, model: str4(args.model) });
2594
- if (!r)
2595
- return { text: `Could not queue the job. ${UNREACHABLE.text}`, isError: true };
2662
+ const res = await postSaying(ctx, "/proxy/overnight", { spec, model: str4(args.model) }, "Could not queue the job.");
2663
+ if ("fail" in res)
2664
+ return res.fail;
2665
+ const r = res.json;
2596
2666
  const jobId = str4(r.jobId) ?? "unknown";
2597
2667
  const now = (ctx.now ?? Date.now)();
2598
2668
  const at = num3(r.scheduledAt);
@@ -2919,13 +2989,14 @@ async function deferTool(ctx, args) {
2919
2989
  const at = atRaw ? parseAt(atRaw, now) : undefined;
2920
2990
  if (atRaw && at === undefined)
2921
2991
  return { text: "`at` takes +2h, +90m or an ISO time.", isError: true };
2922
- const r = await ctx.client.post("/proxy/defer", {
2992
+ const res = await postSaying(ctx, "/proxy/defer", {
2923
2993
  ...at !== undefined ? { resetAt: at } : {},
2924
2994
  ...str4(args.model) ? { model: str4(args.model) } : {},
2925
2995
  host: "mcp"
2926
- });
2927
- if (!r)
2928
- return { text: `Nothing was deferred. ${UNREACHABLE.text}`, isError: true };
2996
+ }, "Nothing was deferred.");
2997
+ if ("fail" in res)
2998
+ return res.fail;
2999
+ const r = res.json;
2929
3000
  const jobId = str4(r.jobId) ?? "unknown";
2930
3001
  const when = num3(r.scheduledAt);
2931
3002
  const startsIn = when !== undefined && when > now + 60000 ? `starts in ${shortDuration(when - now)}` : "starts at the next musher tick (within 5 min)";
@@ -2933,6 +3004,13 @@ async function deferTool(ctx, args) {
2933
3004
  text: `Deferred. Job ${jobId} ${startsIn}. With no platform key it runs on the seat's own Mac (sleds musher must be installed there — sleds doctor checks); with a key, in the cloud. Watch it with the jobs tool or /sled jobs.${str4(args.task) ? ` Note kept: ${str4(args.task)}` : ""}`
2934
3005
  };
2935
3006
  }
3007
+ function sessionIdOf(v) {
3008
+ const s = str4(v)?.trim();
3009
+ if (!s)
3010
+ return;
3011
+ const m = s.match(/(session_[A-Za-z0-9]+)/);
3012
+ return m?.[1] ?? (/^[A-Za-z0-9_-]{8,}$/.test(s) ? s : undefined);
3013
+ }
2936
3014
  async function actualTool(ctx, args) {
2937
3015
  if (!ctx.client)
2938
3016
  return NOT_SET_UP;
@@ -2955,32 +3033,54 @@ async function actualTool(ctx, args) {
2955
3033
  ...num3(args.tokensIn) !== undefined ? { tokensIn: num3(args.tokensIn) } : {},
2956
3034
  ...num3(args.tokensOut) !== undefined ? { tokensOut: num3(args.tokensOut) } : {},
2957
3035
  ...str4(args.by) ? { by: str4(args.by) } : {},
3036
+ ...sessionIdOf(args.sessionId) ? { sessionId: sessionIdOf(args.sessionId) } : {},
2958
3037
  ...str4(args.ref) ? { ref: str4(args.ref) } : {},
2959
3038
  ...phases && Object.keys(phases).length > 0 ? { phases } : {},
2960
3039
  ...num3(args.seatCreditsUsd) !== undefined ? { seatCreditsUsd: num3(args.seatCreditsUsd) } : {}
2961
3040
  };
2962
- const r = await ctx.client.post("/proxy/estimate/actual", {
3041
+ const body = {
2963
3042
  source,
2964
- ...num3(args.minutes) !== undefined ? { actualMinutes: num3(args.minutes) } : {},
3043
+ ...num3(args.minutes) !== undefined ? { saidMinutes: num3(args.minutes) } : {},
2965
3044
  ...num3(args.usd) !== undefined ? { actualUsd: num3(args.usd) } : {},
2966
3045
  ...args.usd === null ? { actualUsd: null } : {},
2967
3046
  ...str4(args.state) ? { state: str4(args.state) } : {},
2968
3047
  ...Object.keys(work).length > 0 ? { work } : {}
2969
- });
2970
- if (!r)
3048
+ };
3049
+ const res = await ctx.client.postFull("/proxy/estimate/actual", body);
3050
+ if (!res)
2971
3051
  return { text: `Nothing recorded. ${UNREACHABLE.text}`, isError: true };
2972
- if (r.ok === false)
3052
+ const r = res.json ?? {};
3053
+ if (res.status === 404 || r.ok === false) {
3054
+ const src = isObject4(source) ? sourceText(source) : "that source";
2973
3055
  return {
2974
- text: `No estimate on file for that source (${str4(r.reason) ?? "unknown"}). Estimate it first.`,
3056
+ text: `Nothing recorded: no estimate on file for ${src}. Size it first — call estimate with the same source and a one-line spec of what was built — then send this actual again; SLEDS will measure the time from that estimate. Do not invent minutes to get past this.`,
2975
3057
  isError: true
2976
3058
  };
3059
+ }
3060
+ if (res.status >= 500)
3061
+ return {
3062
+ text: `Nothing recorded. ${errorMessage(r) ?? "Control plane unreachable"} (HTTP ${res.status}) → try once more in a moment, then move on and say the actual is still owed.`,
3063
+ isError: true
3064
+ };
3065
+ if (res.status < 200 || res.status >= 300)
3066
+ return {
3067
+ text: `Nothing recorded: ${errorMessage(r) ?? `HTTP ${res.status}`}.`,
3068
+ isError: true
3069
+ };
3070
+ const recorded = num3(r.minutes);
3071
+ const from = str4(r.elapsedFrom);
3072
+ const unsized = r.unsized === true;
2977
3073
  const bits = [
2978
- num3(args.minutes) !== undefined ? `${num3(args.minutes)} min` : phases ? "phases recorded" : null,
3074
+ recorded !== undefined ? `${recorded} min${from ? ` measured from the ${from}` : ""}${num3(r.saidMinutes) !== undefined && num3(r.saidMinutes) !== recorded ? ` (you said ${num3(r.saidMinutes)})` : ""}` : phases ? "phases recorded" : null,
2979
3075
  num3(args.usd) !== undefined ? usd2(num3(args.usd)) : args.usd === null ? "$ cleared" : null,
2980
3076
  num3(args.tokensIn) !== undefined || num3(args.tokensOut) !== undefined ? `${(num3(args.tokensIn) ?? 0) + (num3(args.tokensOut) ?? 0)} tokens` : null,
2981
3077
  str4(args.model),
2982
3078
  str4(args.state) ?? "done"
2983
3079
  ].filter(Boolean);
3080
+ if (unsized)
3081
+ return {
3082
+ text: `Recorded: ${bits.join(" · ")}. No estimate came first, so the row was created from this actual and the Sled shows it as landed unsized — the time could only be measured from now, so pass \`minutes\` only if a person states them. Next time, size before you start: \`estimate\` with this source before the first line.`
3083
+ };
2984
3084
  return { text: `Recorded: ${bits.join(" · ")}. Projects shows it beside the estimate.` };
2985
3085
  }
2986
3086
  async function buddyTool(ctx, args) {
@@ -3089,7 +3189,8 @@ async function handleLine(line, ctx) {
3089
3189
  return response(id, {
3090
3190
  protocolVersion: requested ?? PROTOCOL_VERSION,
3091
3191
  capabilities: { tools: {} },
3092
- serverInfo: SERVER_INFO
3192
+ serverInfo: SERVER_INFO,
3193
+ instructions: SERVER_INSTRUCTIONS
3093
3194
  });
3094
3195
  }
3095
3196
  case "ping":
@@ -3872,6 +3973,7 @@ var scanTranscriptForCredits = (path, prior) => {
3872
3973
  };
3873
3974
  var seatWindowBody = (w) => ({
3874
3975
  capturedAt: w.capturedAt,
3976
+ source: "claude",
3875
3977
  ...w.fiveHour ? { fiveHour: w.fiveHour } : {},
3876
3978
  ...w.sevenDay ? { sevenDay: w.sevenDay } : {},
3877
3979
  ...w.spendLimit ? { spendLimit: w.spendLimit } : {}
@@ -3891,7 +3993,7 @@ var postJson = async (o2, path, body) => {
3891
3993
  }
3892
3994
  };
3893
3995
  var postSeatWindow = (o2, w) => postJson(o2, "/proxy/seat", seatWindowBody(w));
3894
- var postSeatWall = (o2, input) => postJson(o2, "/proxy/seat-wall", input);
3996
+ var postSeatWall = (o2, input) => postJson(o2, "/proxy/seat-wall", { source: "claude", ...input });
3895
3997
  var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.";
3896
3998
  var SEAT_WALL_LINE_NO_KEY = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.";
3897
3999
  var seatWallLine = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE : SEAT_WALL_LINE_NO_KEY;
@@ -4507,11 +4609,14 @@ async function runDoctor(o2) {
4507
4609
  if (settings) {
4508
4610
  const seat2 = hasSeatHook(settings);
4509
4611
  const spec = hasSpecHook(settings);
4612
+ const session = hasSessionHook(settings);
4613
+ const stop = hasStopHook(settings);
4614
+ const all = seat2 && spec && session && stop;
4510
4615
  checks.push({
4511
- level: seat2 && spec ? "ok" : "warn",
4616
+ level: all ? "ok" : "warn",
4512
4617
  name: "hooks",
4513
- detail: `seat-event ${seat2 ? "on" : "missing"} · spec-event ${spec ? "on" : "missing"}`,
4514
- fix: seat2 && spec ? undefined : "sleds hooks"
4618
+ detail: `seat-event ${seat2 ? "on" : "missing"} · spec-event ${spec ? "on" : "missing"} · session-event ${session ? "on" : "missing"} · stop-event ${stop ? "on" : "missing"}`,
4619
+ fix: all ? undefined : "sleds hooks"
4515
4620
  });
4516
4621
  const want = env.SLEDS_HOME?.trim() ? resolve3(home, env.SLEDS_HOME.trim()) : undefined;
4517
4622
  const cmds = sledsCommandsIn(settings);
@@ -4638,6 +4743,88 @@ var doctor = async (o2) => {
4638
4743
 
4639
4744
  // src/mcp.ts
4640
4745
  init_meter();
4746
+
4747
+ // src/owed.ts
4748
+ init_config();
4749
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
4750
+ import { dirname as dirname4, join as join8 } from "node:path";
4751
+ var owedPath = (home) => join8(configDir(home), "owed.json");
4752
+ var sessionPath = (home) => join8(configDir(home), "session.json");
4753
+ var OWED_TTL_MS = 3 * 24 * 60 * 60000;
4754
+ var str5 = (v) => typeof v === "string" ? v : undefined;
4755
+ function sourceKey(s) {
4756
+ const trim = (x) => x.trim().slice(0, 120);
4757
+ const kind = str5(s.kind) ?? "manual";
4758
+ const project = trim(str5(s.project) ?? "");
4759
+ const doc = str5(s.doc);
4760
+ const item = str5(s.item);
4761
+ return `${kind}:${project}${doc ? `/${trim(doc)}` : ""}${item ? `#${trim(item)}` : ""}`;
4762
+ }
4763
+ function sourceText2(s) {
4764
+ return [`${str5(s.kind) ?? "?"} ${str5(s.project) ?? "?"}`, str5(s.doc), str5(s.item)].filter(Boolean).join(" › ");
4765
+ }
4766
+ var readJson = (p2) => {
4767
+ if (!existsSync9(p2))
4768
+ return null;
4769
+ try {
4770
+ return JSON.parse(readFileSync8(p2, "utf8"));
4771
+ } catch {
4772
+ return null;
4773
+ }
4774
+ };
4775
+ var writeJson = (p2, v) => {
4776
+ mkdirSync7(dirname4(p2), { recursive: true, mode: 448 });
4777
+ const tmp = `${p2}.${process.pid}.tmp`;
4778
+ writeFileSync7(tmp, `${JSON.stringify(v, null, 2)}
4779
+ `, { mode: 384 });
4780
+ renameSync5(tmp, p2);
4781
+ };
4782
+ var readOwed = (home, now = Date.now()) => {
4783
+ const f = readJson(owedPath(home));
4784
+ const rows = Array.isArray(f?.rows) ? f.rows : [];
4785
+ return rows.filter((r) => r && typeof r.key === "string" && now - r.ts < OWED_TTL_MS);
4786
+ };
4787
+ var writeOwed = (home, rows) => writeJson(owedPath(home), { rows });
4788
+ var readSession = (home) => {
4789
+ const s = readJson(sessionPath(home));
4790
+ return s && typeof s.sessionId === "string" ? s : null;
4791
+ };
4792
+ var writeSession = (home, s) => writeJson(sessionPath(home), s);
4793
+ function withEstimated(rows, a) {
4794
+ const key = sourceKey(a.source);
4795
+ const row2 = {
4796
+ key,
4797
+ source: a.source,
4798
+ ...a.title ? { title: a.title } : {},
4799
+ ts: a.now,
4800
+ ...a.sessionId ? { sessionId: a.sessionId } : {}
4801
+ };
4802
+ return [...rows.filter((r) => r.key !== key), row2];
4803
+ }
4804
+ function withActual(rows, a) {
4805
+ if (a.state === "started")
4806
+ return rows;
4807
+ const key = sourceKey(a.source);
4808
+ return rows.filter((r) => r.key !== key);
4809
+ }
4810
+ function owedBySession(rows, sessionId) {
4811
+ return rows.filter((r) => r.sessionId === sessionId && r.askedAt === undefined);
4812
+ }
4813
+ function markAsked(rows, keys, now) {
4814
+ const set = new Set(keys);
4815
+ return rows.map((r) => set.has(r.key) ? { ...r, askedAt: now } : r);
4816
+ }
4817
+ var ago = (ms) => {
4818
+ const m = Math.round(ms / 60000);
4819
+ if (m < 1)
4820
+ return "just now";
4821
+ if (m < 60)
4822
+ return `${m} min ago`;
4823
+ const h = Math.round(m / 60);
4824
+ return h < 48 ? `${h} h ago` : `${Math.round(h / 24)} d ago`;
4825
+ };
4826
+
4827
+ // src/mcp.ts
4641
4828
  async function usageTool2(deps) {
4642
4829
  if (!meterSource(deps.home))
4643
4830
  return { text: "SLEDS is not set up on this machine. Run `sleds init` first.", isError: true };
@@ -4661,9 +4848,49 @@ async function usageTool2(deps) {
4661
4848
  return { text: lines.join(`
4662
4849
  `) };
4663
4850
  }
4851
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4852
+ function trackingClient(client, home, now) {
4853
+ const after = (path, body, status) => {
4854
+ if (status < 200 || status >= 300 || !isObj2(body.source))
4855
+ return;
4856
+ try {
4857
+ if (path === "/proxy/estimate") {
4858
+ const spec = typeof body.spec === "string" ? body.spec : "";
4859
+ writeOwed(home, withEstimated(readOwed(home, now()), {
4860
+ source: body.source,
4861
+ title: spec.split(`
4862
+ `)[0]?.trim().slice(0, 120) || undefined,
4863
+ now: now(),
4864
+ sessionId: readSession(home)?.sessionId
4865
+ }));
4866
+ } else if (path === "/proxy/estimate/actual") {
4867
+ writeOwed(home, withActual(readOwed(home, now()), {
4868
+ source: body.source,
4869
+ state: typeof body.state === "string" ? body.state : undefined
4870
+ }));
4871
+ }
4872
+ } catch {}
4873
+ };
4874
+ return {
4875
+ async post(path, body) {
4876
+ const r = await client.post(path, body);
4877
+ if (r)
4878
+ after(path, body, 200);
4879
+ return r;
4880
+ },
4881
+ async postFull(path, body) {
4882
+ const r = await client.postFull(path, body);
4883
+ if (r)
4884
+ after(path, body, r.status);
4885
+ return r;
4886
+ },
4887
+ get: (path) => client.get(path)
4888
+ };
4889
+ }
4664
4890
  function contextFor(deps) {
4665
4891
  const src = meterSource(deps.home);
4666
- const client = src ? fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }) : null;
4892
+ const now = deps.now ?? Date.now;
4893
+ const client = src ? trackingClient(fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }), deps.home, now) : null;
4667
4894
  return { client, now: deps.now, usage: () => usageTool2(deps) };
4668
4895
  }
4669
4896
  var callTool2 = (name, args, deps) => callTool(name, args, contextFor(deps));
@@ -4710,7 +4937,10 @@ var hooks = (o2) => {
4710
4937
  const hadSpec = hasSpecHook(settings);
4711
4938
  const withSeat = applySeatHook(settings, o2.seatEventCommand ?? sledsCommand("seat-event", o2.home, o2.env));
4712
4939
  const hadPrompt = hasPromptHook(settings);
4713
- const withSpec = applyPromptHook(applySpecHook(withSeat, o2.specEventCommand ?? sledsCommand("spec-event", o2.home, o2.env)), o2.promptEventCommand ?? sledsCommand("prompt-event", o2.home, o2.env));
4940
+ const hadSession = hasSessionHook(settings);
4941
+ const hadStop = hasStopHook(settings);
4942
+ const withPrompt = applyPromptHook(applySpecHook(withSeat, o2.specEventCommand ?? sledsCommand("spec-event", o2.home, o2.env)), o2.promptEventCommand ?? sledsCommand("prompt-event", o2.home, o2.env));
4943
+ const withSpec = applyStopHook(applySessionHook(withPrompt, o2.sessionEventCommand ?? sledsCommand("session-event", o2.home, o2.env)), o2.stopEventCommand ?? sledsCommand("stop-event", o2.home, o2.env));
4714
4944
  const hadLine = o2.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
4715
4945
  writeSettings(o2.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o2.home, o2.env)).settings : withSpec);
4716
4946
  log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o2.home)}`);
@@ -4718,24 +4948,35 @@ var hooks = (o2) => {
4718
4948
  log(`refreshed statusLine in ${settingsPath(o2.home)}`);
4719
4949
  log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o2.home)}`);
4720
4950
  log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o2.home)}`);
4951
+ log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o2.home)}`);
4952
+ log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o2.home)}`);
4721
4953
  return 0;
4722
4954
  };
4723
4955
 
4724
4956
  // src/commands/init.ts
4725
4957
  init_config();
4726
4958
  init_settings();
4727
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
4959
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
4728
4960
  import { createInterface } from "node:readline";
4729
4961
 
4730
4962
  // src/skill.ts
4731
4963
  var SKILL_MD = `---
4732
4964
  name: sled
4733
- description: Hand work to SLEDS — continue past a rate limit or credit wall on the org's key, hold until reset, or run overnight at half price. Use when the user says /sled, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
4965
+ description: The SLEDS loop for engineering work — size a build before starting it, record the actual when it lands, and hand work to SLEDS past a rate limit or credit wall (continue on the org's key, hold until reset, run overnight at half price). Use when the user says /sled, is about to start a build, picks up a ticket or a spec section, merges or ships something, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
4734
4966
  ---
4735
4967
 
4736
4968
  # SLEDS
4737
4969
 
4738
- Nine to five is 40 of 168 hours. SLEDS puts the other 128 to work: it carries a session past a wall, and it runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on. Nothing changes until a wall or a \`/sled\`.
4970
+ SLEDS is the ledger of engineering work: every piece is sized before it runs and closed with a measured actual after. In this session the seat is the person's; SLEDS holds the record. It also carries a session past a wall and runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on.
4971
+
4972
+ ## The loop (every build, no exceptions)
4973
+
4974
+ 1. **Before picking up work that may already have a row** — a ticket key, a spec section, a hand-off — call \`status\` with the key or a word from the title. If it is on the sled, take it; do not estimate it twice.
4975
+ 2. **Before the first line of any build that takes more than a few minutes** — call \`estimate\` with a stable \`source\` (kind · project · doc · item, e.g. \`repo · sleds · docs/specs/11-the-seam.md › slice 2\`). Say the band to the person. Never a point figure, never your own guess.
4976
+ 3. **When it lands** (merged, shipped, done) — call \`actual\` with the same \`source\`: the state, where the output lives (\`ref\`), the model, and your session link (\`sessionId\`). Do **not** pass \`minutes\` unless the person stated them: SLEDS measures the time from the pickup or the estimate. A follow-up to something already landed is a new \`item\` under the same doc, never an overwrite.
4977
+ 4. If SLEDS says there is no estimate for the source, size it first and send the actual again. If it says unreachable, try once more, then move on and say the actual is still owed. Never invent a number to get past an error.
4978
+
4979
+ The SessionStart hook tells you what this machine still owes; the Stop hook will ask once if you estimated and never posted the actual. Both are \`sleds hooks\`.
4739
4980
 
4740
4981
  ## Hit a limit on your seat
4741
4982
 
@@ -4873,7 +5114,10 @@ var hooks2 = (o2) => {
4873
5114
  const hadSpec = hasSpecHook(settings);
4874
5115
  const withSeat = applySeatHook(settings, o2.seatEventCommand ?? sledsCommand("seat-event", o2.home, o2.env));
4875
5116
  const hadPrompt = hasPromptHook(settings);
4876
- const withSpec = applyPromptHook(applySpecHook(withSeat, o2.specEventCommand ?? sledsCommand("spec-event", o2.home, o2.env)), o2.promptEventCommand ?? sledsCommand("prompt-event", o2.home, o2.env));
5117
+ const hadSession = hasSessionHook(settings);
5118
+ const hadStop = hasStopHook(settings);
5119
+ const withPrompt = applyPromptHook(applySpecHook(withSeat, o2.specEventCommand ?? sledsCommand("spec-event", o2.home, o2.env)), o2.promptEventCommand ?? sledsCommand("prompt-event", o2.home, o2.env));
5120
+ const withSpec = applyStopHook(applySessionHook(withPrompt, o2.sessionEventCommand ?? sledsCommand("session-event", o2.home, o2.env)), o2.stopEventCommand ?? sledsCommand("stop-event", o2.home, o2.env));
4877
5121
  const hadLine = o2.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
4878
5122
  writeSettings(o2.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o2.home, o2.env)).settings : withSpec);
4879
5123
  log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o2.home)}`);
@@ -4881,6 +5125,8 @@ var hooks2 = (o2) => {
4881
5125
  log(`refreshed statusLine in ${settingsPath(o2.home)}`);
4882
5126
  log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o2.home)}`);
4883
5127
  log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o2.home)}`);
5128
+ log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o2.home)}`);
5129
+ log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o2.home)}`);
4884
5130
  return 0;
4885
5131
  };
4886
5132
 
@@ -5093,6 +5339,8 @@ var init = async (o2) => {
5093
5339
  seatEventCommand: o2.seatEventCommand,
5094
5340
  specEventCommand: o2.specEventCommand,
5095
5341
  promptEventCommand: o2.promptEventCommand,
5342
+ sessionEventCommand: o2.sessionEventCommand,
5343
+ stopEventCommand: o2.stopEventCommand,
5096
5344
  log: (line) => wrote.push(line),
5097
5345
  error
5098
5346
  });
@@ -5112,8 +5360,8 @@ var init = async (o2) => {
5112
5360
  ...statusLine2.prior !== undefined ? { priorStatusLine: statusLine2.prior } : {}
5113
5361
  });
5114
5362
  wrote.push(`saved token and proxy URL to ${configPath(o2.home)} (0600)`);
5115
- mkdirSync7(skillDir(o2.home), { recursive: true });
5116
- writeFileSync7(skillPath(o2.home), SKILL_MD);
5363
+ mkdirSync8(skillDir(o2.home), { recursive: true });
5364
+ writeFileSync8(skillPath(o2.home), SKILL_MD);
5117
5365
  wrote.push(`installed skill at ${skillPath(o2.home)}`);
5118
5366
  if (o2.installMusher) {
5119
5367
  const r = o2.installMusher();
@@ -5199,20 +5447,20 @@ var meter = async (o2) => {
5199
5447
  init_config();
5200
5448
  init_meter();
5201
5449
  import { spawnSync as spawnSync3 } from "node:child_process";
5202
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "node:fs";
5450
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "node:fs";
5203
5451
  import { hostname as hostname2 } from "node:os";
5204
- import { dirname as dirname4 } from "node:path";
5452
+ import { dirname as dirname5 } from "node:path";
5205
5453
  var defaultWhich3 = (cmd) => {
5206
5454
  const path = process.env.PATH ?? "";
5207
5455
  for (const dir of path.split(":").filter(Boolean)) {
5208
5456
  const p2 = `${dir}/${cmd}`;
5209
- if (existsSync9(p2))
5457
+ if (existsSync10(p2))
5210
5458
  return p2;
5211
5459
  }
5212
5460
  return null;
5213
5461
  };
5214
5462
  var findClaude2 = (home, configured, which) => {
5215
- if (configured && existsSync9(configured))
5463
+ if (configured && existsSync10(configured))
5216
5464
  return configured;
5217
5465
  const found = which("claude");
5218
5466
  if (found)
@@ -5223,7 +5471,7 @@ var findClaude2 = (home, configured, which) => {
5223
5471
  "/usr/local/bin/claude",
5224
5472
  `${home}/.npm-global/bin/claude`
5225
5473
  ])
5226
- if (existsSync9(p2))
5474
+ if (existsSync10(p2))
5227
5475
  return p2;
5228
5476
  return null;
5229
5477
  };
@@ -5239,8 +5487,8 @@ var musherInstall = (o2) => {
5239
5487
  const plist = musherPlistPath(o2.home, env);
5240
5488
  const label = musherLabel(env);
5241
5489
  const dir = configDir(o2.home, env);
5242
- mkdirSync8(dirname4(plist), { recursive: true });
5243
- mkdirSync8(dir, { recursive: true, mode: 448 });
5490
+ mkdirSync9(dirname5(plist), { recursive: true });
5491
+ mkdirSync9(dir, { recursive: true, mode: 448 });
5244
5492
  const execPath = o2.sledsArgv[0] ?? process.execPath;
5245
5493
  const agentEnv = {
5246
5494
  PATH: musherPath(o2.home, execPath, env.PATH ?? ""),
@@ -5249,9 +5497,9 @@ var musherInstall = (o2) => {
5249
5497
  ...env.CLAUDE_CONFIG_DIR ? { CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR } : {}
5250
5498
  };
5251
5499
  const exec = o2.exec ?? defaultExecSync;
5252
- if (existsSync9(plist))
5500
+ if (existsSync10(plist))
5253
5501
  exec("launchctl", ["unload", plist]);
5254
- writeFileSync8(plist, buildMusherPlist({ sledsArgv: o2.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
5502
+ writeFileSync9(plist, buildMusherPlist({ sledsArgv: o2.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
5255
5503
  const r = exec("launchctl", ["load", plist]);
5256
5504
  if (!r.ok) {
5257
5505
  error(`launchctl load failed: ${r.stderr.trim() || "unknown"}`);
@@ -5270,7 +5518,7 @@ var musherUninstall = (o2) => {
5270
5518
  const env = o2.env ?? process.env;
5271
5519
  const plist = musherPlistPath(o2.home, env);
5272
5520
  const exec = o2.exec ?? defaultExecSync;
5273
- if (existsSync9(plist)) {
5521
+ if (existsSync10(plist)) {
5274
5522
  exec("launchctl", ["unload", plist]);
5275
5523
  rmSync2(plist, { force: true });
5276
5524
  log(`musher removed: ${musherLabel(env)}`);
@@ -5305,7 +5553,7 @@ var musherStatus = async (o2) => {
5305
5553
  const env = o2.env ?? process.env;
5306
5554
  const plist = musherPlistPath(o2.home, env);
5307
5555
  const cfg = readConfig(o2.home);
5308
- log(existsSync9(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
5556
+ log(existsSync10(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
5309
5557
  const claude = findClaude2(o2.home, cfg.musher?.claudePath, o2.which ?? defaultWhich3);
5310
5558
  log(`claude ${claude ?? "not found — sleds musher --claude /path/to/claude"}`);
5311
5559
  log(`mode ${cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
@@ -5417,10 +5665,10 @@ var musher = async (o2) => {
5417
5665
  init_config();
5418
5666
 
5419
5667
  // src/schedule.ts
5420
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
5421
- import { join as join8 } from "node:path";
5668
+ import { existsSync as existsSync11, mkdirSync as mkdirSync10, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
5669
+ import { join as join9 } from "node:path";
5422
5670
  var REVERT_LABEL = "ai.sleds.revert";
5423
- var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
5671
+ var plistPath = (home) => join9(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
5424
5672
  var xmlEscape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5425
5673
  var buildLaunchdPlist = (sledsArgv, deadline) => {
5426
5674
  const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape2(a)}</string>`).join(`
@@ -5459,9 +5707,9 @@ var scheduleRevert = (d, deadlineMs) => {
5459
5707
  if (d.platform === "darwin") {
5460
5708
  const p2 = plistPath(d.home);
5461
5709
  try {
5462
- mkdirSync9(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
5710
+ mkdirSync10(join9(d.home, "Library", "LaunchAgents"), { recursive: true });
5463
5711
  d.exec("launchctl", ["unload", p2]);
5464
- writeFileSync9(p2, buildLaunchdPlist(d.sledsArgv, deadline));
5712
+ writeFileSync10(p2, buildLaunchdPlist(d.sledsArgv, deadline));
5465
5713
  const r = d.exec("launchctl", ["load", p2]);
5466
5714
  if (r.ok)
5467
5715
  return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
@@ -5488,7 +5736,7 @@ var scheduleRevert = (d, deadlineMs) => {
5488
5736
  var cancelRevert = (d, kind) => {
5489
5737
  if (kind === "launchd" || d.platform === "darwin") {
5490
5738
  const p2 = plistPath(d.home);
5491
- if (existsSync10(p2)) {
5739
+ if (existsSync11(p2)) {
5492
5740
  d.exec("launchctl", ["unload", p2]);
5493
5741
  rmSync3(p2, { force: true });
5494
5742
  }
@@ -5801,14 +6049,14 @@ var seatEvent = async (o2) => {
5801
6049
 
5802
6050
  // src/commands/seatSync.ts
5803
6051
  import { execFileSync as execFileSync2 } from "node:child_process";
5804
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, writeFileSync as writeFileSync10 } from "node:fs";
6052
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, writeFileSync as writeFileSync11 } from "node:fs";
5805
6053
  import { platform } from "node:os";
5806
- import { join as join10 } from "node:path";
6054
+ import { join as join11 } from "node:path";
5807
6055
 
5808
6056
  // src/cookieDb.ts
5809
6057
  import { copyFileSync, rmSync as rmSync4 } from "node:fs";
5810
6058
  import { tmpdir } from "node:os";
5811
- import { join as join9 } from "node:path";
6059
+ import { join as join10 } from "node:path";
5812
6060
  var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
5813
6061
  var isBun = () => typeof globalThis.Bun !== "undefined";
5814
6062
  async function queryRows(dbPath) {
@@ -5837,7 +6085,7 @@ function nodeSqlite() {
5837
6085
  return mod;
5838
6086
  }
5839
6087
  async function readCookieRows(dbPath) {
5840
- const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
6088
+ const copy = join10(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
5841
6089
  copyFileSync(dbPath, copy);
5842
6090
  try {
5843
6091
  const rows = await queryRows(copy);
@@ -6113,7 +6361,7 @@ function findFile(root, name) {
6113
6361
  const stack = [root];
6114
6362
  while (stack.length > 0) {
6115
6363
  const dir = stack.pop();
6116
- if (!dir || !existsSync11(dir))
6364
+ if (!dir || !existsSync12(dir))
6117
6365
  continue;
6118
6366
  let entries;
6119
6367
  try {
@@ -6122,7 +6370,7 @@ function findFile(root, name) {
6122
6370
  continue;
6123
6371
  }
6124
6372
  for (const e of entries) {
6125
- const p2 = join10(dir, e.name);
6373
+ const p2 = join11(dir, e.name);
6126
6374
  if (e.isDirectory())
6127
6375
  stack.push(p2);
6128
6376
  else if (e.name === name)
@@ -6133,8 +6381,8 @@ function findFile(root, name) {
6133
6381
  }
6134
6382
  function findCookieDb(home, source) {
6135
6383
  const b = BROWSERS[source.browser];
6136
- const root = join10(home, APP_SUPPORT, b.appDir);
6137
- if (!existsSync11(root))
6384
+ const root = join11(home, APP_SUPPORT, b.appDir);
6385
+ if (!existsSync12(root))
6138
6386
  throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
6139
6387
  if (!b.profiled) {
6140
6388
  const db2 = findFile(root, "Cookies");
@@ -6142,15 +6390,15 @@ function findCookieDb(home, source) {
6142
6390
  throw new Error(`no cookie store under ${root}`);
6143
6391
  return db2;
6144
6392
  }
6145
- const localStatePath = join10(root, "Local State");
6146
- const localState = existsSync11(localStatePath) ? readFileSync9(localStatePath, "utf8") : null;
6393
+ const localStatePath = join11(root, "Local State");
6394
+ const localState = existsSync12(localStatePath) ? readFileSync10(localStatePath, "utf8") : null;
6147
6395
  const wanted = source.profile ?? "Default";
6148
6396
  const { dir, ambiguous } = resolveProfileDir(localState, wanted);
6149
6397
  if (ambiguous)
6150
6398
  throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
6151
6399
  if (!dir)
6152
6400
  throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
6153
- const db = cookieDbCandidates(join10(root, dir)).find((p2) => existsSync11(p2));
6401
+ const db = cookieDbCandidates(join11(root, dir)).find((p2) => existsSync12(p2));
6154
6402
  if (!db)
6155
6403
  throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
6156
6404
  return db;
@@ -6193,7 +6441,7 @@ function fetchSeatUsage(jar, org, now, run2) {
6193
6441
  }
6194
6442
  function launchAgentPlist(bun, script, home, source) {
6195
6443
  const label = launchLabel(source);
6196
- const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
6444
+ const log = join11(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
6197
6445
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6198
6446
  const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
6199
6447
  `);
@@ -6215,7 +6463,7 @@ ${args}
6215
6463
  `;
6216
6464
  }
6217
6465
  function plistPath2(home, source) {
6218
- return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
6466
+ return join11(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
6219
6467
  }
6220
6468
  function install(home, argv, source, log) {
6221
6469
  const bun = argv[0] ?? process.execPath;
@@ -6224,10 +6472,10 @@ function install(home, argv, source, log) {
6224
6472
  log("cannot resolve the sleds script path to install the timer");
6225
6473
  return 1;
6226
6474
  }
6227
- const dir = join10(home, "Library", "LaunchAgents");
6228
- mkdirSync10(dir, { recursive: true });
6475
+ const dir = join11(home, "Library", "LaunchAgents");
6476
+ mkdirSync11(dir, { recursive: true });
6229
6477
  const path = plistPath2(home, source);
6230
- writeFileSync10(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
6478
+ writeFileSync11(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
6231
6479
  try {
6232
6480
  execFileSync2("launchctl", ["unload", path], { stdio: "ignore" });
6233
6481
  } catch {}
@@ -6240,7 +6488,7 @@ function uninstall(home, source, log) {
6240
6488
  try {
6241
6489
  execFileSync2("launchctl", ["unload", "-w", path], { stdio: "ignore" });
6242
6490
  } catch {}
6243
- log(existsSync11(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
6491
+ log(existsSync12(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
6244
6492
  return 0;
6245
6493
  }
6246
6494
  var seatSync = async (o2) => {
@@ -6334,9 +6582,62 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
6334
6582
  }
6335
6583
  }
6336
6584
 
6585
+ // src/commands/sessionEvent.ts
6586
+ init_config();
6587
+ var parseSessionPayload = (raw) => {
6588
+ try {
6589
+ const j = JSON.parse(raw);
6590
+ return typeof j === "object" && j !== null ? j : {};
6591
+ } catch {
6592
+ return {};
6593
+ }
6594
+ };
6595
+ function sessionContext(a) {
6596
+ const who = [a.email, a.org ? `workspace ${a.org}` : undefined].filter(Boolean).join(" · ");
6597
+ const lines = [
6598
+ `SLEDS seat${who ? `: ${who}` : ""}. The loop: \`status\` before picking up work that may already have a row; \`estimate\` with a source before building anything that takes more than a few minutes; \`actual\` with the same source when it lands — never invent minutes, SLEDS measures them. A follow-up is a new item under the same doc.`
6599
+ ];
6600
+ if (a.owed.length > 0) {
6601
+ const shown = [...a.owed].sort((x, y) => y.ts - x.ts).slice(0, 5);
6602
+ lines.push(`Estimated from this machine and not yet landed (${a.owed.length}): ${shown.map((r) => `${r.title ? `${r.title} — ` : ""}${sourceText2(r.source)} (${ago(a.now - r.ts)})`).join("; ")}${a.owed.length > shown.length ? "; …" : ""}. If one of these is what you are continuing, post its actual when it lands; if it was abandoned, say so and it can be withdrawn.`);
6603
+ }
6604
+ return lines.join(`
6605
+ `);
6606
+ }
6607
+ var sessionEvent = async (o2) => {
6608
+ const log = o2.log ?? console.log;
6609
+ const now = (o2.now ?? Date.now)();
6610
+ const raw = o2.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
6611
+ const payload = parseSessionPayload(raw);
6612
+ if (payload.session_id) {
6613
+ try {
6614
+ writeSession(o2.home, {
6615
+ sessionId: payload.session_id,
6616
+ startedAt: now,
6617
+ ...payload.source ? { source: payload.source } : {}
6618
+ });
6619
+ } catch {}
6620
+ }
6621
+ let email;
6622
+ let org;
6623
+ try {
6624
+ const cfg = readConfig(o2.home);
6625
+ email = cfg.email;
6626
+ org = cfg.org?.slug;
6627
+ } catch {}
6628
+ let owed = [];
6629
+ try {
6630
+ owed = readOwed(o2.home, now);
6631
+ } catch {
6632
+ owed = [];
6633
+ }
6634
+ log(sessionContext({ email, org, owed, now }));
6635
+ return 0;
6636
+ };
6637
+
6337
6638
  // src/commands/specEvent.ts
6338
6639
  init_meter();
6339
- import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
6640
+ import { readFileSync as readFileSync11, statSync as statSync2 } from "node:fs";
6340
6641
  import { basename as basename2, isAbsolute, relative } from "node:path";
6341
6642
  var HOOK_STDIN_TIMEOUT_MS3 = 1000;
6342
6643
  var ESTIMATE_TIMEOUT_MS = 4000;
@@ -6392,7 +6693,7 @@ function readFileOrNull(path) {
6392
6693
  try {
6393
6694
  if (statSync2(path).size > 2000000)
6394
6695
  return null;
6395
- return readFileSync10(path, "utf8");
6696
+ return readFileSync11(path, "utf8");
6396
6697
  } catch {
6397
6698
  return null;
6398
6699
  }
@@ -6452,7 +6753,7 @@ var specEvent = async (o2) => {
6452
6753
  // src/commands/status.ts
6453
6754
  init_config();
6454
6755
  init_settings();
6455
- import { existsSync as existsSync12 } from "node:fs";
6756
+ import { existsSync as existsSync13 } from "node:fs";
6456
6757
  var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
6457
6758
  try {
6458
6759
  const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
@@ -6526,7 +6827,7 @@ var status = async (o2) => {
6526
6827
  const log = o2.log ?? console.log;
6527
6828
  const nowMs = (o2.now ?? Date.now)();
6528
6829
  const cfg = readConfig(o2.home);
6529
- const settings = existsSync12(settingsPath(o2.home)) ? readSettings(o2.home) : {};
6830
+ const settings = existsSync13(settingsPath(o2.home)) ? readSettings(o2.home) : {};
6530
6831
  const inSettings = readProxy(settings);
6531
6832
  const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
6532
6833
  const token = inSettings?.token ?? cfg.token;
@@ -6700,17 +7001,68 @@ var announce = (o2, events) => {
6700
7001
  }));
6701
7002
  };
6702
7003
 
7004
+ // src/commands/stopEvent.ts
7005
+ var parseStopPayload = (raw) => {
7006
+ try {
7007
+ const j = JSON.parse(raw);
7008
+ return typeof j === "object" && j !== null ? j : {};
7009
+ } catch {
7010
+ return {};
7011
+ }
7012
+ };
7013
+ function stopReason(rows) {
7014
+ const named = rows.map((r) => `${r.title ? `${r.title} — ` : ""}${sourceText2(r.source)}`).join("; ");
7015
+ return `SLEDS: this session estimated ${rows.length === 1 ? "a row" : `${rows.length} rows`} and no actual was posted: ${named}. If the work landed, call \`actual\` with the same source, the state, where the output lives (ref) and your session link — no minutes, SLEDS measures them. If it did not land, say so in one line (it stays on the sled as ready) and stop again. Do not invent a number.`;
7016
+ }
7017
+ function decideStop(a) {
7018
+ if (a.stopHookActive || !a.sessionId)
7019
+ return { block: false };
7020
+ const due = owedBySession(a.rows, a.sessionId);
7021
+ if (due.length === 0)
7022
+ return { block: false };
7023
+ return {
7024
+ block: true,
7025
+ reason: stopReason(due),
7026
+ rows: markAsked(a.rows, due.map((r) => r.key), a.now)
7027
+ };
7028
+ }
7029
+ var stopEvent = async (o2) => {
7030
+ const log = o2.log ?? console.log;
7031
+ const now = (o2.now ?? Date.now)();
7032
+ const raw = o2.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
7033
+ const payload = parseStopPayload(raw);
7034
+ let rows;
7035
+ try {
7036
+ rows = readOwed(o2.home, now);
7037
+ } catch {
7038
+ return 0;
7039
+ }
7040
+ const d = decideStop({
7041
+ rows,
7042
+ sessionId: payload.session_id,
7043
+ stopHookActive: payload.stop_hook_active === true,
7044
+ now
7045
+ });
7046
+ if (!d.block)
7047
+ return 0;
7048
+ try {
7049
+ writeOwed(o2.home, d.rows);
7050
+ } catch {}
7051
+ log(JSON.stringify({ decision: "block", reason: d.reason }));
7052
+ return 0;
7053
+ };
7054
+
6703
7055
  // src/commands/uninstall.ts
6704
7056
  import { spawnSync as spawnSync4 } from "node:child_process";
6705
- import { existsSync as existsSync14, rmSync as rmSync5 } from "node:fs";
7057
+ import { existsSync as existsSync15, rmSync as rmSync5 } from "node:fs";
6706
7058
 
6707
7059
  // src/codex.ts
6708
7060
  init_config();
6709
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
6710
- import { dirname as dirname5, join as join11 } from "node:path";
7061
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync12 } from "node:fs";
7062
+ import { dirname as dirname6, join as join12 } from "node:path";
6711
7063
  var PROVIDER_ID2 = "sleds";
6712
- var codexDir2 = (home) => join11(home, ".codex");
6713
- var codexConfigPath2 = (home) => join11(codexDir2(home), "config.toml");
7064
+ var codexDir2 = (home) => join12(home, ".codex");
7065
+ var codexConfigPath2 = (home) => join12(codexDir2(home), "config.toml");
6714
7066
  function removeCodexProvider2(toml, o2 = {}) {
6715
7067
  const lines = toml.split(`
6716
7068
  `);
@@ -6739,13 +7091,13 @@ function removeCodexProvider2(toml, o2 = {}) {
6739
7091
  }
6740
7092
  function uninstallCodex(home) {
6741
7093
  const path = codexConfigPath2(home);
6742
- if (!existsSync13(path))
7094
+ if (!existsSync14(path))
6743
7095
  return null;
6744
- const prior = readFileSync11(path, "utf8");
7096
+ const prior = readFileSync12(path, "utf8");
6745
7097
  const next = removeCodexProvider2(prior);
6746
7098
  if (next === prior)
6747
7099
  return null;
6748
- writeFileSync11(path, next, { mode: 384 });
7100
+ writeFileSync12(path, next, { mode: 384 });
6749
7101
  return `removed the "${PROVIDER_ID2}" provider from ${path}`;
6750
7102
  }
6751
7103
 
@@ -6762,7 +7114,7 @@ var uninstall2 = (o2) => {
6762
7114
  (o2.log ?? console.log)(` ${codexLine}`);
6763
7115
  const log = o2.log ?? console.log;
6764
7116
  const removed = [];
6765
- if (existsSync14(settingsPath(o2.home))) {
7117
+ if (existsSync15(settingsPath(o2.home))) {
6766
7118
  let settings = readSettings(o2.home);
6767
7119
  let changed = false;
6768
7120
  if (hasEitherKey(settings)) {
@@ -6795,22 +7147,32 @@ var uninstall2 = (o2) => {
6795
7147
  changed = true;
6796
7148
  removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(o2.home)}`);
6797
7149
  }
7150
+ if (hasSessionHook(settings)) {
7151
+ settings = removeSessionHook(settings);
7152
+ changed = true;
7153
+ removed.push(`hooks.SessionStart session-event entry from ${settingsPath(o2.home)}`);
7154
+ }
7155
+ if (hasStopHook(settings)) {
7156
+ settings = removeStopHook(settings);
7157
+ changed = true;
7158
+ removed.push(`hooks.Stop stop-event entry from ${settingsPath(o2.home)}`);
7159
+ }
6798
7160
  if (changed)
6799
7161
  writeSettings(o2.home, settings);
6800
7162
  }
6801
7163
  for (const file of [meterCachePath(o2.home), seatStatePath(o2.home)]) {
6802
- if (existsSync14(file)) {
7164
+ if (existsSync15(file)) {
6803
7165
  rmSync5(file, { force: true });
6804
7166
  removed.push(file);
6805
7167
  }
6806
7168
  }
6807
7169
  const skill = skillDir(o2.home);
6808
- if (existsSync14(skill)) {
7170
+ if (existsSync15(skill)) {
6809
7171
  rmSync5(skill, { recursive: true, force: true });
6810
7172
  removed.push(skill);
6811
7173
  }
6812
7174
  const plist = musherPlistPath(o2.home);
6813
- if (existsSync14(plist)) {
7175
+ if (existsSync15(plist)) {
6814
7176
  (o2.unloadAgent ?? defaultUnload)(plist);
6815
7177
  rmSync5(plist, { force: true });
6816
7178
  removed.push(`launchd agent ${musherLabel()}`);
@@ -6882,13 +7244,13 @@ var verify = async (o2) => {
6882
7244
 
6883
7245
  // src/config.ts
6884
7246
  init_settings();
6885
- import { chmodSync as chmodSync2, existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync12, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "node:fs";
6886
- import { join as join12, resolve as resolve4 } from "node:path";
7247
+ import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "node:fs";
7248
+ import { join as join13, resolve as resolve4 } from "node:path";
6887
7249
  var configDir2 = (home, env = process.env) => {
6888
7250
  const o2 = env.SLEDS_HOME?.trim();
6889
- return o2 ? resolve4(home, o2) : join12(home, ".config", "sleds");
7251
+ return o2 ? resolve4(home, o2) : join13(home, ".config", "sleds");
6890
7252
  };
6891
- var configPath2 = (home) => join12(configDir2(home), "config.json");
7253
+ var configPath2 = (home) => join13(configDir2(home), "config.json");
6892
7254
  var isObject12 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6893
7255
  var optString2 = (v) => typeof v === "string" ? v : undefined;
6894
7256
  var parsePriorEnv2 = (v) => {
@@ -6991,9 +7353,9 @@ var parseOrgInfo2 = (raw) => {
6991
7353
  };
6992
7354
  var readConfig2 = (home) => {
6993
7355
  const p2 = configPath2(home);
6994
- if (!existsSync15(p2))
7356
+ if (!existsSync16(p2))
6995
7357
  return {};
6996
- return parseConfig2(JSON.parse(readFileSync12(p2, "utf8")));
7358
+ return parseConfig2(JSON.parse(readFileSync13(p2, "utf8")));
6997
7359
  };
6998
7360
 
6999
7361
  // src/schedule.ts
@@ -7006,7 +7368,7 @@ var defaultExec = (cmd, args) => {
7006
7368
  };
7007
7369
 
7008
7370
  // src/settings.ts
7009
- import { dirname as dirname6, join as join13, resolve as resolve5 } from "node:path";
7371
+ import { dirname as dirname7, join as join14, resolve as resolve5 } from "node:path";
7010
7372
  var withSeatEnv2 = (command, home, env = process.env) => {
7011
7373
  const parts = [];
7012
7374
  const sh = env.SLEDS_HOME?.trim();
@@ -7019,6 +7381,15 @@ var withSeatEnv2 = (command, home, env = process.env) => {
7019
7381
  return parts.join(" ");
7020
7382
  };
7021
7383
  var shellWord2 = (v) => /^[\w./~-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
7384
+ var isObject13 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7385
+ var isSledsHookFor2 = (sub) => (entry) => {
7386
+ if (!isObject13(entry) || !Array.isArray(entry.hooks))
7387
+ return false;
7388
+ const re = new RegExp(`\\b${sub}\\b`);
7389
+ return entry.hooks.some((h) => isObject13(h) && h.type === "command" && typeof h.command === "string" && re.test(h.command));
7390
+ };
7391
+ var isSledsSessionHook2 = isSledsHookFor2("session-event");
7392
+ var isSledsStopHook2 = isSledsHookFor2("stop-event");
7022
7393
 
7023
7394
  // src/index.ts
7024
7395
  var USAGE = `usage: sleds <command> [options]
@@ -7072,6 +7443,10 @@ commands:
7072
7443
  statusline Print the one-line meter Claude Code shows (installed by init; 10s cache).
7073
7444
  hooks Register the Claude Code Notification hook for seat limits (init does this too).
7074
7445
  seat-event Claude Code hook: record a seat wall and say how to continue. Reads hook JSON on stdin.
7446
+ session-event Claude Code SessionStart hook: states the loop and the rows this machine still
7447
+ owes before the first turn. Reads hook JSON on stdin.
7448
+ stop-event Claude Code Stop hook: a session that estimated a row and posted no actual is asked
7449
+ once, then let go. Reads hook JSON on stdin.
7075
7450
  prompt-event Claude Code UserPromptSubmit hook: at 90 % of a seat bar the seat speaks up in the
7076
7451
  session (once per bar per reset); out of usage credits blocks the prompt once with the exits.
7077
7452
  spec-event Claude Code PostToolUse hook: when a spec-shaped markdown file is written, size it
@@ -7127,6 +7502,8 @@ var shellQuote = (s) => /^[\w./-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}
7127
7502
  var statusLineCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "statusline"].map(shellQuote).join(" "), home);
7128
7503
  var seatEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "seat-event"].map(shellQuote).join(" "), home);
7129
7504
  var promptEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "prompt-event"].map(shellQuote).join(" "), home);
7505
+ var sessionEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "session-event"].map(shellQuote).join(" "), home);
7506
+ var stopEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "stop-event"].map(shellQuote).join(" "), home);
7130
7507
  var specEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "spec-event"].map(shellQuote).join(" "), home);
7131
7508
  var main = async (argv) => {
7132
7509
  const { cmd, flags } = parseArgs(argv);
@@ -7199,7 +7576,9 @@ var main = async (argv) => {
7199
7576
  statusLineCommand: statusLineCommand(sledsArgv()),
7200
7577
  seatEventCommand: seatEventCommand(sledsArgv()),
7201
7578
  specEventCommand: specEventCommand(sledsArgv()),
7202
- promptEventCommand: promptEventCommand(sledsArgv())
7579
+ promptEventCommand: promptEventCommand(sledsArgv()),
7580
+ sessionEventCommand: sessionEventCommand(sledsArgv()),
7581
+ stopEventCommand: stopEventCommand(sledsArgv())
7203
7582
  });
7204
7583
  }
7205
7584
  case "hooks":
@@ -7207,7 +7586,9 @@ var main = async (argv) => {
7207
7586
  home,
7208
7587
  seatEventCommand: seatEventCommand(sledsArgv()),
7209
7588
  specEventCommand: specEventCommand(sledsArgv()),
7210
- promptEventCommand: promptEventCommand(sledsArgv())
7589
+ promptEventCommand: promptEventCommand(sledsArgv()),
7590
+ sessionEventCommand: sessionEventCommand(sledsArgv()),
7591
+ stopEventCommand: stopEventCommand(sledsArgv())
7211
7592
  });
7212
7593
  case "seat-event":
7213
7594
  return seatEvent({ home });
@@ -7215,6 +7596,10 @@ var main = async (argv) => {
7215
7596
  return specEvent({ home });
7216
7597
  case "prompt-event":
7217
7598
  return promptEvent({ home });
7599
+ case "session-event":
7600
+ return sessionEvent({ home });
7601
+ case "stop-event":
7602
+ return stopEvent({ home });
7218
7603
  case "on":
7219
7604
  return on({
7220
7605
  home,
@@ -7258,7 +7643,7 @@ ${renderHelpText(argv[1])}`);
7258
7643
  case "version":
7259
7644
  case "--version":
7260
7645
  case "-v":
7261
- console.log("sleds 0.1.22");
7646
+ console.log("sleds 0.1.23");
7262
7647
  return 0;
7263
7648
  default:
7264
7649
  console.error(`unknown command: ${cmd}
@@ -7289,6 +7674,8 @@ export {
7289
7674
  parseArgs,
7290
7675
  promptEventCommand,
7291
7676
  seatEventCommand,
7677
+ sessionEventCommand,
7292
7678
  specEventCommand,
7293
- statusLineCommand
7679
+ statusLineCommand,
7680
+ stopEventCommand
7294
7681
  };