humanish 0.119.0 → 0.120.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.120.0: Analysis completes under Bun, runs past 16 participants print a bounded list, Observer counts participants by status (2026-10-09)
12
+
13
+ humanish 0.120.0 completes an analysis run from a Bun process, prints a run of more than 16
14
+ participants in a bounded number of lines, and counts and filters the Observer's participants by
15
+ status and persona. Under Bun, `undici` is Bun's built-in module, whose `Agent` has no `destroy()`,
16
+ and the analysis provider's teardown after each request threw a `TypeError` that replaced the
17
+ request's result, so every automatic analysis from a Bun process ended `unknown` with
18
+ `AUTOMATIC_ANALYSIS_OUTCOME_UNKNOWN` and kept no receipt. A failing teardown is now ignored.
19
+ `humanish watch` and `humanish run` print a computer-use run of more than 16 participants as one
20
+ line counting them by status, up to 16 participants that did not pass, and a `not listed:` line.
21
+ Each participant line holds its closing message on one line cut at 160 characters, and a warning
22
+ printed the same for each participant prints once. The Observer's participant grid counts the
23
+ participants by status when they ended more than one way, and pressing a count filters the grid.
24
+ Its view options gain a Persona filter, a filter change returns to the first page, and Next page
25
+ and Previous page open the new page at its first participant.
26
+
27
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.120.0)
28
+
11
29
  ## 0.119.0: Participants arrive over time, one analysis past 16 participants in cohorts, late starts skipped past the budget, study warnings redacted (2026-10-09)
12
30
 
13
31
  humanish 0.119.0 lets a computer-use or shared-world study start its participants over time, and
@@ -221,7 +221,14 @@ export function createAnalysisProvider(options) {
221
221
  finally {
222
222
  clearTimeout(timer);
223
223
  request.signal?.removeEventListener("abort", onAbort);
224
- await dispatcher.destroy().catch(() => undefined);
224
+ // Under Bun, `undici` is Bun's built-in module, whose Agent has no destroy(). Closing the
225
+ // agent must never replace the request's result, so a teardown that throws is ignored.
226
+ try {
227
+ await dispatcher.destroy();
228
+ }
229
+ catch {
230
+ // The agent belongs to this request alone; nothing else uses it after this point.
231
+ }
225
232
  }
226
233
  };
227
234
  }
