humanish 0.32.0 → 0.34.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/dist/program.js CHANGED
@@ -11,6 +11,7 @@ import { runInit } from "./init.js";
11
11
  import { inspectLabManifest, listLabManifests, resolveLabManifest } from "./labs.js";
12
12
  import { runLabPreflight } from "./lab-preflight.js";
13
13
  import { runLab, resolveLabDryRun, selectLabBackend } from "./lab-engine.js";
14
+ import { loadAdapterScorer } from "./adapter-scorer-loader.js";
14
15
  import { CUA_ACTOR_LAB_SCHEMA } from "./cua-actor-lab.js";
15
16
  import { openTarget, renderObserver, serveObserver } from "./observer.js";
16
17
  import { serveObserverStatic } from "./observer-static.js";
@@ -481,6 +482,7 @@ function registerWatchCommand(parent, io) {
481
482
  .option("--redact-repos", "Lab only: redact repo labels in durable artifacts.")
482
483
  .option("--no-redact-repos", "Lab only: persist repo labels. Use only for public-safe runs.")
483
484
  .option("--keep", "Lab only: keep disposable clone sandbox for debugging.")
485
+ .option("--scorer <path>", "Terminal/computer-use/shared-world labs only: repo-relative adopter scorer module (.mjs). Overrides review.scorer.ref. Executable code — review it as code.")
484
486
  .option("--run-id <id>", "Explicit run id for deterministic fixture tests.")
485
487
  .option("--cwd <path>", "Target project directory.", ".")
486
488
  .option("--env-file <path>", "Load a local env file for this watch without persisting values.")
@@ -574,6 +576,7 @@ function registerWatchCommand(parent, io) {
574
576
  repo: options.repo,
575
577
  ...(options.repos === undefined ? {} : { repos: options.repos }),
576
578
  ...(options.runId === undefined ? {} : { runId: options.runId }),
579
+ ...(options.scorer === undefined ? {} : { scorer: options.scorer }),
577
580
  ...(options.sims === undefined ? {} : { sims: options.sims }),
578
581
  ...(options.expose === undefined ? {} : { expose: options.expose }),
579
582
  ...(options.tunnel === undefined ? {} : { tunnel: options.tunnel }),
@@ -1238,6 +1241,7 @@ function registerLabCommands(parent, io) {
1238
1241
  .option("--redact-repos", "Meta only: redact repo labels in durable lab artifacts.")
1239
1242
  .option("--no-redact-repos", "Meta only: persist repo labels in durable lab artifacts. Use only for public-safe runs.")
1240
1243
  .option("--keep", "Smoke labs only: keep disposable clone sandbox for debugging.")
1244
+ .option("--scorer <path>", "Terminal/computer-use/shared-world labs only: repo-relative adopter scorer module (.mjs). Overrides review.scorer.ref. Executable code — review it as code.")
1241
1245
  .option("--json", JSON_OPTION_DESCRIPTION)
1242
1246
  .addHelpText("after", [
1243
1247
  "",
@@ -1245,6 +1249,7 @@ function registerLabCommands(parent, io) {
1245
1249
  " humanish lab run first-run",
1246
1250
  " humanish lab run fanout-demo --rerun-failed-from latest --lanes lane-02,lane-04",
1247
1251
  " humanish lab run oss --dry-run --json --no-open",
1252
+ " humanish lab run my-terminal-lab --scorer scorers/product.mjs",
1248
1253
  " humanish lab run .humanish/labs/private-dogfood.yaml --env-file .humanish/local/provider.env",
1249
1254
  "",
1250
1255
  "Human watch path:",
@@ -1473,6 +1478,39 @@ async function runOssSmokeAction(args) {
1473
1478
  writeResult(args.command, args.io, result, formatOssLabHuman);
1474
1479
  args.io.setExitCode(result.ok ? 0 : 2);
1475
1480
  }
1481
+ /**
1482
+ * Resolve `review.scorer.ref` (or the `--scorer` override) to a loaded adopter scorer, fail-closed
1483
+ * (typed error) PRE-SPEND. Precedence: CLI `--scorer` overrides the manifest; `source` records which
1484
+ * won. No scorer declared → `{ ok: true }` with no scorer. A declared scorer on an unsupported
1485
+ * backend, or a bad/unreadable/broken ref, → `{ ok: false }` so the caller aborts with exit 2.
1486
+ */
1487
+ async function maybeLoadAdapterScorer(args) {
1488
+ const ref = args.flag ?? args.config.review?.scorer?.ref;
1489
+ if (ref === undefined)
1490
+ return { ok: true };
1491
+ const source = args.flag !== undefined ? "cli-flag" : "manifest";
1492
+ const loaded = await loadAdapterScorer({ cwd: args.cwd, ref, backend: args.backend, source });
1493
+ if (!loaded.ok)
1494
+ return { ok: false, error: loaded.error };
1495
+ return { ok: true, scorer: { hooks: loaded.hooks, provenance: loaded.provenance } };
1496
+ }
1497
+ /** Terminal route hooks bag from a loaded scorer (deriveArtifacts is browser-only, dropped here). */
1498
+ function terminalScorerHooks(scorer) {
1499
+ const { hooks } = scorer;
1500
+ return {
1501
+ ...(hooks.score ? { score: hooks.score } : {}),
1502
+ ...(hooks.deriveFeedback ? { deriveFeedback: hooks.deriveFeedback } : {})
1503
+ };
1504
+ }
1505
+ /** Browser route hooks bag from a loaded scorer (score + deriveFeedback + deriveArtifacts). */
1506
+ function browserScorerHooks(scorer) {
1507
+ const { hooks } = scorer;
1508
+ return {
1509
+ ...(hooks.score ? { score: hooks.score } : {}),
1510
+ ...(hooks.deriveFeedback ? { deriveFeedback: hooks.deriveFeedback } : {}),
1511
+ ...(hooks.deriveArtifacts ? { deriveArtifacts: hooks.deriveArtifacts } : {})
1512
+ };
1513
+ }
1476
1514
  async function runLabCommand(args) {
1477
1515
  const resolved = await resolveLabManifest(args.options.cwd, args.lab);
1478
1516
  if (!resolved.ok) {
@@ -1508,6 +1546,33 @@ async function runLabCommand(args) {
1508
1546
  args.io.setExitCode(2);
1509
1547
  return;
1510
1548
  }
1549
+ // #316: resolve + load a config-declared/CLI-flagged adopter scorer FAIL-CLOSED, before any spend.
1550
+ // A declared gate that cannot load (bad ref, not found, load failure, no hooks, unsupported backend)
1551
+ // aborts with exit 2 rather than green-passing.
1552
+ const scorerLoad = await maybeLoadAdapterScorer({
1553
+ cwd: args.options.cwd,
1554
+ config,
1555
+ backend,
1556
+ flag: args.options.scorer
1557
+ });
1558
+ if (!scorerLoad.ok) {
1559
+ const result = {
1560
+ schema: "humanish.run-result.v1",
1561
+ ok: false,
1562
+ cwd: resolve(args.options.cwd),
1563
+ warnings: [],
1564
+ error: scorerLoad.error
1565
+ };
1566
+ writeResult(args.command, args.io, result, formatRunHuman);
1567
+ args.io.setExitCode(2);
1568
+ return;
1569
+ }
1570
+ const scorer = scorerLoad.scorer;
1571
+ if (scorer) {
1572
+ // Cross-repo guardrail: `humanish lab run` now import()s host JS named in the manifest. Surface it
1573
+ // visibly so the invoker (who may not be the manifest author) knows executable code just ran.
1574
+ args.io.writeErr(`warning: review scorer ${scorer.provenance.ref} (${scorer.provenance.source}) is executable host code loaded and run in-process — review it as code, not config.\n`);
1575
+ }
1511
1576
  switch (backend) {
1512
1577
  case "synthetic":
1513
1578
  await runSyntheticBackend({ ...args, config });
@@ -1519,19 +1584,19 @@ async function runLabCommand(args) {
1519
1584
  await runSmokeBackend({ ...args, config });
1520
1585
  return;
1521
1586
  case "cua":
1522
- await runCuaBackend({ ...args, config });
1587
+ await runCuaBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1523
1588
  return;
1524
1589
  case "scripted":
1525
1590
  await runScriptedBackend({ ...args, config });
1526
1591
  return;
1527
1592
  case "terminal":
1528
- await runTerminalBackend({ ...args, config });
1593
+ await runTerminalBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1529
1594
  return;
1530
1595
  case "shared-world":
1531
- await runSharedWorldBackend({ ...args, config });
1596
+ await runSharedWorldBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1532
1597
  return;
1533
1598
  case "concurrent-shared-world":
1534
- await runConcurrentSharedWorldBackend({ ...args, config });
1599
+ await runConcurrentSharedWorldBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1535
1600
  return;
1536
1601
  default:
1537
1602
  // Compile-time exhaustiveness: a future backend must be handled here, not silently no-op.
@@ -1718,6 +1783,7 @@ async function runCuaBackend(args) {
1718
1783
  }
1719
1784
  : {}),
1720
1785
  ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
1786
+ ...(args.scorer ? { cuaHooks: browserScorerHooks(args.scorer), scorerProvenance: args.scorer.provenance } : {}),
1721
1787
  ...(args.options.rerunFailedFrom === undefined
1722
1788
  ? {}
1723
1789
  : {
@@ -1853,7 +1919,8 @@ async function runTerminalBackend(args) {
1853
1919
  cwd: args.options.cwd,
1854
1920
  open: args.mode === "watch" ? false : shouldOpen,
1855
1921
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
1856
- ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
1922
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
1923
+ ...(args.scorer ? { terminalHooks: terminalScorerHooks(args.scorer), scorerProvenance: args.scorer.provenance } : {})
1857
1924
  });
1858
1925
  if (outcome.backend !== "terminal") {
1859
1926
  throw new Error(`Expected terminal backend, got ${outcome.backend}.`);
@@ -1885,7 +1952,8 @@ async function runSharedWorldBackend(args) {
1885
1952
  cwd: args.options.cwd,
1886
1953
  open: args.mode === "watch" ? false : shouldOpen,
1887
1954
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
1888
- ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
1955
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
1956
+ ...(args.scorer ? { sharedWorldHooks: browserScorerHooks(args.scorer), scorerProvenance: args.scorer.provenance } : {})
1889
1957
  });
1890
1958
  if (outcome.backend !== "shared-world") {
1891
1959
  throw new Error(`Expected shared-world backend, got ${outcome.backend}.`);
@@ -1975,7 +2043,8 @@ async function runConcurrentSharedWorldBackend(args) {
1975
2043
  }
1976
2044
  }
1977
2045
  : {}),
1978
- ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
2046
+ ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
2047
+ ...(args.scorer ? { sharedWorldHooks: browserScorerHooks(args.scorer), scorerProvenance: args.scorer.provenance } : {})
1979
2048
  });
1980
2049
  }
1981
2050
  catch (error) {