wicked-crew 0.16.4 → 0.16.5

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 (150) hide show
  1. package/dist/api/ask-paths.d.ts +5 -2
  2. package/dist/api/ask-paths.d.ts.map +1 -1
  3. package/dist/api/ask-paths.js +6 -3
  4. package/dist/api/ask-paths.js.map +1 -1
  5. package/dist/api/chat-scope.d.ts +8 -3
  6. package/dist/api/chat-scope.d.ts.map +1 -1
  7. package/dist/api/chat-scope.js +17 -5
  8. package/dist/api/chat-scope.js.map +1 -1
  9. package/dist/api/chat-workflow.d.ts +60 -0
  10. package/dist/api/chat-workflow.d.ts.map +1 -0
  11. package/dist/api/chat-workflow.js +80 -0
  12. package/dist/api/chat-workflow.js.map +1 -0
  13. package/dist/api/endpoint-manifest-live.d.ts.map +1 -1
  14. package/dist/api/endpoint-manifest-live.js +2 -0
  15. package/dist/api/endpoint-manifest-live.js.map +1 -1
  16. package/dist/api/needs.d.ts +116 -0
  17. package/dist/api/needs.d.ts.map +1 -0
  18. package/dist/api/needs.js +618 -0
  19. package/dist/api/needs.js.map +1 -0
  20. package/dist/api/routes.d.ts +23 -1
  21. package/dist/api/routes.d.ts.map +1 -1
  22. package/dist/api/routes.js +288 -23
  23. package/dist/api/routes.js.map +1 -1
  24. package/dist/api/run-timing-index.d.ts +6 -0
  25. package/dist/api/run-timing-index.d.ts.map +1 -1
  26. package/dist/api/run-timing-index.js +18 -0
  27. package/dist/api/run-timing-index.js.map +1 -1
  28. package/dist/api/server.d.ts +8 -0
  29. package/dist/api/server.d.ts.map +1 -1
  30. package/dist/api/server.js +146 -1
  31. package/dist/api/server.js.map +1 -1
  32. package/dist/api/system-snapshot.d.ts +7 -15
  33. package/dist/api/system-snapshot.d.ts.map +1 -1
  34. package/dist/api/system-snapshot.js +7 -115
  35. package/dist/api/system-snapshot.js.map +1 -1
  36. package/dist/core/run-identity.d.ts.map +1 -1
  37. package/dist/core/run-identity.js +3 -0
  38. package/dist/core/run-identity.js.map +1 -1
  39. package/dist/evals/capture.d.ts +93 -0
  40. package/dist/evals/capture.d.ts.map +1 -0
  41. package/dist/evals/capture.js +419 -0
  42. package/dist/evals/capture.js.map +1 -0
  43. package/dist/evals/replay.d.ts +33 -0
  44. package/dist/evals/replay.d.ts.map +1 -0
  45. package/dist/evals/replay.js +87 -0
  46. package/dist/evals/replay.js.map +1 -0
  47. package/dist/evals/routes.d.ts +48 -0
  48. package/dist/evals/routes.d.ts.map +1 -0
  49. package/dist/evals/routes.js +121 -0
  50. package/dist/evals/routes.js.map +1 -0
  51. package/dist/evals/run-cli.d.ts +31 -0
  52. package/dist/evals/run-cli.d.ts.map +1 -0
  53. package/dist/evals/run-cli.js +128 -0
  54. package/dist/evals/run-cli.js.map +1 -0
  55. package/dist/evals/runner.d.ts +34 -0
  56. package/dist/evals/runner.d.ts.map +1 -0
  57. package/dist/evals/runner.js +181 -0
  58. package/dist/evals/runner.js.map +1 -0
  59. package/dist/evals/score.d.ts +31 -0
  60. package/dist/evals/score.d.ts.map +1 -0
  61. package/dist/evals/score.js +191 -0
  62. package/dist/evals/score.js.map +1 -0
  63. package/dist/evals/seed-needs-you.d.ts +17 -0
  64. package/dist/evals/seed-needs-you.d.ts.map +1 -0
  65. package/dist/evals/seed-needs-you.js +57 -0
  66. package/dist/evals/seed-needs-you.js.map +1 -0
  67. package/dist/evals/seed-rig.d.ts +9 -0
  68. package/dist/evals/seed-rig.d.ts.map +1 -0
  69. package/dist/evals/seed-rig.js +156 -0
  70. package/dist/evals/seed-rig.js.map +1 -0
  71. package/dist/evals/stats.d.ts +31 -0
  72. package/dist/evals/stats.d.ts.map +1 -0
  73. package/dist/evals/stats.js +59 -0
  74. package/dist/evals/stats.js.map +1 -0
  75. package/dist/evals/steering.d.ts +9 -0
  76. package/dist/evals/steering.d.ts.map +1 -0
  77. package/dist/evals/steering.js +25 -0
  78. package/dist/evals/steering.js.map +1 -0
  79. package/dist/evals/store.d.ts +26 -0
  80. package/dist/evals/store.d.ts.map +1 -0
  81. package/dist/evals/store.js +96 -0
  82. package/dist/evals/store.js.map +1 -0
  83. package/dist/evals/synthetic.d.ts +17 -0
  84. package/dist/evals/synthetic.d.ts.map +1 -0
  85. package/dist/evals/synthetic.js +71 -0
  86. package/dist/evals/synthetic.js.map +1 -0
  87. package/dist/evals/types.d.ts +29 -0
  88. package/dist/evals/types.d.ts.map +1 -0
  89. package/dist/evals/types.js +25 -0
  90. package/dist/evals/types.js.map +1 -0
  91. package/dist/evals/workflow.d.ts +33 -0
  92. package/dist/evals/workflow.d.ts.map +1 -0
  93. package/dist/evals/workflow.js +66 -0
  94. package/dist/evals/workflow.js.map +1 -0
  95. package/dist/insights/analyze-cli.d.ts +18 -0
  96. package/dist/insights/analyze-cli.d.ts.map +1 -0
  97. package/dist/insights/analyze-cli.js +71 -0
  98. package/dist/insights/analyze-cli.js.map +1 -0
  99. package/dist/insights/analyze.d.ts +136 -0
  100. package/dist/insights/analyze.d.ts.map +1 -0
  101. package/dist/insights/analyze.js +358 -0
  102. package/dist/insights/analyze.js.map +1 -0
  103. package/dist/insights/review-service.d.ts +127 -0
  104. package/dist/insights/review-service.d.ts.map +1 -0
  105. package/dist/insights/review-service.js +392 -0
  106. package/dist/insights/review-service.js.map +1 -0
  107. package/dist/insights/routes.d.ts +15 -0
  108. package/dist/insights/routes.d.ts.map +1 -0
  109. package/dist/insights/routes.js +66 -0
  110. package/dist/insights/routes.js.map +1 -0
  111. package/dist/insights/run-facts.d.ts +64 -0
  112. package/dist/insights/run-facts.d.ts.map +1 -0
  113. package/dist/insights/run-facts.js +152 -0
  114. package/dist/insights/run-facts.js.map +1 -0
  115. package/dist/insights/stats.d.ts +38 -0
  116. package/dist/insights/stats.d.ts.map +1 -0
  117. package/dist/insights/stats.js +102 -0
  118. package/dist/insights/stats.js.map +1 -0
  119. package/dist/insights/store.d.ts +89 -0
  120. package/dist/insights/store.d.ts.map +1 -0
  121. package/dist/insights/store.js +87 -0
  122. package/dist/insights/store.js.map +1 -0
  123. package/dist/insights/workflow.d.ts +22 -0
  124. package/dist/insights/workflow.d.ts.map +1 -0
  125. package/dist/insights/workflow.js +39 -0
  126. package/dist/insights/workflow.js.map +1 -0
  127. package/dist/qe/ledger.d.ts +15 -0
  128. package/dist/qe/ledger.d.ts.map +1 -1
  129. package/dist/qe/ledger.js +24 -0
  130. package/dist/qe/ledger.js.map +1 -1
  131. package/dist/schedules/routes.d.ts +48 -0
  132. package/dist/schedules/routes.d.ts.map +1 -0
  133. package/dist/schedules/routes.js +72 -0
  134. package/dist/schedules/routes.js.map +1 -0
  135. package/dist/schedules/scheduler.d.ts +58 -0
  136. package/dist/schedules/scheduler.d.ts.map +1 -0
  137. package/dist/schedules/scheduler.js +194 -0
  138. package/dist/schedules/scheduler.js.map +1 -0
  139. package/dist/schedules/store.d.ts +67 -0
  140. package/dist/schedules/store.d.ts.map +1 -0
  141. package/dist/schedules/store.js +148 -0
  142. package/dist/schedules/store.js.map +1 -0
  143. package/dist/team/routes.d.ts +10 -0
  144. package/dist/team/routes.d.ts.map +1 -1
  145. package/dist/team/routes.js +3 -0
  146. package/dist/team/routes.js.map +1 -1
  147. package/endpoint-manifest.json +132 -1
  148. package/package.json +4 -3
  149. package/workflows/review-recent-work.json +54 -0
  150. package/workflows/run-evals.json +29 -0