@@ -50,6 +50,47 @@ function subjectLine(appUrl, subject) {
50
50
  function participantStatus(status) {
51
51
  return status === "contract_proof_only" ? "dry run, nothing ran live" : status;
52
52
  }
53
+ /** A warning the route adds for each participant prints once. */
54
+ function warningLines(warnings) {
55
+ return [...new Set(warnings)].map((warning) => `warning: ${warning}`);
56
+ }
57
+ /** How many participants a run lists by name, and how long a closing message may run in a list. */
58
+ const LISTED_PARTICIPANTS = 16;
59
+ const REASON_CHARS = 160;
60
+ /** A closing message on one line of at most REASON_CHARS characters. */
61
+ function oneLine(text) {
62
+ const line = text.replace(/\s+/g, " ").trim();
63
+ return line.length > REASON_CHARS ? `${line.slice(0, REASON_CHARS - 1).trimEnd()}…` : line;
64
+ }
65
+ function participantLine({ id, status, session, diagnostics }) {
66
+ return [
67
+ `participant ${id}: ${participantStatus(status)}${session ? ` (${session.completionReason})` : ""}`,
68
+ ...(diagnostics ? [formatCuaDiagnostics(diagnostics)] : []),
69
+ ...(session ? [oneLine(session.reason)] : []),
70
+ ].join(" · ");
71
+ }
72
+ /**
73
+ * A fan-out run's participants: one line each up to LISTED_PARTICIPANTS, which fits one terminal
74
+ * screen with the run's other lines. A larger run prints its count by status, the first
75
+ * LISTED_PARTICIPANTS that did not pass, and how many it left out.
76
+ */
77
+ function participantLines(participants) {
78
+ if (participants.length <= 1)
79
+ return [];
80
+ if (participants.length <= LISTED_PARTICIPANTS)
81
+ return participants.map(participantLine);
82
+ const counts = new Map();
83
+ for (const { status } of participants)
84
+ counts.set(participantStatus(status), (counts.get(participantStatus(status)) ?? 0) + 1);
85
+ const notPassed = participants.filter(({ status }) => status !== "passed" && status !== "contract_proof_only");
86
+ const listed = notPassed.slice(0, LISTED_PARTICIPANTS);
87
+ const unlistedNotPassed = notPassed.length - listed.length;
88
+ return [
89
+ `participants: ${participants.length} · ${[...counts].map(([status, count]) => `${count} ${status}`).join(" · ")}`,
90
+ ...listed.map(participantLine),
91
+ `not listed: ${participants.length - listed.length} participants${unlistedNotPassed > 0 ? ` (${unlistedNotPassed} did not pass)` : ""}. The Observer shows each one.`,
92
+ ];
93
+ }
53
94
  export function formatConcurrentSharedWorldStudyHuman(result) {
54
95
  return withError(result.error, [
55
96
  ...runHeader(result, "shared-world"),
@@ -78,7 +119,7 @@ export function formatConcurrentSharedWorldStudyHuman(result) {
78
119
  ...(result.observer?.opened === undefined
79
120
  ? []
80
121
  : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
81
- ...result.warnings.map((warning) => `warning: ${warning}`),
122
+ ...warningLines(result.warnings),
82
123
  ]);
83
124
  }
84
125
  export function formatTerminalStudyHuman(result) {
@@ -95,7 +136,7 @@ export function formatTerminalStudyHuman(result) {
95
136
  ...(result.observer?.opened === undefined
96
137
  ? []
97
138
  : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
98
- ...result.warnings.map((warning) => `warning: ${warning}`),
139
+ ...warningLines(result.warnings),
99
140
  ]);
100
141
  }
101
142
  /** `subject` is the study's declared subject, which names it when the result has no URL. */
@@ -121,7 +162,7 @@ export function formatScriptedStudyHuman(result, subject) {
121
162
  ...(result.observer?.opened === undefined
122
163
  ? []
123
164
  : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
124
- ...result.warnings.map((warning) => `warning: ${warning}`),
165
+ ...warningLines(result.warnings),
125
166
  ]);
126
167
  }
127
168
  /** `subject` is the study's declared subject, which names it when the result has no URL. */
@@ -140,9 +181,7 @@ export function formatCuaStudyHuman(result, subject) {
140
181
  ? [`rerun: ${result.rerun.selectedLaneIds.join(", ")} from ${result.rerun.sourceRunId}`]
141
182
  : []),
142
183
  ...(result.diagnostics ? [`diagnostic: ${formatCuaDiagnostics(result.diagnostics)}`] : []),
143
- ...((result.lanes?.length ?? 0) > 1
144
- ? result.lanes.map((participant) => `participant ${participant.id}: ${participantStatus(participant.status)}${participant.session ? ` (${participant.session.completionReason})` : ""}${participant.diagnostics ? ` · ${formatCuaDiagnostics(participant.diagnostics)}` : ""}${participant.session ? ` · ${participant.session.reason}` : ""}`)
145
- : []),
184
+ ...participantLines(result.lanes ?? []),
146
185
  ...(result.session && (result.lanes?.length ?? 0) <= 1
147
186
  ? [
148
187
  `session: ${participantStatus(result.session.status)} (${result.session.completionReason})${result.session.stopCause ? ` · ${formatCuaStopCause(result.session.stopCause)}` : ""} · ${result.session.reason}`,
@@ -159,6 +198,6 @@ export function formatCuaStudyHuman(result, subject) {
159
198
  ...(result.observer?.opened === undefined
160
199
  ? []
161
200
  : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
162
- ...result.warnings.map((warning) => `warning: ${warning}`),
201
+ ...warningLines(result.warnings),
163
202
  ]);
164
203
  }