humanish 0.65.0 → 0.67.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.
@@ -31,8 +31,9 @@ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-
31
31
  import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
32
32
  import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson } from "./browser-evidence-hygiene.js";
33
33
  import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
34
- import { createLocalAgentProvider, detectLocalAgents } from "./local-agent-cli.js";
34
+ import { detectLocalAgents } from "./local-agent-cli.js";
35
35
  import { startAppServerSession } from "./local-agent-appserver.js";
36
+ import { startClaudeSession } from "./local-agent-claude-session.js";
36
37
  import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
37
38
  import { probeUrl, readDetachedLog, runDetachedStep, startDetachedProcess } from "./e2b-detached.js";
38
39
  import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, collectExternalCommsThread, deployCommsCatch, externalCatchHealthy, externalInboxUrl, refreshInboxSurface, writeInboxSurface } from "./comms-sandbox-catch.js";
@@ -1149,7 +1150,7 @@ async function openDesktopTerminal(desktop, requestTimeoutMs, workdir) {
1149
1150
  // a study that measures our own mojibake against an unconfigured template would be measuring
1150
1151
  // the template. The PRODUCT-side fix (an ASCII fallback when the locale is not UTF-8) is in
1151
1152
  // src/terminal-encoding.ts, and it is the one that matters for real users.
1152
- ` (cd ${shellSingleQuote(dir)} 2>/dev/null || cd /home/user; DISPLAY=:0 LANG=C.UTF-8 LC_ALL=C.UTF-8 nohup "$candidate" >/dev/null 2>&1 &)`,
1153
+ ` (cd ${shellSingleQuote(dir)} 2>/dev/null || cd /home/user; DISPLAY=:0 LANG=C.UTF-8 LC_ALL=C.UTF-8 HUMANISH_STUDY_PARTICIPANT=1 nohup "$candidate" >/dev/null 2>&1 &)`,
1153
1154
  " sleep 3",
1154
1155
  ' echo "humanish: opened $candidate"',
1155
1156
  " exit 0",
@@ -1178,6 +1179,16 @@ async function startDesktopStream(desktop, browserWindowId) {
1178
1179
  await desktop.stream.start({ requireAuth: true });
1179
1180
  }
1180
1181
  }