@@ -0,0 +1,618 @@
1
+ /**
2
+ * The one "needs you" rule (`GET /system.attention[]`, api-types 0.140.0) and its companion,
3
+ * `unresolved[]`.
4
+ *
5
+ * The operator's rule: anything that is work stuck (the system slowing or preventing it, work that
6
+ * cannot move) is counted, prioritized by impact. So `attention[]` holds one item per ROOT CAUSE
7
+ * holding work up right now:
8
+ *
9
+ * - `prevents`: work cannot move until a person acts (an open gate, a stalled run, a recent
10
+ * failure, a down component, a seat gap with no alternative);
11
+ * - `slows`: the system itself slows work (host load).
12
+ *
13
+ * One cause that holds several runs is ONE item, naming them in `blockedRunIds`. A merge needs
14
+ * evidence, never a similar label: the same repo failing the same check on a base that fails it
15
+ * too (plus the run whose stated problem IS that red base, when it waits at a gate: its gate is the
16
+ * item's action); the same down component a held run's denial names; the same seats leaving runs
17
+ * with no distinct judge. Seat problems fold into the run item they block.
18
+ *
19
+ * What is NOT stuck never counts, and is never hidden either (operator, 2026-10-27: "don't let
20
+ * the system go green with stale jobs"): superseded runs, failed runs older than 7 days and asks
21
+ * parked at their turn gate past a day move to `unresolved[]`, each with what it cost and how to
22
+ * resolve it. Answered asks are not stuck. Pending proposals are `toReview`.
23
+ *
24
+ * Order (DEFINITION §4): runs held, then prevents before slows, then a component before one run's own
25
+ * cause (a component also holds every new run), then the longest wait (the gate's arrival, never the
26
+ * run's launch), then the units left.
27
+ *
28
+ * Pure: every input is a reading the route already made.
29
+ */
30
+ const DAY_MS = 86_400_000;
31
+ /** A failed run launched longer ago than this is history to resolve, not a need. */
32
+ export const FAILED_FRESH_MS = 7 * DAY_MS;
33
+ /** An ask parked at its turn gate longer than this is unresolved work. */
34
+ export const PARKED_AFTER_MS = DAY_MS;
35
+ /** The live components whose `down` (or silence) holds every live run. */
36
+ const WORK_PATH = new Set(['daemon', 'gateway', 'broker']);
37
+ /** What `down` means in words for the rows where "down" reads oddly. */
38
+ const DOWN_WORD = { load: 'saturated', disk: 'nearly full', state_home: 'refusing launches', skills: 'refusing launches' };
39
+ /** A held run's denial names the component it died on. */
40
+ const CITES = {
41
+ gateway: /\b(inference gateway|agw[:\s]|gateway)\b/i,
42
+ broker: /\bbroker\b/i,
43
+ };
44
+ /** A floor denial whose base fails the same check: the red-base evidence. */
45
+ const BASE_RED = /base_red_unseparated|the run base [0-9a-f]{6,40} fails this check too|base fails (this check )?too/i;
46
+ /** The first failing check a floor denial names (`test: exit 101`). */
47
+ const CHECK_NAME = /(?:^|[\s(\]—-])([A-Za-z][\w.-]*): exit -?\d+/;
48
+ /** A run whose own stated problem is that its repo's base is red (the fix the group waits on). */
49
+ const STATES_RED_BASE = /\bbase\b[^.\n]{0,40}\bred\b|\bred base\b/i;
50
+ /** A denial that IS the missing judge (the evaluator ran on the creator's seat, or none could judge). */
51
+ const JUDGE_DENIAL = /distinct[_ ]evaluator|no distinct judge|could not be judged|no (seat|judge) (can|could) judge/i;
52
+ /** A dead-seat denial: every eligible seat failed the unit. */
53
+ const DEAD_SEAT = /dead seat|every eligible seat has (now )?failed/i;
54
+ const short = (id) => id.slice(0, 8);
55
+ /** "1 job", "4 jobs": a count in words, never "job(s)". */
56
+ export const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
57
+ /** A job title as a person reads it: identifiers' underscores become spaces. */
58
+ const readable = (t) => t.replace(/_/g, ' ');
59
+ const isoOf = (ms) => (ms === null || !Number.isFinite(ms) ? null : new Date(ms).toISOString());
60
+ const secsToMs = (v) => (typeof v === 'number' && Number.isFinite(v) ? v * 1000 : null);
61
+ const msOf = (v) => {
62
+ if (typeof v !== 'string' || v === '')
63
+ return null;
64
+ const t = Date.parse(v);
65
+ return Number.isFinite(t) ? t : null;
66
+ };
67
+ const norm = (s) => (typeof s === 'string' ? s.trim().replace(/\s+/g, ' ').toLowerCase() : '');
68
+ const titleOf = (v) => (typeof v.session.title === 'string' && v.session.title !== '' ? v.session.title : v.session.problem.slice(0, 60));
69
+ const repoOf = (v) => (typeof v.session.repo_ref === 'string' && v.session.repo_ref !== '' ? v.session.repo_ref : null);
70
+ const unitsLeftOf = (v) => (v === undefined ? 0 : v.units.filter((u) => u.status !== 'done').length);
71
+ const isAskRun = (v) => v.session.ask_path === true || /^ask\//.test(v.session.workflow_id ?? '');
72
+ const DELIVERED = new Set(['delivered', 'pushed']);
73
+ /**
74
+ * Which run replaced this one, if any: a run naming it in `retry_of`, or a LATER run with the same
75
+ * repo, workflow and title that completed or delivered. A failed run with a retry is resolved by
76
+ * that retry (`retried`); anything else superseded is still the operator's to close.
77
+ */
78
+ export function supersession(views) {
79
+ const out = new Map();
80
+ for (const v of views) {
81
+ const r = v.session.retry_of;
82
+ // A cancelled retry carries nothing: the work it was meant to redo is still the original's.
83
+ if (v.session.status === 'cancelled')
84
+ continue;
85
+ if (typeof r === 'string' && r !== '' && r !== v.session.id && !out.has(r))
86
+ out.set(r, { by: v.session.id, retried: true });
87
+ }
88
+ const done = views
89
+ .filter((v) => v.session.status === 'completed' || DELIVERED.has(v.session.delivery ?? ''))
90
+ .map((v) => ({ v, at: secsToMs(v.session.created_at) }))
91
+ .filter((x) => x.at !== null)
92
+ .sort((a, b) => a.at - b.at);
93
+ for (const v of views) {
94
+ if (out.has(v.session.id))
95
+ continue;
96
+ const at = secsToMs(v.session.created_at);
97
+ const title = norm(titleOf(v));
98
+ if (at === null || title === '')
99
+ continue;
100
+ const same = done.filter((d) => d.v.session.id !== v.session.id &&
101
+ d.at > at &&
102
+ repoOf(d.v) === repoOf(v) &&
103
+ d.v.session.workflow_id === v.session.workflow_id &&
104
+ norm(titleOf(d.v)) === title);
105
+ // The completed successor first (a run that delivered and then failed is a weaker link).
106
+ const later = same.find((d) => d.v.session.status === 'completed') ?? same[0];
107
+ if (later !== undefined)
108
+ out.set(v.session.id, { by: later.v.session.id, retried: false });
109
+ }
110
+ return out;
111
+ }
112
+ /** An ask's TURN gate (answered by the chat's next message): a terminal gate with every unit done.
113
+ * An ask held at an escalation is a real gate (68c26704). */
114
+ export function isAnsweredAsk(v, run) {
115
+ if (v.session.status !== 'awaiting_human' || !isAskRun(v))
116
+ return false;
117
+ const kind = run?.gate?.kind ?? null;
118
+ if (kind === 'escalation')
119
+ return false;
120
+ return v.session.ask_turn === true && v.units.length > 0 && v.units.every((u) => u.status === 'done');
121
+ }
122
+ function gateTarget(run, runId) {
123
+ return {
124
+ kind: 'run',
125
+ runId,
126
+ ...(run?.gate?.ord != null ? { gateOrd: run.gate.ord } : {}),
127
+ ...(run?.gate?.interactionId != null ? { interactionId: run.gate.interactionId } : {}),
128
+ };
129
+ }
130
+ /** The unit a held run's gate is about, and the words it was denied with. */
131
+ function denialOf(v, run) {
132
+ if (v === undefined)
133
+ return '';
134
+ // A live run's cause is its CURRENT gate's denial, and only while it is held there: an earlier
135
+ // unit's old denial (retried, overridden) never re-labels a deliver gate or a stalled run.
136
+ if (run !== undefined) {
137
+ if (run.liveness !== 'held')
138
+ return '';
139
+ const ord = run.gate?.ord ?? null;
140
+ const at = ord !== null ? v.units.find((u) => u.ord === ord) : undefined;
141
+ if (at !== undefined)
142
+ return typeof at.denial_reason === 'string' ? at.denial_reason : '';
143
+ }
144
+ return v.units.map((u) => u.denial_reason ?? '').find((d) => d !== '') ?? '';
145
+ }
146
+ /** The seats in a run's pool that cannot take its work, in words ("pi signed out, copilot benched"). */
147
+ function seatsOut(v, clis) {
148
+ const pool = v?.session.clis !== undefined && v.session.clis.length > 0 ? new Set(v.session.clis.map((k) => k.split('#', 1)[0])) : null;
149
+ return clis
150
+ .filter((c) => (pool === null || pool.has(c.cli.split('#', 1)[0])) && (c.state === 'signed_out' || c.state === 'benched'))
151
+ .map((c) => `${c.cli} ${c.state === 'benched' ? `benched${c.benchedUntil !== undefined ? ` until ${c.benchedUntil.slice(11, 16)} UTC` : ''}` : 'signed out'}`);
152
+ }
153
+ /** Why a held run is held, in a person's words (the denial's category, never its raw text). */
154
+ function heldWords(denial, dead, noFailover) {
155
+ if (dead)
156
+ return noFailover ? 'No other helper can take over' : 'Its helper stopped unexpectedly';
157
+ if (JUDGE_DENIAL.test(denial))
158
+ return 'It needs a reviewer other than the helper that built it';
159
+ // Enforcement that never ran is not a refusal: the calls went unchecked (core#653).
160
+ if (/governance_unproven|gate-hook never ran|went unchecked/i.test(denial))
161
+ return 'Its tool calls ran without the required checks';
162
+ if (/boundary-deny|denied a tool[- ]call|tool call was refused|refused (a |the )?tool/i.test(denial))
163
+ return 'A tool call was refused by the job’s rules';
164
+ if (/input governance/i.test(denial))
165
+ return 'Its input was refused by the job’s rules';
166
+ if (/floor failed|repo checks/i.test(denial))
167
+ return 'Its checks failed';
168
+ return 'It stopped and waits for your decision';
169
+ }
170
+ /** What a gate that waits on you asks, in a person's words. */
171
+ function waitingWords(kind) {
172
+ if (kind === 'deliver')
173
+ return 'Ready to deliver';
174
+ if (kind === 'plan_approval' || kind === 'plan')
175
+ return 'Waiting for you to approve its plan';
176
+ if (kind === 'question')
177
+ return 'It asks you a question';
178
+ if (kind === 'terminal')
179
+ return 'Waiting for you to accept it';
180
+ return 'Waiting for your decision';
181
+ }
182
+ function runItem(m, clis) {
183
+ const r = m.run;
184
+ const v = m.view;
185
+ const title = readable(v !== undefined ? titleOf(v) : `job ${short(m.runId)}`);
186
+ if (r === undefined) {
187
+ // A recent failed run: it stopped, and a person decides what happens to the work.
188
+ return {
189
+ id: `run:${m.runId}`,
190
+ source: 'run',
191
+ kind: 'prevents',
192
+ title,
193
+ reason: 'It failed',
194
+ remedy: 'Open the job: retry it, or archive it with a reason.',
195
+ target: { kind: 'run', runId: m.runId },
196
+ action: { kind: 'open_run', label: 'Open the job' },
197
+ };
198
+ }
199
+ const denial = denialOf(v, r);
200
+ const dead = r.liveness === 'held' && DEAD_SEAT.test(denial);
201
+ const out = dead ? seatsOut(v, clis) : [];
202
+ const signIn = out.filter((x) => x.endsWith('signed out')).map((x) => x.split(' ')[0]);
203
+ return {
204
+ id: `run:${m.runId}`,
205
+ source: 'run',
206
+ kind: 'prevents',
207
+ title: dead ? `A job lost its helper: ${title}` : title,
208
+ reason: r.liveness === 'waiting_on_you' ? waitingWords(r.gate?.kind)
209
+ : r.liveness === 'held' ? heldWords(denial, dead, out.length > 0)
210
+ : `${r.reason.charAt(0).toUpperCase()}${r.reason.slice(1)}`,
211
+ remedy: r.liveness === 'waiting_on_you'
212
+ ? 'Answer the gate.'
213
+ : r.liveness === 'held'
214
+ ? dead
215
+ ? `Approve the retry${signIn.length > 0 ? `; sign in ${signIn.join(' and ')} so another helper can take over` : ''}${out.length > signIn.length ? ` (${out.filter((x) => !x.endsWith('signed out')).join(', ')})` : ''}.`
216
+ : 'Open the gate and decide: retry with a note, reassign, or stop.'
217
+ : 'Open the job: reassign the step or stop it. The watchdog re-dispatches a silent step on its own after its threshold.',
218
+ target: gateTarget(r, m.runId),
219
+ action: r.gate !== undefined ? { kind: 'open_gate', label: 'Open the gate' } : { kind: 'open_run', label: 'Open the job' },
220
+ };
221
+ }
222
+ export function buildNeeds(input) {
223
+ const { now } = input;
224
+ const byId = new Map(input.views.map((v) => [v.session.id, v]));
225
+ const runById = new Map(input.runs.map((r) => [r.runId, r]));
226
+ const sup = supersession(input.views);
227
+ const unresolved = [];
228
+ const members = [];
229
+ /** Runs that are not stuck (answered asks): no later pass (judge gaps, no worker, load) brings them back. */
230
+ const notStuck = new Set();
231
+ const eligible = (runId) => !notStuck.has(runId) && !sup.has(runId) && byId.get(runId)?.session.archived_at == null;
232
+ const costFor = (v) => {
233
+ const c = input.costOf?.(v.session.id);
234
+ const start = secsToMs(v.session.created_at);
235
+ const end = secsToMs(v.session.ended_at) ?? (v.session.status === 'failed' || v.session.status === 'completed' || v.session.status === 'cancelled' ? null : now);
236
+ return { costUsd: c?.costUsd ?? null, tokens: c?.tokens ?? null, wallMs: start !== null && end !== null ? Math.max(0, end - start) : null };
237
+ };
238
+ const pushUnresolved = (v, why, reason, since, actions, by) => {
239
+ unresolved.push({
240
+ id: `unresolved:${v.session.id}`,
241
+ runId: v.session.id,
242
+ title: readable(titleOf(v)),
243
+ repo: repoOf(v),
244
+ workflow: v.session.workflow_id ?? null,
245
+ status: v.session.status,
246
+ why,
247
+ reason,
248
+ ...(by !== undefined ? { supersededBy: by } : {}),
249
+ since: isoOf(since),
250
+ cost: costFor(v),
251
+ actions,
252
+ });
253
+ };
254
+ for (const v of input.views) {
255
+ if (v.session.archived_at != null)
256
+ continue; // resolved on the record
257
+ const id = v.session.id;
258
+ const run = runById.get(id);
259
+ const s = sup.get(id);
260
+ const status = v.session.status;
261
+ const gateAt = msOf(run?.gate?.receivedAt ?? null);
262
+ const endedAt = secsToMs(v.session.ended_at);
263
+ if (status === 'failed') {
264
+ if (s?.retried === true)
265
+ continue; // retried: the retry carries the work
266
+ // Aged from the FAILURE (its end); a run with no end on record is aged from its launch.
267
+ const failedAt = endedAt ?? secsToMs(v.session.created_at);
268
+ if (s !== undefined) {
269
+ const d = byId.get(s.by);
270
+ pushUnresolved(v, 'superseded', `failed; superseded by ${short(s.by)}, which ${d !== undefined && DELIVERED.has(d.session.delivery ?? '') ? 'delivered' : 'completed'}`, failedAt, ['close_superseded', 'archive'], s.by);
271
+ continue;
272
+ }
273
+ if (failedAt !== null && now - failedAt > FAILED_FRESH_MS) {
274
+ pushUnresolved(v, 'failed', `failed ${plural(Math.round((now - failedAt) / DAY_MS), 'day')} ago and was never retried or archived`, failedAt, ['retry', 'archive']);
275
+ continue;
276
+ }
277
+ members.push({ runId: id, view: v, run: undefined, severity: 'red', waitedMs: failedAt });
278
+ continue;
279
+ }
280
+ if (run === undefined)
281
+ continue; // completed / cancelled: resolved
282
+ // An open run is superseded only while it is parked at a gate (resolve can close it) and its
283
+ // successor did not itself fail or get cancelled (then this run is still the work).
284
+ const successor = s !== undefined ? byId.get(s.by) : undefined;
285
+ const replaced = s !== undefined && status === 'awaiting_human' && successor?.session.status !== 'failed' && successor?.session.status !== 'cancelled';
286
+ if (s !== undefined && !replaced)
287
+ sup.delete(id);
288
+ if (s !== undefined && replaced) {
289
+ const d = byId.get(s.by);
290
+ const how = s.retried ? `retried as ${short(s.by)}` : `superseded by ${short(s.by)}`;
291
+ const what = d === undefined ? '' : d.session.status === 'completed' || DELIVERED.has(d.session.delivery ?? '') ? `, which ${DELIVERED.has(d.session.delivery ?? '') ? 'delivered' : 'completed'}` : `, which is ${d.session.status.replace(/_/g, ' ')}`;
292
+ pushUnresolved(v, 'superseded', `still open at its ${run.gate?.kind?.replace(/_/g, ' ') ?? 'last'} gate; ${how}${what}`, gateAt, ['close_superseded', 'archive'], s.by);
293
+ continue;
294
+ }
295
+ if (isAnsweredAsk(v, run)) {
296
+ notStuck.add(id);
297
+ if (gateAt !== null && now - gateAt > PARKED_AFTER_MS) {
298
+ pushUnresolved(v, 'parked', `an answered ask parked at its turn for ${Math.round((now - gateAt) / 3_600_000)} h`, gateAt, ['archive']);
299
+ }
300
+ continue;
301
+ }
302
+ if (run.liveness === 'working' || run.liveness === 'quiet')
303
+ continue;
304
+ members.push({ runId: id, view: v, run, severity: run.liveness === 'waiting_on_you' ? 'yellow' : 'red', waitedMs: gateAt });
305
+ }
306
+ const groups = new Map();
307
+ const join = (key, m, build) => {
308
+ const g = groups.get(key);
309
+ if (g === undefined)
310
+ groups.set(key, { key, members: [m], build });
311
+ else if (!g.members.some((x) => x.runId === m.runId))
312
+ g.members.push(m);
313
+ };
314
+ // Down (or silent) components first: a held run whose denial names one joins it.
315
+ const infraItems = [];
316
+ for (const row of input.infra) {
317
+ if (row.state === 'down')
318
+ infraItems.push({ row, kind: 'prevents' });
319
+ else if (row.state === 'degraded' && (row.key === 'load' || row.key === 'daemon' || (row.key === 'skills' && /launches wait/i.test(row.reason))))
320
+ infraItems.push({ row, kind: 'slows' });
321
+ else if (row.state === 'unknown' && WORK_PATH.has(row.key))
322
+ infraItems.push({ row, kind: 'slows' });
323
+ }
324
+ // A down component claims the held runs whose denial names it, and the stalled runs on its path
325
+ // (a stalled run behind a dead gateway is the gateway's, not a cause of its own).
326
+ const claimed = new Set();
327
+ for (const { row } of infraItems) {
328
+ if (row.state !== 'down')
329
+ continue;
330
+ const cites = CITES[row.key];
331
+ const onPath = input.infraHoldsLive(row.key);
332
+ for (const m of members) {
333
+ if (m.run === undefined)
334
+ continue;
335
+ if ((cites !== undefined && cites.test(denialOf(m.view, m.run))) || (onPath && m.run.liveness === 'stalled'))
336
+ claimed.add(`${row.key}\u0000${m.runId}`);
337
+ }
338
+ }
339
+ // Red base: same repo, same failing check, a base that fails it too.
340
+ const baseKey = (m) => {
341
+ if (m.run === undefined || m.view === undefined)
342
+ return null;
343
+ const d = denialOf(m.view, m.run);
344
+ if (!BASE_RED.test(d))
345
+ return null;
346
+ const repo = repoOf(m.view);
347
+ if (repo === null)
348
+ return null;
349
+ return `base:${repo}:${CHECK_NAME.exec(d)?.[1] ?? 'checks'}`;
350
+ };
351
+ const rest = [];
352
+ for (const m of members) {
353
+ if ([...claimed].some((c) => c.endsWith(`\u0000${m.runId}`)))
354
+ continue;
355
+ const k = baseKey(m);
356
+ if (k !== null) {
357
+ join(k, m, (ms) => baseRedItem(k, ms));
358
+ continue;
359
+ }
360
+ rest.push(m);
361
+ }
362
+ // The fix the red base waits on: a run in that repo whose own problem is the red base.
363
+ const left = [];
364
+ for (const m of rest) {
365
+ const repo = m.view !== undefined ? repoOf(m.view) : null;
366
+ // The fix waits at its DELIVER gate (its own work is done); one fix per red base.
367
+ const target = m.run !== undefined && m.run.gate?.kind === 'deliver' && repo !== null && STATES_RED_BASE.test(m.view?.session.problem ?? '')
368
+ ? [...groups.values()].filter((g) => g.key.startsWith(`base:${repo}:`) && !g.members.some(baseKeyless)).sort((a, b) => b.members.length - a.members.length)[0]
369
+ : undefined;
370
+ if (target !== undefined)
371
+ target.members.push(m);
372
+ else
373
+ left.push(m);
374
+ }
375
+ // Runs with no distinct judge and nothing else holding them: one item per set of missing seats.
376
+ const gapByRun = new Map(input.gaps.map((g) => [g.run.runId, g]));
377
+ const seatKey = (gap) => `seat:${gap.creator ?? ''}:${gap.blockedBy.map((c) => c.cli).sort().join('+')}`;
378
+ const seatGroup = (key, gap) => (ms) => {
379
+ const who = gap.blockedBy.map((c) => `${c.cli} ${c.state === 'benched' ? 'benched' : c.state.replace('_', ' ')}`);
380
+ return {
381
+ id: ms.length === 1 ? `judge:${ms[0].runId}` : key,
382
+ severity: 'red',
383
+ source: 'run',
384
+ kind: 'prevents',
385
+ title: ms.length === 1 ? 'A job has no independent reviewer' : `${ms.length} jobs have no independent reviewer`,
386
+ reason: `No helper in the pool can review ${gap.creator ?? 'the creator'}’s work${who.length > 0 ? ` (${who.join(', ')})` : ''}`,
387
+ remedy: 'Sign in another helper in the job’s pool, or reassign the step to a helper whose work another helper can review.',
388
+ target: gateTarget(ms[0].run, ms[0].runId),
389
+ action: ms[0].run?.gate !== undefined ? { kind: 'open_gate', label: 'Open the gate' } : { kind: 'open_run', label: 'Open the job' },
390
+ };
391
+ };
392
+ for (const m of left) {
393
+ const gap = gapByRun.get(m.runId);
394
+ // A held run whose own denial IS the missing judge joins the other runs missing the same seats;
395
+ // a run held by its own failure keeps its item, the seat gap named in it.
396
+ if (gap !== undefined && JUDGE_DENIAL.test(denialOf(m.view, m.run))) {
397
+ const key = seatKey(gap);
398
+ join(key, m, seatGroup(key, gap));
399
+ continue;
400
+ }
401
+ join(`run:${m.runId}`, m, (ms) => {
402
+ const base = runItem(ms[0], input.clis);
403
+ if (gap === undefined)
404
+ return base;
405
+ const who = gap.blockedBy.map((c) => `${c.cli} ${c.state === 'benched' ? 'benched' : c.state.replace('_', ' ')}`);
406
+ return { ...base, severity: 'red', reason: `${base.reason}; no independent reviewer for ${gap.creator ?? 'its creator'}’s work${who.length > 0 ? ` (${who.join(', ')})` : ''}` };
407
+ });
408
+ }
409
+ const memberIds = new Set(members.map((m) => m.runId));
410
+ for (const gap of input.gaps) {
411
+ if (memberIds.has(gap.run.runId) || !eligible(gap.run.runId))
412
+ continue;
413
+ const key = seatKey(gap);
414
+ join(key, { runId: gap.run.runId, view: byId.get(gap.run.runId), run: gap.run, severity: 'red', waitedMs: msOf(gap.run.gate?.receivedAt ?? null) }, seatGroup(key, gap));
415
+ }
416
+ // No seat can take any work at all: that is its own item, holding every run.
417
+ const anyWorker = input.clis.some((c) => c.workEligible && c.state !== 'signed_out' && c.state !== 'benched');
418
+ if (input.clis.length > 0 && !anyWorker) {
419
+ const live = input.runs.filter((r) => eligible(r.runId)).map((r) => ({ runId: r.runId, view: byId.get(r.runId), run: r, severity: 'red', waitedMs: msOf(r.gate?.receivedAt ?? null) }));
420
+ groups.set('cli:none', {
421
+ key: 'cli:none',
422
+ members: live,
423
+ build: () => ({
424
+ id: 'cli:none',
425
+ severity: 'red',
426
+ source: 'cli',
427
+ kind: 'prevents',
428
+ title: 'No helper can take work',
429
+ reason: `Every helper is signed out or benched (${seatsOut(undefined, input.clis).join(', ')})`,
430
+ remedy: 'Sign a CLI in, or wait for a bench to lift.',
431
+ target: { kind: 'cli', cli: input.clis[0].cli },
432
+ action: input.clis.some((c) => c.canSignIn) ? { kind: 'sign_in', label: 'Sign in' } : null,
433
+ }),
434
+ });
435
+ }
436
+ const items = [];
437
+ const finish = (g, body) => {
438
+ const ms = g.members;
439
+ const waits = ms.map((m) => m.waitedMs).filter((x) => x !== null);
440
+ items.push({
441
+ ...body,
442
+ severity: body.severity ?? (ms.some((m) => m.severity === 'red') ? 'red' : 'yellow'),
443
+ blocks: ms.length,
444
+ blockedRunIds: ms.map((m) => m.runId),
445
+ // A slowdown has no wait of its own: the runs it slows wait on their own causes.
446
+ waitedSince: body.kind === 'slows' ? null : isoOf(waits.length > 0 ? Math.min(...waits) : null),
447
+ unitsLeft: ms.reduce((a, m) => a + unitsLeftOf(m.view), 0),
448
+ });
449
+ };
450
+ for (const g of groups.values())
451
+ finish(g, g.build(g.members));
452
+ // Components: a down one holds the runs whose denial names it (and every live run when it is on
453
+ // their path); host load slows the held runs whose next step is real work, and every new run.
454
+ const heldMembers = members.filter((m) => m.run !== undefined);
455
+ for (const { row, kind } of infraItems) {
456
+ const held = [];
457
+ const add = (m) => {
458
+ if (!held.some((x) => x.runId === m.runId))
459
+ held.push(m);
460
+ };
461
+ if (row.key === 'load') {
462
+ for (const m of heldMembers)
463
+ if (m.run.gate?.kind === 'escalation' && m.view !== undefined && !isAskRun(m.view))
464
+ add(m);
465
+ for (const r of input.runs)
466
+ if ((r.liveness === 'working' || r.liveness === 'quiet' || r.liveness === 'stalled') && eligible(r.runId))
467
+ add({ runId: r.runId, view: byId.get(r.runId), run: r, severity: 'yellow', waitedMs: null });
468
+ }
469
+ else {
470
+ for (const m of heldMembers)
471
+ if (claimed.has(`${row.key}\u0000${m.runId}`))
472
+ add(m);
473
+ if (input.infraHoldsLive(row.key)) {
474
+ for (const r of input.runs)
475
+ if ((r.liveness === 'working' || r.liveness === 'quiet' || r.liveness === 'stalled') && eligible(r.runId))
476
+ add({ runId: r.runId, view: byId.get(r.runId), run: r, severity: 'red', waitedMs: null });
477
+ }
478
+ }
479
+ // A slowdown that slows no work is not stuck work (a down component still prevents new runs).
480
+ // The one exception: a run list that could not be read, so nothing proves there is no work.
481
+ if (kind === 'slows' && held.length === 0 && !(row.key === 'daemon' && row.state === 'degraded'))
482
+ continue;
483
+ const down = row.state === 'down';
484
+ finish({ members: held }, {
485
+ id: `infra:${row.key}`,
486
+ severity: down ? 'red' : 'yellow',
487
+ source: 'infra',
488
+ kind,
489
+ title: `${row.label}: ${row.state === 'unknown' ? 'not answering' : down ? (DOWN_WORD[row.key] ?? 'down') : 'degraded'}`,
490
+ reason: `${row.reason}${held.length > 0 ? `; it ${kind === 'slows' ? 'slows the next step of' : 'holds'} ${plural(held.length, 'job')}${row.key === 'load' ? ' and every new one' : ''}` : ''}`,
491
+ remedy: row.remedy ?? 'Re-check.',
492
+ target: { kind: 'infra', key: row.key },
493
+ action: { kind: 'recheck', label: 'Re-check' },
494
+ });
495
+ }
496
+ const KIND_RANK = { prevents: 0, slows: 1 };
497
+ items.sort((a, b) => b.blocks - a.blocks ||
498
+ KIND_RANK[a.kind ?? 'prevents'] - KIND_RANK[b.kind ?? 'prevents'] ||
499
+ // A component also holds every NEW run: on a tie it outranks one run's own cause.
500
+ (a.source !== 'run' ? 0 : 1) - (b.source !== 'run' ? 0 : 1) ||
501
+ (msOf(a.waitedSince ?? null) ?? Infinity) - (msOf(b.waitedSince ?? null) ?? Infinity) ||
502
+ (b.unitsLeft ?? 0) - (a.unitsLeft ?? 0) ||
503
+ a.id.localeCompare(b.id));
504
+ const costed = unresolved.filter((u) => u.cost.costUsd !== null);
505
+ const tokened = unresolved.filter((u) => u.cost.tokens !== null);
506
+ unresolved.sort((a, b) => (b.cost.costUsd ?? -1) - (a.cost.costUsd ?? -1) ||
507
+ (msOf(a.since) ?? Infinity) - (msOf(b.since) ?? Infinity) ||
508
+ a.runId.localeCompare(b.runId));
509
+ return {
510
+ attention: items,
511
+ unresolved,
512
+ unresolvedSpend: {
513
+ costUsd: costed.length > 0 ? Math.round(costed.reduce((s, u) => s + (u.cost.costUsd ?? 0), 0) * 10_000) / 10_000 : null,
514
+ tokens: tokened.length > 0 ? tokened.reduce((t, u) => ({ input: t.input + u.cost.tokens.input, output: t.output + u.cost.tokens.output }), { input: 0, output: 0 }) : null,
515
+ costKnown: costed.length,
516
+ },
517
+ };
518
+ }
519
+ function baseRedItem(key, ms) {
520
+ const [, repo, check] = key.split(':');
521
+ const fix = ms.find((m) => STATES_RED_BASE.test(m.view?.session.problem ?? '') && baseKeyless(m));
522
+ const floors = ms.filter((m) => m !== fix);
523
+ const fixTitle = fix?.view !== undefined ? titleOf(fix.view) : '';
524
+ return {
525
+ id: key,
526
+ source: 'run',
527
+ kind: 'prevents',
528
+ title: `${readable(repo)}'s base is red inside the floor sandbox`,
529
+ // The fix is named once (the remedy), never in both lines.
530
+ // Delivery opens the fix's PR (merge stays a person's), and nothing retries the held jobs on its
531
+ // own: the sentence says all three steps.
532
+ reason: `${plural(floors.length, 'job')} ${floors.length === 1 ? 'fails' : 'fail'} \`${check}\` on a base that fails it too.${fix !== undefined ? ' The fix is ready: approve its delivery and merge it, then retry the held jobs.' : ''}`,
533
+ remedy: fix !== undefined
534
+ ? `Deliver “${readable(fixTitle)}”.`
535
+ : 'Make the base green (or use the floor override), then retry the held jobs.',
536
+ target: gateTarget((fix ?? ms[0]).run, (fix ?? ms[0]).runId),
537
+ action: { kind: 'open_gate', label: fix !== undefined ? 'Open the fix’s gate' : 'Open the gate' },
538
+ };
539
+ }
540
+ /** A run that joined a red-base group as its fix (it has no red-base denial of its own). */
541
+ function baseKeyless(m) {
542
+ return !BASE_RED.test(denialOf(m.view, m.run));
543
+ }
544
+ /** The level: `red` when an attention item is red; `yellow` when anything needs a look OR is
545
+ * unresolved; `green` only when both are empty. */
546
+ export function needsLevel(attention, unresolved = []) {
547
+ if (attention.some((a) => a.severity === 'red'))
548
+ return 'red';
549
+ return attention.length > 0 || unresolved.length > 0 ? 'yellow' : 'green';
550
+ }
551
+ /** Fold one run's event log into what it cost (`cliUsage` frames). `null` fields when none carried it. */
552
+ export function foldRunCost(events) {
553
+ let input = 0;
554
+ let output = 0;
555
+ let tokens = false;
556
+ let cost = 0;
557
+ let costed = false;
558
+ for (const e of events) {
559
+ if (e.type !== 'cliUsage')
560
+ continue;
561
+ const i = typeof e['inputTokens'] === 'number' ? e['inputTokens'] : undefined;
562
+ const o = typeof e['outputTokens'] === 'number' ? e['outputTokens'] : undefined;
563
+ if (i !== undefined || o !== undefined) {
564
+ tokens = true;
565
+ input += i ?? 0;
566
+ output += o ?? 0;
567
+ }
568
+ if (typeof e['costUsd'] === 'number' && Number.isFinite(e['costUsd'])) {
569
+ costed = true;
570
+ cost += e['costUsd'];
571
+ }
572
+ }
573
+ return { costUsd: costed ? Math.round(cost * 10_000) / 10_000 : null, tokens: tokens ? { input, output } : null };
574
+ }
575
+ /** Per-run cost, read once per terminal run (it cannot change) and briefly for an open one. The
576
+ * snapshot never waits past its bound: a run not read yet reads unknown, and the next read has it. */
577
+ export class RunCostCache {
578
+ ttlMs;
579
+ now;
580
+ done = new Map();
581
+ inFlight = null;
582
+ constructor(ttlMs = 60_000, now = Date.now) {
583
+ this.ttlMs = ttlMs;
584
+ this.now = now;
585
+ }
586
+ get(runId) {
587
+ return this.done.get(runId)?.cost;
588
+ }
589
+ /** Read every listed run not cached, stale, or whose `stamp` moved (a failed run resumed and ended
590
+ * again has a new end: its cost is read again); resolves when all are read. */
591
+ fill(runs, read) {
592
+ if (this.inFlight !== null)
593
+ return this.inFlight;
594
+ const t = this.now();
595
+ const todo = runs.filter((r) => {
596
+ const c = this.done.get(r.id);
597
+ return c === undefined || c.stamp !== (r.stamp ?? '') || (!c.final && t - c.at > this.ttlMs);
598
+ });
599
+ if (todo.length === 0)
600
+ return Promise.resolve();
601
+ this.inFlight = (async () => {
602
+ for (const r of todo) {
603
+ try {
604
+ const events = await read(r.id);
605
+ if (events !== null)
606
+ this.done.set(r.id, { at: this.now(), cost: foldRunCost(events), final: r.final, stamp: r.stamp ?? '' });
607
+ }
608
+ catch {
609
+ // unreadable: stays unknown, retried next read
610
+ }
611
+ }
612
+ })().finally(() => {
613
+ this.inFlight = null;
614
+ });
615
+ return this.inFlight;
616
+ }
617
+ }
618
+ //# sourceMappingURL=needs.js.map