tldr-experts 0.31.0 → 0.32.0

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.
@@ -5,11 +5,13 @@ import {
5
5
  DEFAULT_ON_HOST_TOKENS_EXCEED,
6
6
  WAVES_FILE,
7
7
  asRunBudget,
8
+ buildProgress,
8
9
  nowRfc3339,
9
10
  spentBasis,
11
+ storyDependsOn,
10
12
  tallyOf,
11
13
  validateRunBudget
12
- } from "./chunk-me0kzc6k.js";
14
+ } from "./chunk-5at77jx8.js";
13
15
  import {
14
16
  EventLog,
15
17
  OUTCOME_NOT_RECORDED,
@@ -47,13 +49,16 @@ import {
47
49
  FRAMEWORK_ROOT,
48
50
  MAX_ITEM_CHARS,
49
51
  PLAN_STATUSES,
52
+ PROJECT_FRAMEWORK_DIR,
50
53
  listRunDirs,
54
+ parseHandoff,
51
55
  parseYaml,
52
- parseYamlRepairing
56
+ parseYamlRepairing,
57
+ withoutSrcToken
53
58
  } from "./chunk-9bd6b3xf.js";
54
59
 
55
60
  // src/core/statusline/runSnapshot.ts
56
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
61
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
57
62
  import { join as join4 } from "node:path";
58
63
 
59
64
  // src/core/run/RunStore.ts
@@ -653,23 +658,123 @@ var PLAN_SHAPE_RULES = [
653
658
  {
654
659
  issue: "#319",
655
660
  text: "**End-to-end coverage is not the last story.** Either a test-harness story runs in W1 and later stories " + "depend on it and run against it, or each UI or feature story carries the e2e command in its own dod. A " + "final e2e story that depends on everything is the story never reached. The `plan` check names a command " + "some stories of one epic and repo carry and a sibling does not — an advisory: say in that story why not."
661
+ },
662
+ {
663
+ issue: "#365",
664
+ text: "**A story that adds an invariant over EXISTING data lands in the same story as, or in a wave after, the " + "story that makes existing rows and fixtures satisfy it.** A check constraint, a `NOT NULL`, a required " + "field or a validation rule written ahead of the story that populates what it enforces makes that earlier " + "story's own dod structurally red — every fixture the enforcing story's own tests run against still lacks " + "the value, and two stories in the SAME wave run in separate worktrees that never see each other's writes, " + "so 'later wave' is the only way one story's data can satisfy another's rule. If the invariant must land " + "first, write it NON-ENFORCING — nullable, consistency-only, no constraint — and let the later story " + "tighten it once the data exists. The `plan` check refuses a story whose acceptance or test plan names an " + "enforcement over a field a later story's acceptance or test plan names populating."
656
665
  }
657
666
  ];
658
667
 
659
668
  // src/core/run/runOutcome.ts
669
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
660
670
  import { join as join3 } from "node:path";
661
671
 
662
672
  // src/core/build/handoff.ts
663
673
  var LINE_BREAK_MARK = "⏎";
664
674
  var LINE_BREAK_NOTE = `\`${LINE_BREAK_MARK}\` marks a line break inside a quoted command; the review log beside this file ` + "keeps the command verbatim.";
665
675
  var FINDING_STATUS_RE = new RegExp(`—\\s+(${PLAN_STATUSES.join("|")})\\s+—`);
676
+ function findingStatus(bullet) {
677
+ const found = FINDING_STATUS_RE.exec(bullet)?.[1];
678
+ return found === undefined ? null : found;
679
+ }
680
+ var FINDING_ID_RE = /^\s*(?:[-*]\s+)?(\S+)\s+·\s/;
681
+ function findingId(bullet) {
682
+ return FINDING_ID_RE.exec(bullet)?.[1] ?? null;
683
+ }
666
684
  var FINDING_REASON_RE = new RegExp(`—\\s+(?:${PLAN_STATUSES.join("|")})\\s+—\\s+[^:]*:\\s*(\\S.*)$`);
685
+ function findingReason(bullet) {
686
+ const found = FINDING_REASON_RE.exec(withoutSrcToken(bullet).trimEnd())?.[1];
687
+ return found === undefined ? null : found.trim();
688
+ }
667
689
 
668
690
  // src/core/build/dependencyHold.ts
691
+ function dependencyIsPending(status) {
692
+ return status === "review" || status === "in_progress";
693
+ }
694
+ function decidingHold(holds) {
695
+ return holds.find((hold) => !dependencyIsPending(hold.status)) ?? holds[0] ?? null;
696
+ }
697
+ function dependencyWaitReason(held) {
698
+ return `it waits on dependency ${held.id}, which is \`${held.status}\` and is offered again by the next ` + `invocation; ${held.id} \`done\` is what releases it, and no reopen is owed`;
699
+ }
669
700
  var NOT_A_HOLD_REASON = "its block is not a dependency hold — a reviewer, a DoD or a developer recorded it, " + "and reopening a dependency does not answer that; `tldrx story reopen <id>` does";
670
701
 
671
702
  // src/core/run/runOutcome.ts
672
703
  var BUILD_HANDOFF = join3(BUILD_PHASE, "handoff.md");
704
+ var FINDINGS = "Findings";
705
+ var REASON_NOT_RECORDED = "no reason recorded — `04-build/handoff.md` carries no `## Findings` bullet for it";
706
+ function storiesView(runDir) {
707
+ const progress = buildProgress(runDir);
708
+ if (progress === null)
709
+ return null;
710
+ const stories = progress.waves.flatMap((wave) => wave.stories);
711
+ const counts = {
712
+ total: stories.length,
713
+ done: stories.filter((s) => s.status === "done").length,
714
+ in_progress: stories.filter((s) => s.status === "in_progress").length,
715
+ review: stories.filter((s) => s.status === "review").length,
716
+ blocked: stories.filter((s) => s.status === "blocked").length,
717
+ todo: stories.filter((s) => s.status === "todo").length
718
+ };
719
+ const reasons = blockedReasons(runDir);
720
+ const statusById = new Map(stories.map((story) => [story.id, story.status]));
721
+ const unfinished = stories.filter((story) => story.status !== "done").map((story) => {
722
+ const waitingOn = story.status === "todo" && !progress.implicit ? pendingHold(storyDependsOn(runDir, story.id), statusById) : null;
723
+ return {
724
+ id: story.id,
725
+ status: story.status,
726
+ reason: reasons.get(story.id) ?? REASON_NOT_RECORDED,
727
+ ...waitingOn === null ? {} : { waitingOn }
728
+ };
729
+ });
730
+ const waiting = unfinished.find((story) => story.waitingOn !== undefined);
731
+ return {
732
+ counts,
733
+ unfinished,
734
+ firstBlocked: unfinished.find((story) => story.status === "blocked") ?? null,
735
+ firstWaiting: waiting?.waitingOn === undefined ? null : { id: waiting.id, waitingOn: waiting.waitingOn, reason: dependencyWaitReason(waiting.waitingOn) }
736
+ };
737
+ }
738
+ function pendingHold(dependsOn, statusById) {
739
+ const holds = [];
740
+ for (const id of dependsOn) {
741
+ const status = statusById.get(id);
742
+ if (status === undefined || status === "done")
743
+ continue;
744
+ holds.push({ id, status });
745
+ }
746
+ const deciding = decidingHold(holds);
747
+ return deciding !== null && dependencyIsPending(deciding.status) ? deciding : null;
748
+ }
749
+ function blockedReasons(runDir) {
750
+ const out = new Map;
751
+ const path = join3(runDir, BUILD_HANDOFF);
752
+ if (!existsSync2(path))
753
+ return out;
754
+ let text;
755
+ try {
756
+ text = readFileSync3(path, "utf8");
757
+ } catch {
758
+ return out;
759
+ }
760
+ let bullets;
761
+ try {
762
+ bullets = parseHandoff(text).sections.find((section) => section.name === FINDINGS)?.bullets.map((b) => b.text) ?? [];
763
+ } catch {
764
+ return out;
765
+ }
766
+ for (const bullet of bullets) {
767
+ if (findingStatus(bullet) === "done")
768
+ continue;
769
+ const id = findingId(bullet);
770
+ const reason = findingReason(bullet);
771
+ if (id === null || reason === null)
772
+ continue;
773
+ if (!out.has(id))
774
+ out.set(id, reason);
775
+ }
776
+ return out;
777
+ }
673
778
  var NO_BUILD_PHASE = "this run's workflow has no Build phase, so no story could be delivered";
674
779
  function describeRunOutcome(outcome) {
675
780
  if (outcome === undefined)
@@ -689,6 +794,7 @@ function statusWithOutcome(status, outcome) {
689
794
 
690
795
  // src/core/run/autoGate.ts
691
796
  var AUTO_GATE_ACTOR = "auto";
797
+ var RATE_LIMIT_RESUME_NOTE_PREFIX = "run auto resumed this stage: its auto gate was held by an unfinished " + "story a rate-limit warning parked, and the provider's own clock has since cleared it.";
692
798
  var RETRYABLE_CONDITIONS = new Set(["checks", "claim-sources"]);
693
799
  var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
694
800
  var HELD_SEPARATOR = " · ";
@@ -725,7 +831,15 @@ function describeGateSignature(sig) {
725
831
  }
726
832
 
727
833
  // src/core/facilitator/skipIf.ts
728
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
834
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4 } from "node:fs";
835
+
836
+ // src/core/facilitator/seedSolution.ts
837
+ var WHAT_STAGE_OWN_INPUT_PREFIXES = [
838
+ `${PROJECT_FRAMEWORK_DIR}/memory/`,
839
+ `${PROJECT_FRAMEWORK_DIR}/map/`
840
+ ];
841
+
842
+ // src/core/facilitator/skipIf.ts
729
843
  function phaseDirs(runDir) {
730
844
  try {
731
845
  return readdirSync(runDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^0[1-9]-/.test(entry.name)).map((entry) => entry.name).sort();
@@ -734,19 +848,19 @@ function phaseDirs(runDir) {
734
848
  }
735
849
  }
736
850
  function openQuestionIds(path) {
737
- if (!existsSync2(path))
851
+ if (!existsSync3(path))
738
852
  return [];
739
853
  try {
740
- return openBlocks(parseQuestions(readFileSync3(path, "utf8")).blocks).map((block) => block.id);
854
+ return openBlocks(parseQuestions(readFileSync4(path, "utf8")).blocks).map((block) => block.id);
741
855
  } catch {
742
856
  return [];
743
857
  }
744
858
  }
745
859
  function blockingQuestionIds(path) {
746
- if (!existsSync2(path))
860
+ if (!existsSync3(path))
747
861
  return [];
748
862
  try {
749
- return openBlocks(parseQuestions(readFileSync3(path, "utf8")).blocks).filter((block) => !isAdvisory(block)).map((block) => block.id);
863
+ return openBlocks(parseQuestions(readFileSync4(path, "utf8")).blocks).filter((block) => !isAdvisory(block)).map((block) => block.id);
750
864
  } catch {
751
865
  return [];
752
866
  }
@@ -828,10 +942,10 @@ function fromTolerantRead(root) {
828
942
  }
829
943
  function readBudgetMirror(runDir) {
830
944
  const path = join4(runDir, "run.yml");
831
- if (!existsSync3(path))
945
+ if (!existsSync4(path))
832
946
  return { ceiling_usd: 0, spent_usd: 0 };
833
947
  try {
834
- const text = readFileSync4(path, "utf8");
948
+ const text = readFileSync5(path, "utf8");
835
949
  return {
836
950
  ceiling_usd: numberAfter(text, "ceiling_usd"),
837
951
  spent_usd: numberAfter(text, "spent_usd")
@@ -848,16 +962,306 @@ function numberAfter(text, key) {
848
962
  // src/core/run/waiting.ts
849
963
  import { basename as basename2, join as join6 } from "node:path";
850
964
 
965
+ // src/core/facilitator/agentEvents.ts
966
+ function rateLimitLine(frame) {
967
+ const parts = [frame.status];
968
+ if (frame.utilization !== null)
969
+ parts.push(`${String(Math.round(frame.utilization * 100))}% of`);
970
+ parts.push(frame.window === null ? "the provider's window" : `the ${frame.window} window`);
971
+ if (frame.resetsAt !== null)
972
+ parts.push(`· resets ${new Date(frame.resetsAt * 1000).toISOString()}`);
973
+ return parts.join(" ");
974
+ }
975
+ var STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
976
+ class AgentStream {
977
+ openTools = new Map;
978
+ seenQuestions = new Set;
979
+ provider;
980
+ codexStructured = null;
981
+ questionCount = 0;
982
+ constructor(provider = "claude") {
983
+ this.provider = provider;
984
+ }
985
+ push(line) {
986
+ const doc = parseLine(line);
987
+ if (doc === null)
988
+ return [];
989
+ if (this.provider === "codex")
990
+ return this.codex(doc);
991
+ switch (doc.type) {
992
+ case "system":
993
+ return this.system(doc);
994
+ case "assistant":
995
+ return this.assistant(doc);
996
+ case "user":
997
+ return this.user(doc);
998
+ case "result":
999
+ return this.result(doc);
1000
+ case "rate_limit_event":
1001
+ return this.rateLimit(doc);
1002
+ default:
1003
+ return [];
1004
+ }
1005
+ }
1006
+ codex(doc) {
1007
+ if (doc.type === "thread.started") {
1008
+ return [{ kind: "start", model: null, sessionId: str(doc.thread_id) }];
1009
+ }
1010
+ if (doc.type === "turn.failed") {
1011
+ return [{ kind: "error", message: str(obj(doc.error)?.message) ?? "the Codex turn failed" }];
1012
+ }
1013
+ if (doc.type === "turn.completed") {
1014
+ const usage = obj(doc.usage);
1015
+ const structured = this.codexStructured;
1016
+ return [
1017
+ {
1018
+ kind: "cost",
1019
+ usd: null,
1020
+ inputTokens: num(usage?.input_tokens),
1021
+ outputTokens: num(usage?.output_tokens),
1022
+ cacheCreationTokens: num(usage?.cache_write_input_tokens),
1023
+ cacheReadTokens: num(usage?.cached_input_tokens)
1024
+ },
1025
+ ...this.questions(obj(structured)?.questions_asked),
1026
+ { kind: "done", ok: true, structured, costUsd: 0 }
1027
+ ];
1028
+ }
1029
+ if (doc.type !== "item.started" && doc.type !== "item.completed")
1030
+ return [];
1031
+ const item = obj(doc.item);
1032
+ if (item === null)
1033
+ return [];
1034
+ const id = str(item.id);
1035
+ if (item.type === "agent_message") {
1036
+ const message = str(item.text);
1037
+ if (message === null)
1038
+ return [];
1039
+ const structured = parseLine(message);
1040
+ if (structured !== null)
1041
+ this.codexStructured = structured;
1042
+ return [{ kind: "text", text: message }];
1043
+ }
1044
+ if (item.type !== "command_execution")
1045
+ return [];
1046
+ const command = str(item.command);
1047
+ if (doc.type === "item.started") {
1048
+ return [{ kind: "tool", id, name: "Bash", target: command }];
1049
+ }
1050
+ return [{
1051
+ kind: "tool-done",
1052
+ id,
1053
+ name: "Bash",
1054
+ ok: item.status === "completed" && (item.exit_code === 0 || item.exit_code === null),
1055
+ ms: null,
1056
+ countsAsRead: true
1057
+ }];
1058
+ }
1059
+ system(doc) {
1060
+ if (doc.subtype !== "init")
1061
+ return [];
1062
+ return [{ kind: "start", model: str(doc.model), sessionId: str(doc.session_id) }];
1063
+ }
1064
+ rateLimit(doc) {
1065
+ const info = obj(doc.rate_limit_info);
1066
+ const status = str(info?.status);
1067
+ if (info === null || status === null)
1068
+ return [];
1069
+ const window = str(info.rateLimitType);
1070
+ const named = window === null ? null : obj(obj(info.unifiedWindows)?.[window]);
1071
+ return [{
1072
+ kind: "rate-limit",
1073
+ status,
1074
+ window,
1075
+ utilization: maybeNum(info.utilization) ?? maybeNum(named?.utilization),
1076
+ resetsAt: maybeNum(info.resetsAt) ?? maybeNum(named?.resetsAt)
1077
+ }];
1078
+ }
1079
+ assistant(doc) {
1080
+ const message = obj(doc.message);
1081
+ const blocks = Array.isArray(message?.content) ? message.content : [];
1082
+ const at = millis(doc.timestamp);
1083
+ const events = [];
1084
+ for (const raw of blocks) {
1085
+ const block = obj(raw);
1086
+ if (block === null)
1087
+ continue;
1088
+ if (block.type === "text") {
1089
+ const text = str(block.text);
1090
+ if (text !== null && text.trim() !== "")
1091
+ events.push({ kind: "text", text: text.trim() });
1092
+ continue;
1093
+ }
1094
+ if (block.type !== "tool_use")
1095
+ continue;
1096
+ const name = str(block.name) ?? "tool";
1097
+ const id = str(block.id);
1098
+ if (name === STRUCTURED_OUTPUT_TOOL) {
1099
+ events.push(...this.questions(obj(block.input)?.questions_asked));
1100
+ continue;
1101
+ }
1102
+ if (id !== null)
1103
+ this.openTools.set(id, { name, at });
1104
+ events.push({ kind: "tool", id, name, target: toolTarget(name, obj(block.input)) });
1105
+ }
1106
+ const usage = obj(message?.usage);
1107
+ if (usage !== null) {
1108
+ events.push({
1109
+ kind: "cost",
1110
+ usd: null,
1111
+ inputTokens: num(usage.input_tokens),
1112
+ outputTokens: num(usage.output_tokens),
1113
+ cacheCreationTokens: num(usage.cache_creation_input_tokens),
1114
+ cacheReadTokens: num(usage.cache_read_input_tokens)
1115
+ });
1116
+ }
1117
+ return events;
1118
+ }
1119
+ user(doc) {
1120
+ const message = obj(doc.message);
1121
+ const blocks = Array.isArray(message?.content) ? message.content : [];
1122
+ const at = millis(doc.timestamp);
1123
+ const events = [];
1124
+ for (const raw of blocks) {
1125
+ const block = obj(raw);
1126
+ if (block === null || block.type !== "tool_result")
1127
+ continue;
1128
+ const id = str(block.tool_use_id);
1129
+ const open = id === null ? undefined : this.openTools.get(id);
1130
+ if (id !== null)
1131
+ this.openTools.delete(id);
1132
+ if (open === undefined)
1133
+ continue;
1134
+ events.push({
1135
+ kind: "tool-done",
1136
+ id,
1137
+ name: open.name,
1138
+ ok: block.is_error !== true,
1139
+ ms: at === null || open.at === null ? null : Math.max(0, at - open.at)
1140
+ });
1141
+ }
1142
+ return events;
1143
+ }
1144
+ result(doc) {
1145
+ const events = [];
1146
+ const structured = doc.structured_output ?? null;
1147
+ events.push(...this.questions(obj(structured)?.questions_asked));
1148
+ const cost = typeof doc.total_cost_usd === "number" ? doc.total_cost_usd : 0;
1149
+ const usage = obj(doc.usage);
1150
+ events.push({
1151
+ kind: "cost",
1152
+ usd: cost,
1153
+ inputTokens: num(usage?.input_tokens),
1154
+ outputTokens: num(usage?.output_tokens),
1155
+ cacheCreationTokens: num(usage?.cache_creation_input_tokens),
1156
+ cacheReadTokens: num(usage?.cache_read_input_tokens)
1157
+ });
1158
+ const ok = doc.is_error !== true;
1159
+ if (!ok) {
1160
+ const errors = Array.isArray(doc.errors) ? doc.errors : [];
1161
+ const first = errors.find((entry) => typeof entry === "string");
1162
+ events.push({ kind: "error", message: first ?? str(doc.subtype) ?? "the sub-agent reported an error" });
1163
+ }
1164
+ events.push({ kind: "done", ok, structured, costUsd: cost });
1165
+ return events;
1166
+ }
1167
+ questions(value) {
1168
+ if (!Array.isArray(value))
1169
+ return [];
1170
+ const events = [];
1171
+ for (const entry of value) {
1172
+ if (typeof entry !== "string" || entry.trim() === "")
1173
+ continue;
1174
+ const text = entry.trim();
1175
+ if (this.seenQuestions.has(text))
1176
+ continue;
1177
+ this.seenQuestions.add(text);
1178
+ this.questionCount += 1;
1179
+ events.push({ kind: "question", index: this.questionCount, text });
1180
+ }
1181
+ return events;
1182
+ }
1183
+ }
1184
+ function toolTarget(name, input) {
1185
+ if (input === null)
1186
+ return null;
1187
+ const named = (() => {
1188
+ switch (name) {
1189
+ case "Read":
1190
+ case "Write":
1191
+ case "NotebookEdit":
1192
+ return input.file_path ?? input.notebook_path;
1193
+ case "Edit":
1194
+ case "MultiEdit":
1195
+ return input.file_path;
1196
+ case "Bash":
1197
+ case "BashOutput":
1198
+ return input.command;
1199
+ case "Grep":
1200
+ case "Glob":
1201
+ return input.pattern;
1202
+ case "WebFetch":
1203
+ return input.url;
1204
+ case "WebSearch":
1205
+ return input.query;
1206
+ case "Task":
1207
+ case "Agent":
1208
+ return input.description;
1209
+ case "TodoWrite":
1210
+ return null;
1211
+ default:
1212
+ return null;
1213
+ }
1214
+ })();
1215
+ if (typeof named === "string" && named !== "")
1216
+ return named;
1217
+ if (named !== null && named !== undefined)
1218
+ return null;
1219
+ for (const value of Object.values(input)) {
1220
+ if (typeof value === "string" && value !== "")
1221
+ return value;
1222
+ }
1223
+ return null;
1224
+ }
1225
+ function parseLine(line) {
1226
+ const trimmed = line.trim();
1227
+ if (trimmed === "" || !trimmed.startsWith("{"))
1228
+ return null;
1229
+ try {
1230
+ const doc = JSON.parse(trimmed);
1231
+ return typeof doc === "object" && doc !== null && !Array.isArray(doc) ? doc : null;
1232
+ } catch {
1233
+ return null;
1234
+ }
1235
+ }
1236
+ function obj(value) {
1237
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1238
+ }
1239
+ function str(value) {
1240
+ return typeof value === "string" && value !== "" ? value : null;
1241
+ }
1242
+ function num(value) {
1243
+ return typeof value === "number" ? value : 0;
1244
+ }
1245
+ function maybeNum(value) {
1246
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1247
+ }
1248
+ function millis(value) {
1249
+ if (typeof value !== "string")
1250
+ return null;
1251
+ const at = Date.parse(value);
1252
+ return Number.isFinite(at) ? at : null;
1253
+ }
1254
+
851
1255
  // src/core/run/prepared.ts
852
- import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "node:fs";
1256
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "node:fs";
853
1257
  import { join as join5 } from "node:path";
854
1258
  var PENDING_JSON = "pending.json";
855
1259
  function preparedBundles(runDir, stageId) {
856
1260
  const dir = join5(runDir, ".agent", stageId);
857
- if (!existsSync4(dir))
1261
+ if (!existsSync5(dir))
858
1262
  return [];
859
1263
  const found = [];
860
- if (existsSync4(join5(dir, PENDING_JSON)))
1264
+ if (existsSync5(join5(dir, PENDING_JSON)))
861
1265
  found.push(dir);
862
1266
  let entries;
863
1267
  try {
@@ -873,7 +1277,7 @@ function preparedBundles(runDir, stageId) {
873
1277
  } catch {
874
1278
  continue;
875
1279
  }
876
- if (existsSync4(join5(child, PENDING_JSON)))
1280
+ if (existsSync5(join5(child, PENDING_JSON)))
877
1281
  found.push(child);
878
1282
  }
879
1283
  return found;
@@ -900,6 +1304,52 @@ function startedHeadless(runDir, stageId) {
900
1304
  return lastStageStartMode(runDir, stageId) === "headless";
901
1305
  }
902
1306
 
1307
+ // src/core/run/rateLimitPark.ts
1308
+ function currentRateLimitPark(runDir, stageId) {
1309
+ let events;
1310
+ try {
1311
+ events = EventLog.forRun(runDir).readAll().events;
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ let stageStartIndex = -1;
1316
+ for (let index = events.length - 1;index >= 0; index -= 1) {
1317
+ const event = events[index];
1318
+ if (event !== undefined && event.stage === stageId && event.type === "stage.started") {
1319
+ stageStartIndex = index;
1320
+ break;
1321
+ }
1322
+ }
1323
+ if (stageStartIndex === -1)
1324
+ return null;
1325
+ let park = null;
1326
+ for (let index = events.length - 1;index >= stageStartIndex; index -= 1) {
1327
+ const event = events[index];
1328
+ if (event === undefined || event.type !== "agent.rate_limited" || event.stage !== stageId)
1329
+ continue;
1330
+ const payload = event.payload;
1331
+ const parked = payload.parked;
1332
+ if (typeof parked !== "string" || parked === "")
1333
+ continue;
1334
+ park = {
1335
+ status: typeof payload.status === "string" ? payload.status : "",
1336
+ window: typeof payload.window === "string" ? payload.window : null,
1337
+ utilization: typeof payload.utilization === "number" ? payload.utilization : null,
1338
+ resetsAt: typeof payload.resets_at === "number" ? payload.resets_at : null,
1339
+ parked
1340
+ };
1341
+ break;
1342
+ }
1343
+ if (park === null)
1344
+ return null;
1345
+ const view = storiesView(runDir);
1346
+ if (view === null || view.unfinished.length === 0)
1347
+ return null;
1348
+ if (view.unfinished.some((story) => story.status !== "todo"))
1349
+ return null;
1350
+ return park;
1351
+ }
1352
+
903
1353
  // src/core/run/waiting.ts
904
1354
  var MOVABLE_KINDS = [
905
1355
  "gate",
@@ -949,6 +1399,16 @@ function waitingFor(run, runDir) {
949
1399
  switch (entry.stage.status) {
950
1400
  case "awaiting_gate": {
951
1401
  const held = heldByNote(entry.stage.gate?.note ?? "");
1402
+ if (held.length === 1 && held[0] === "stories") {
1403
+ const park = currentRateLimitPark(runDir, entry.stage.id);
1404
+ if (park !== null) {
1405
+ return {
1406
+ kind: "gate",
1407
+ message: `gate on ${entry.phase.id}/${entry.stage.id} — parked by a rate-limit warning ` + `(${rateLimitLine(park)})` + (park.resetsAt === null ? " — it stated no reset instant, so nothing here knows when it resumes" : `, resumes automatically at ${new Date(park.resetsAt * 1000).toISOString()}`),
1408
+ questions: open
1409
+ };
1410
+ }
1411
+ }
952
1412
  return {
953
1413
  kind: "gate",
954
1414
  message: `gate on ${entry.phase.id}/${entry.stage.id}` + (held.length === 0 ? "" : ` — held by ${held.join(", ")}`) + ` — \`tldrx approve\` or \`tldrx reject --note "…"\``,
@@ -20,7 +20,7 @@ import {
20
20
  runSnapshot,
21
21
  statusWithOutcome,
22
22
  whatIsWaiting
23
- } from "./chunk-3r0hhgcf.js";
23
+ } from "./chunk-whvae1vw.js";
24
24
  import"./chunk-ae6bkfs5.js";
25
25
  import {
26
26
  expertsDir,
@@ -28,7 +28,7 @@ import {
28
28
  pathsIntersect,
29
29
  readExpertDomain,
30
30
  stackExpertNames
31
- } from "./chunk-me0kzc6k.js";
31
+ } from "./chunk-5at77jx8.js";
32
32
  import {
33
33
  isFinished
34
34
  } from "./chunk-akemhezh.js";
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-3r0hhgcf.js";
5
+ } from "./chunk-whvae1vw.js";
6
6
  import"./chunk-ae6bkfs5.js";
7
- import"./chunk-me0kzc6k.js";
7
+ import"./chunk-5at77jx8.js";
8
8
  import"./chunk-akemhezh.js";
9
9
  import"./chunk-8ds2ret8.js";
10
10
  import"./chunk-yyhwf3bp.js";