1182
+ // "can't" followed by a PERCEPTION verb describes what the screen showed, not an inability to
1183
+ // proceed: "the canvas truncates it so you can't even read the whole thing", "I can't tell from
1184
+ // the screen whether the rename is persisted", "so I could not read its full description". Five
1185
+ // of five completed live runs on 2026-09-01 (two on drawDB, three on the planted benchmark app)
1186
+ // were refused as "not a credible pass" on exactly these sentences, every one a defect report
1187
+ // written AFTER the participant reached the goal. The more precisely a participant describes a
1188
+ // display defect, the more likely the scan was to refuse the run — the incentive inversion #453
1189
+ // fixed for resolved arcs, back in a new shape. "could not complete", "could not connect",
1190
+ // "unable to get focus" still count: those name an inability to act.
1191
+ const PERCEPTION_AFTER_MODAL = /\b(can'?t|cannot|could ?not|couldn'?t|unable to|wasn'?t able to)\s+(even\s+|quite\s+|really\s+|fully\s+)?(read|see|tell|view|make out|verify|confirm|be sure|be certain|judge|know)\b/g;
1181
1192
  function hasBlockerLanguage(text) {
1182
1193
  return /\b(can'?t|cannot|could not|unable|blocked|blocker|failed|invalid|not set)\b/.test(text)
1183
1194
  || /\b(shows|showing|hit|encountered|returned|got)\b.{0,80}\berror\b/.test(text)
@@ -1193,7 +1204,11 @@ function completionReasonContradictsGoal(reason) {
1193
1204
  * first — failure narration the participant itself reports as overcome is friction on the
1194
1205
  * road, not a blocker at the destination (#453). */
1195
1206
  function completionReasonBlocksVerdict(reason) {
1196
- return hasBlockerLanguage(stripResolvedArcSegments(stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase()))));
1207
+ // Perception phrases are stripped for the VERDICT only: "I could not read the full description"
1208
+ // is friction worth a tally count and a feedback candidate (the friction scan above keeps it),
1209
+ // and it is not a reason to refuse the pass.
1210
+ return hasBlockerLanguage(stripResolvedArcSegments(stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase())))
1211
+ .replace(PERCEPTION_AFTER_MODAL, ""));
1197
1212
  }
1198
1213
  // A failure segment counts as a resolved arc when the recovery is self-reported either in the
1199
1214
  // SAME segment ("the import failed but then went through") or — the common report shape — in the
@@ -1221,6 +1236,10 @@ function stripResolvedArcSegments(text) {
1221
1236
  }
1222
1237
  function stripNegatedNonBlockerPhrases(text) {
1223
1238
  return text
1239
+ // FIRST, before the narrower rules eat the "no blockers" and leave "encountered ... error"
1240
+ // behind: "I encountered no blockers or unclear error output." refused a clean passing run on
1241
+ // 2026-09-01. A verb of encounter followed by "no" negates the whole clause, so drop the clause.
1242
+ .replace(/\b(?:encountered|hit|saw|found|met|had|got|ran into)\s+no\s+[^.!?\n]*/g, " ")
1224
1243
  .replace(/\bno\s+(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\s+(?:was\s+|were\s+)?(?:encountered|observed|found|hit|seen|reported|detected)\b/g, "")
1225
1244
  .replace(/\bwithout\s+(?:a\s+|any\s+)?(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\b/g, "")
1226
1245
  .replace(/\bnot\s+(?:blocked|a blocker|an error|failed)\b/g, "")
@@ -1286,8 +1305,10 @@ export function resolveSelfReportedFriction(session) {
1286
1305
  export async function runCuaLane(spec, deps) {
1287
1306
  const { config, appUrl, cloneRoute, localTreeRoute, serve, subjectRepo, subjectEnvNames } = deps;
1288
1307
  const desktopCliRoute = deps.desktopCliRoute === true;
1289
- // The local brain, when there is one. `appServer` owns a process, so the lane closes it.
1308
+ // The local brain, when there is one. `appServer` / `claudeSession` own a process, so the lane
1309
+ // closes it.
1290
1310
  let appServer;
1311
+ let claudeSession;
1291
1312
  let localAgentProvider;
1292
1313
  const subjectEnvValues = config.subject.envValues ?? {};
1293
1314
  const targetUrl = spec.targetUrl ?? appUrl;
@@ -1588,11 +1609,14 @@ export async function runCuaLane(spec, deps) {
1588
1609
  localAgentProvider = appServer.provider;
1589
1610
  }
1590
1611
  else if (deps.localAgent === "claude") {
1591
- localAgentProvider = createLocalAgentProvider({
1592
- agent: "claude",
1612
+ // One session for the whole run, like the codex thread above (#520). The one-shot
1613
+ // provider (createLocalAgentProvider) spawned `claude -p` per turn, and every turn
1614
+ // started with no memory of the last.
1615
+ claudeSession = await startClaudeSession({
1593
1616
  ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
1594
1617
  ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model })
1595
1618
  });
1619
+ localAgentProvider = claudeSession.provider;
1596
1620
  }
1597
1621
  // World is ready: release the pipeline gate so the remaining lanes may start.
1598
1622
  provisioned = true;
@@ -1728,6 +1752,7 @@ export async function runCuaLane(spec, deps) {
1728
1752
  // The local brain owns a process. Close it before anything else can throw: a leaked
1729
1753
  // app-server per lane would outlive the run and keep a thread open on the operator's plan.
1730
1754
  appServer?.close();
1755
+ await claudeSession?.close();
1731
1756
  // Stop the mid-run inbox-surface loop FIRST — before the teardown evidence drain below — so the two
1732
1757
  // `cat`s never overlap and the final surface state is deterministic. A surface failure can never
1733
1758
  // block teardown (the loop body is fully try/caught and this await is on its already-caught promise).
@@ -2963,13 +2988,17 @@ async function runCuaActorLabInScope(options) {
2963
2988
  message: outcome?.sessionError
2964
2989
  ?? (outcome?.noEngagement
2965
2990
  ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
2966
- : observer.ok
2967
- ? outcome?.session?.completionReason === "harness_error"
2968
- ? `Computer-use session ended with a harness error: ${outcome.session.reason}`
2969
- : outcome?.session?.status !== "passed"
2970
- ? `Computer-use session ended with ${outcome?.session?.status ?? "unknown"}: ${outcome?.session?.reason ?? "no terminal reason"}`
2971
- : "Computer-use lab did not produce a terminal session."
2972
- : observer.error?.message ?? "Observer failed for the computer-use lab run.")
2991
+ // The lane result (toLaneResult) named this refusal; the N=1 envelope fell through to
2992
+ // "did not produce a terminal session", which is false — it produced one and refused it.
2993
+ : outcome?.selfReportedBlocker
2994
+ ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
2995
+ : observer.ok
2996
+ ? outcome?.session?.completionReason === "harness_error"
2997
+ ? `Computer-use session ended with a harness error: ${outcome.session.reason}`
2998
+ : outcome?.session?.status !== "passed"
2999
+ ? `Computer-use session ended with ${outcome?.session?.status ?? "unknown"}: ${outcome?.session?.reason ?? "no terminal reason"}`
3000
+ : "Computer-use lab did not produce a terminal session."
3001
+ : observer.error?.message ?? "Observer failed for the computer-use lab run.")
2973
3002
  };
2974
3003
  }
2975
3004
  const failingLane = (outcomes ?? []).find((outcome) => !laneOk(outcome));
@@ -3117,6 +3146,15 @@ function buildSingleLaneBundle(args) {
3117
3146
  captureRedaction: args.redactScreenshots ? "blurred" : "raw",
3118
3147
  ...(outcome?.session ? { session: outcome.session } : {}),
3119
3148
  ...(outcome?.sessionError ? { sessionError: outcome.sessionError } : {}),
3149
+ ...(outcome === undefined
3150
+ ? {}
3151
+ : {
3152
+ credibility: {
3153
+ noEngagement: outcome.noEngagement === true,
3154
+ selfReportedBlocker: outcome.selfReportedBlocker === true,
3155
+ reportedFriction: outcome.reportedFriction === true
3156
+ }
3157
+ }),
3120
3158
  source: args.source,
3121
3159
  ...(args.inProgress === undefined ? {} : { inProgress: args.inProgress }),
3122
3160
  ...(args.subjectProvenance === undefined ? {} : { subjectProvenance: args.subjectProvenance }),
@@ -3871,22 +3909,36 @@ export function buildCuaBundle(args) {
3871
3909
  const singleStudyTasks = args.inProgress !== true && args.session?.trace.taskFunnel !== undefined
3872
3910
  ? aggregateTaskFunnels([args.session.trace.taskFunnel])
3873
3911
  : undefined;
3912
+ // What happened to the participant, as the LANE judged it — the same rule the fan-out roll-up
3913
+ // applies (participantStatusForOutcome). Before #476 this read the actor's own status, so a
3914
+ // run the lane refused as "not a credible pass" was written up as verdict pass, 1/1 reached
3915
+ // the goal, and every projection of the bundle (Observer tally, `runs`, the status index)
3916
+ // repeated it. Found on a real drawDB run whose participant wrote "Blocked after partial
3917
+ // completion".
3918
+ const participantStatus = args.session === undefined
3919
+ ? undefined
3920
+ : participantStatusForCredibility(args.session.status, args.credibility);
3921
+ const credibilityNote = args.session === undefined || participantStatus === args.session.status
3922
+ ? undefined
3923
+ : args.credibility?.noEngagement === true
3924
+ ? "Not counted as a pass: the participant took no actions and said nothing."
3925
+ : "Not counted as a pass: the participant's final message described a blocker.";
3874
3926
  const review = {
3875
3927
  schema: REVIEW_SCHEMA,
3876
3928
  verdict: args.inProgress === true
3877
3929
  ? "contract_proof_only"
3878
- : args.session
3879
- ? verdictForStatus(args.session.status)
3930
+ : participantStatus !== undefined
3931
+ ? verdictForStatus(participantStatus)
3880
3932
  : args.sessionError
3881
3933
  ? "fail"
3882
3934
  : "contract_proof_only",
3883
3935
  // One lane is still a study with a denominator of one, and saying so keeps a single-lane
3884
3936
  // result from being read as though it generalized.
3885
- ...(args.session && args.inProgress !== true
3886
- ? { participants: tallyParticipantOutcomes([args.session.status]) }
3937
+ ...(participantStatus !== undefined && args.inProgress !== true
3938
+ ? { participants: tallyParticipantOutcomes([participantStatus], [args.credibility?.reportedFriction === true]) }
3887
3939
  : {}),
3888
3940
  ...(singleStudyTasks === undefined ? {} : { tasks: singleStudyTasks }),
3889
- summary: reason,
3941
+ summary: credibilityNote === undefined ? reason : `${credibilityNote} ${reason}`,
3890
3942
  gaps: args.session || args.sessionError
3891
3943
  ? []
3892
3944
  : args.inProgress === true
@@ -4309,7 +4361,10 @@ export function buildCuaFanoutBundle(args) {
4309
4361
  // it — but `reachedGoal` was reading the trace status directly, so one run could be both
4310
4362
  // "not a passed lane" AND "1/1 reached the goal". The headline number a researcher reads
4311
4363
  // first was the dishonest one. Found by a provider bug that ended a study on turn one.
4312
- terminalOutcomes.map((outcome) => (outcome.noEngagement === true ? "incomplete" : outcome.session.status)),
4364
+ terminalOutcomes.map((outcome) => participantStatusForCredibility(outcome.session.status, {
4365
+ noEngagement: outcome.noEngagement === true,
4366
+ selfReportedBlocker: outcome.selfReportedBlocker === true
4367
+ })),
4313
4368
  // A participant who reached the goal AND told you the road there was broken is the most
4314
4369
  // useful result a study produces; reporting only the outcome would bury it.
4315
4370
  terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
@@ -4486,6 +4541,22 @@ function providerResourcesForOutcome(args) {
4486
4541
  }
4487
4542
  }];
4488
4543
  }
4544
+ /**
4545
+ * The status a participant is TALLIED under, given what the lane made of the session. A
4546
+ * goal_satisfied claim with zero engagement is a session that ran out before anything happened;
4547
+ * one whose final message describes a blocker is a participant who could not proceed and said so.
4548
+ * Both keep their trace status (the claim is evidence); neither is a participant who reached the
4549
+ * goal. One rule for the single lane and the fan-out roll-up (#476).
4550
+ */
4551
+ export function participantStatusForCredibility(status, credibility) {
4552
+ if (status !== "passed" || credibility === undefined)
4553
+ return status;
4554
+ if (credibility.noEngagement)
4555
+ return "incomplete";
4556
+ if (credibility.selfReportedBlocker)
4557
+ return "blocked";
4558
+ return status;
4559
+ }
4489
4560
  function verdictForStatus(status) {
4490
4561
  switch (status) {
4491
4562
  case "passed":