humanish 0.49.0 → 0.50.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.
Files changed (59) hide show
  1. package/README.md +52 -3
  2. package/dist/concurrent-shared-world-lab.d.ts +5 -0
  3. package/dist/concurrent-shared-world-lab.js +28 -0
  4. package/dist/concurrent-shared-world-lab.js.map +1 -1
  5. package/dist/cua-actor-lab.d.ts +7 -0
  6. package/dist/cua-actor-lab.js +46 -2
  7. package/dist/cua-actor-lab.js.map +1 -1
  8. package/dist/e2b-terminal-lab.d.ts +7 -0
  9. package/dist/e2b-terminal-lab.js +54 -0
  10. package/dist/e2b-terminal-lab.js.map +1 -1
  11. package/dist/init-templates.js +7 -2
  12. package/dist/init-templates.js.map +1 -1
  13. package/dist/lab-engine.d.ts +11 -0
  14. package/dist/lab-engine.js +18 -0
  15. package/dist/lab-engine.js.map +1 -1
  16. package/dist/oss-lab.d.ts +3 -0
  17. package/dist/oss-lab.js.map +1 -1
  18. package/dist/oss-meta-lab.d.ts +3 -0
  19. package/dist/oss-meta-lab.js +13 -0
  20. package/dist/oss-meta-lab.js.map +1 -1
  21. package/dist/program.d.ts +14 -0
  22. package/dist/program.js +127 -9
  23. package/dist/program.js.map +1 -1
  24. package/dist/run-detail.d.ts +59 -0
  25. package/dist/run-detail.js +108 -0
  26. package/dist/run-detail.js.map +1 -0
  27. package/dist/run-index.d.ts +71 -0
  28. package/dist/run-index.js +206 -0
  29. package/dist/run-index.js.map +1 -0
  30. package/dist/run-paths.js +16 -1
  31. package/dist/run-paths.js.map +1 -1
  32. package/dist/run-projection.d.ts +182 -0
  33. package/dist/run-projection.js +349 -0
  34. package/dist/run-projection.js.map +1 -0
  35. package/dist/run-status.d.ts +139 -0
  36. package/dist/run-status.js +218 -0
  37. package/dist/run-status.js.map +1 -0
  38. package/dist/run.d.ts +15 -0
  39. package/dist/run.js +91 -6
  40. package/dist/run.js.map +1 -1
  41. package/dist/scripted-browser-lab.d.ts +5 -0
  42. package/dist/scripted-browser-lab.js +27 -0
  43. package/dist/scripted-browser-lab.js.map +1 -1
  44. package/dist/shared-world-lab.d.ts +5 -0
  45. package/dist/shared-world-lab.js +27 -0
  46. package/dist/shared-world-lab.js.map +1 -1
  47. package/dist/tui-app.js +402 -0
  48. package/dist/tui-contract.d.ts +73 -0
  49. package/dist/tui-contract.js +32 -0
  50. package/dist/tui-contract.js.map +1 -0
  51. package/dist/tui-launch.d.ts +48 -0
  52. package/dist/tui-launch.js +159 -0
  53. package/dist/tui-launch.js.map +1 -0
  54. package/docs/contracts/run-bundle.md +8 -0
  55. package/docs/contracts/schemas.md +70 -1
  56. package/docs/goals/current.md +3 -2
  57. package/docs/ramp/README.md +1 -1
  58. package/package.json +8 -5
  59. package/skills/humanish/SKILL.md +19 -0
package/dist/program.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { discoverProviderKeys } from "./key-resolution.js";
3
3
  import type { ObserverResult, ObserverServer } from "./observer.js";
4
4
  import type { OssMetaLabResult } from "./oss-meta-lab.js";
5
+ import { type TuiModule } from "./tui-contract.js";
5
6
  export declare const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
6
7
  export interface CliIo {
7
8
  writeOut(text: string): void;
@@ -18,7 +19,20 @@ export interface UnexpectedErrorEnvelope {
18
19
  }
19
20
  export declare function createProgram(io?: Partial<CliIo> & {
20
21
  keyDiscovery?: typeof discoverProviderKeys;
22
+ tuiRuntime?: Partial<TuiRuntime>;
21
23
  }): Command;
24
+ /**
25
+ * The pieces of the outside world the `tui` command touches. Injectable for the same reason
26
+ * `keyDiscovery` is: the refusals ARE the behavior worth testing, and a test cannot make a real
27
+ * TTY, an old Node, or a missing bundle appear.
28
+ */
29
+ export interface TuiRuntime {
30
+ stdin: NodeJS.ReadStream;
31
+ stdout: NodeJS.WriteStream;
32
+ nodeVersion: string;
33
+ /** Resolves the bundle, or null when it is not present. */
34
+ loadTui(bundle: URL): Promise<TuiModule | null>;
35
+ }
22
36
  /**
23
37
  * Default browser-open policy for a lab backend run. Mirrors the observe/watch gate:
24
38
  * an explicit --open/--no-open wins; --json (machine mode) never auto-opens; otherwise a
package/dist/program.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { formatOrientationHuman, readOrientation } from "./orientation.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -24,6 +24,10 @@ import { DEFAULT_OSS_REPOS, runOssLab } from "./oss-lab.js";
24
24
  import { cleanupOssMetaLabSandboxes, cleanupStaleOssMetaLabSandboxes, runOssMetaLab, startOssMetaLabLiveRefresh } from "./oss-meta-lab.js";
25
25
  import { cleanupRun, doctor, listRuns, readReview, runDryRun, verifyRun } from "./run.js";
26
26
  import { reclaimRunSandboxes } from "./reclaim.js";
27
+ import { RunIndexCache, readRunIndex } from "./run-index.js";
28
+ import { readRunDetail } from "./run-detail.js";
29
+ import { launchRun, readLaunchLogTail } from "./tui-launch.js";
30
+ import { TUI_MIN_NODE_MAJOR, nodeSupportsTui, tuiBundleUrl } from "./tui-contract.js";
27
31
  import { runCommsCatchHost } from "./comms-catch-host.js";
28
32
  import { DEFAULT_SANDBOX_CATCH_PORT } from "./comms-sandbox-catch.js";
29
33
  export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
@@ -138,6 +142,7 @@ function reportUnexpectedActionError(command, io, error) {
138
142
  export function createProgram(io = {}) {
139
143
  const cliIo = { ...defaultIo, ...io };
140
144
  keyDiscoveryFn = io.keyDiscovery ?? discoverProviderKeys;
145
+ tuiRuntime = { ...defaultTuiRuntime, ...io.tuiRuntime };
141
146
  const program = new HumanishCommand(undefined, cliIo);
142
147
  // Bare `humanish` orients instead of printing sixteen subcommands (#367). --help is untouched;
143
148
  // this is only what happens when no command was chosen at all.
@@ -179,6 +184,7 @@ export function createProgram(io = {}) {
179
184
  ].join("\n"));
180
185
  registerInitCommand(program, cliIo);
181
186
  registerDoctorCommand(program, cliIo);
187
+ registerTuiCommand(program, cliIo);
182
188
  registerKeysCommand(program, cliIo);
183
189
  registerRunCommand(program, cliIo);
184
190
  registerVerifyCommand(program, cliIo);
@@ -237,6 +243,104 @@ function registerDoctorCommand(parent, io) {
237
243
  io.setExitCode(result.ok ? 0 : 2);
238
244
  });
239
245
  }
246
+ const defaultTuiRuntime = {
247
+ stdin: process.stdin,
248
+ stdout: process.stdout,
249
+ nodeVersion: process.version,
250
+ loadTui: async (bundle) => {
251
+ if (!existsSync(bundle))
252
+ return null;
253
+ return (await import(bundle.href));
254
+ }
255
+ };
256
+ let tuiRuntime = defaultTuiRuntime;
257
+ const TUI_RESULT_SCHEMA = "humanish.tui-result.v1";
258
+ function refuseTui(command, io, refusal) {
259
+ if (wantsJson(command)) {
260
+ io.writeOut(`${JSON.stringify(refusal, null, 2)}\n`);
261
+ }
262
+ else {
263
+ io.writeErr(`${refusal.error.message}\n`);
264
+ }
265
+ markInvocationEnvelopeWritten(command);
266
+ io.setExitCode(2);
267
+ }
268
+ /**
269
+ * The stakeholder surface (#455). Every other command is written so an agent can drive it; this one
270
+ * is the opposite — it takes the screen and waits for a person.
271
+ *
272
+ * That inversion is why it refuses rather than degrades. An agent that runs `humanish tui` with a
273
+ * piped stdout has asked for something that cannot exist, and the useful answer is a structured
274
+ * error naming the command that WOULD have answered the question. A TUI that quietly rendered
275
+ * frames into a pipe would poison a transcript with escape codes and look like a hang.
276
+ */
277
+ function registerTuiCommand(parent, io) {
278
+ parent
279
+ .command("tui")
280
+ .description("Open the interactive terminal surface for browsing labs and runs (humans only).")
281
+ .summary("Open the interactive terminal surface.")
282
+ .option("--cwd <path>", "Target project directory.", ".")
283
+ .option("--json", JSON_OPTION_DESCRIPTION)
284
+ .action(async (options, command) => {
285
+ const { stdin, stdout } = tuiRuntime;
286
+ if (stdin.isTTY !== true || stdout.isTTY !== true) {
287
+ refuseTui(command, io, {
288
+ schema: TUI_RESULT_SCHEMA,
289
+ ok: false,
290
+ error: {
291
+ code: "HUMANISH_TUI_REQUIRES_TTY",
292
+ message: "humanish tui needs an interactive terminal. For scripted or agent use, `humanish runs --json` lists the same runs and `humanish lab run --json` starts one."
293
+ }
294
+ });
295
+ return;
296
+ }
297
+ if (!nodeSupportsTui(tuiRuntime.nodeVersion)) {
298
+ refuseTui(command, io, {
299
+ schema: TUI_RESULT_SCHEMA,
300
+ ok: false,
301
+ error: {
302
+ code: "HUMANISH_TUI_UNSUPPORTED_NODE",
303
+ message: `humanish tui needs Node ${TUI_MIN_NODE_MAJOR} or newer (this is ${tuiRuntime.nodeVersion}). Every other humanish command still works on this runtime.`
304
+ }
305
+ });
306
+ return;
307
+ }
308
+ // The Ink app ships as a pre-built bundle beside the compiled CLI and is loaded ONLY here, so
309
+ // no agent-facing command pays its parse cost.
310
+ const bundle = tuiBundleUrl(import.meta.url);
311
+ const runIndexCache = new RunIndexCache();
312
+ const loaded = await tuiRuntime.loadTui(bundle);
313
+ if (loaded === null) {
314
+ refuseTui(command, io, {
315
+ schema: TUI_RESULT_SCHEMA,
316
+ ok: false,
317
+ error: {
318
+ code: "HUMANISH_TUI_BUNDLE_MISSING",
319
+ message: `The terminal surface bundle is missing at ${bundle.pathname}. In a checkout, run \`pnpm build\`; in an install, this package is incomplete — please report it.`
320
+ }
321
+ });
322
+ return;
323
+ }
324
+ const exitCode = await loaded.startTui({
325
+ cwd: resolve(options.cwd),
326
+ version: { cli: CLI_VERSION },
327
+ capabilities: {
328
+ // One cache for the life of the surface: it refreshes on a cadence, and re-walking every
329
+ // run tree each tick is the cost this index exists to avoid.
330
+ readRunIndex: (target, readOptions) => readRunIndex(target, { ...readOptions, cache: runIndexCache }),
331
+ listLabs: listLabManifests,
332
+ startRun: launchRun,
333
+ readLaunchLog: readLaunchLogTail,
334
+ readRunDetail
335
+ },
336
+ stdin,
337
+ stdout
338
+ });
339
+ // The surface owned the screen; it has already told the operator whatever there was to say.
340
+ markInvocationEnvelopeWritten(command);
341
+ io.setExitCode(exitCode);
342
+ });
343
+ }
240
344
  // The discovery fn the env seam calls; injectable via createProgram for hermetic CLI tests.
241
345
  let keyDiscoveryFn = discoverProviderKeys;
242
346
  const KEYS_RESULT_SCHEMA = "humanish.keys-result.v1";
@@ -1755,6 +1859,11 @@ async function runLabCommand(args) {
1755
1859
  args.io.writeErr(`warning: ${warning}\n`);
1756
1860
  }
1757
1861
  const config = resolved.config;
1862
+ // The run's identity (#455): which manifest, where it lives, and whether it is committed, a
1863
+ // local overlay, or an explicit path. Resolved once here — the only place that knows all three —
1864
+ // and carried into the run's status record and bundle so the filesystem can answer "which lab
1865
+ // produced this run" without the old `persona.source = "lab:<id>"` string convention.
1866
+ const lab = { id: config.id, path: resolved.path, origin: resolved.origin };
1758
1867
  const backend = selectLabBackend(config);
1759
1868
  if (backend !== "cua" && labRerunFlagsRequested(args.options)) {
1760
1869
  writeUnsupportedRerunFlagsResult(args, backend);
@@ -1806,28 +1915,28 @@ async function runLabCommand(args) {
1806
1915
  }
1807
1916
  switch (backend) {
1808
1917
  case "synthetic":
1809
- await runSyntheticBackend({ ...args, config });
1918
+ await runSyntheticBackend({ ...args, config, labProvenance: lab });
1810
1919
  return;
1811
1920
  case "meta":
1812
- await runMetaBackend({ ...args, config });
1921
+ await runMetaBackend({ ...args, config, labProvenance: lab });
1813
1922
  return;
1814
1923
  case "smoke":
1815
- await runSmokeBackend({ ...args, config });
1924
+ await runSmokeBackend({ ...args, config, labProvenance: lab });
1816
1925
  return;
1817
1926
  case "cua":
1818
- await runCuaBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1927
+ await runCuaBackend({ ...args, config, labProvenance: lab, ...(scorer ? { scorer } : {}) });
1819
1928
  return;
1820
1929
  case "scripted":
1821
- await runScriptedBackend({ ...args, config });
1930
+ await runScriptedBackend({ ...args, config, labProvenance: lab });
1822
1931
  return;
1823
1932
  case "terminal":
1824
- await runTerminalBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1933
+ await runTerminalBackend({ ...args, config, labProvenance: lab, ...(scorer ? { scorer } : {}) });
1825
1934
  return;
1826
1935
  case "shared-world":
1827
- await runSharedWorldBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1936
+ await runSharedWorldBackend({ ...args, config, labProvenance: lab, ...(scorer ? { scorer } : {}) });
1828
1937
  return;
1829
1938
  case "concurrent-shared-world":
1830
- await runConcurrentSharedWorldBackend({ ...args, config, ...(scorer ? { scorer } : {}) });
1939
+ await runConcurrentSharedWorldBackend({ ...args, config, labProvenance: lab, ...(scorer ? { scorer } : {}) });
1831
1940
  return;
1832
1941
  default:
1833
1942
  // Compile-time exhaustiveness: a future backend must be handled here, not silently no-op.
@@ -1870,6 +1979,7 @@ async function runSyntheticBackend(args) {
1870
1979
  }
1871
1980
  const outcome = await runLab(args.config, {
1872
1981
  cwd: args.options.cwd,
1982
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
1873
1983
  count: simCount,
1874
1984
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
1875
1985
  ...(args.options.runId === undefined ? {} : { runId: args.options.runId })
@@ -1977,6 +2087,7 @@ async function runCuaBackend(args) {
1977
2087
  try {
1978
2088
  outcome = await runLab(args.config, {
1979
2089
  cwd: args.options.cwd,
2090
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
1980
2091
  // Watch mode opens the served Observer below (or prints the phone target under --expose)
1981
2092
  // instead of the static render — preserved byte-for-byte from the pre-0.18 open policy so a
1982
2093
  // non-follow watch (dry-run/--json) does not double-open. Run mode keeps the static open.
@@ -2112,6 +2223,7 @@ async function runScriptedBackend(args) {
2112
2223
  });
2113
2224
  const outcome = await runLab(args.config, {
2114
2225
  cwd: args.options.cwd,
2226
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2115
2227
  // Watch mode opens the served Observer below instead of the static render.
2116
2228
  open: args.mode === "watch" ? false : shouldOpen,
2117
2229
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
@@ -2148,6 +2260,7 @@ async function runTerminalBackend(args) {
2148
2260
  });
2149
2261
  const outcome = await runLab(args.config, {
2150
2262
  cwd: args.options.cwd,
2263
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2151
2264
  open: args.mode === "watch" ? false : shouldOpen,
2152
2265
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
2153
2266
  ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
@@ -2181,6 +2294,7 @@ async function runSharedWorldBackend(args) {
2181
2294
  });
2182
2295
  const outcome = await runLab(args.config, {
2183
2296
  cwd: args.options.cwd,
2297
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2184
2298
  open: args.mode === "watch" ? false : shouldOpen,
2185
2299
  ...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
2186
2300
  ...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
@@ -2262,6 +2376,7 @@ async function runConcurrentSharedWorldBackend(args) {
2262
2376
  try {
2263
2377
  outcome = await runLab(args.config, {
2264
2378
  cwd: args.options.cwd,
2379
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2265
2380
  open: wantsFollow ? false : shouldOpen,
2266
2381
  dryRun,
2267
2382
  ...(wantsFollow
@@ -2423,6 +2538,7 @@ async function runSmokeBackend(args) {
2423
2538
  const repos = labReposOverride(args.options);
2424
2539
  const outcome = await runLab(args.config, {
2425
2540
  cwd: args.options.cwd,
2541
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2426
2542
  count: limit,
2427
2543
  ...(repos === undefined ? {} : { repos }),
2428
2544
  ...(args.options.keep === undefined ? {} : { keep: args.options.keep }),
@@ -2479,6 +2595,8 @@ async function runMetaBackend(args) {
2479
2595
  try {
2480
2596
  const outcome = await runLab(args.config, {
2481
2597
  cwd: args.options.cwd,
2598
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2599
+ ...(args.labProvenance === undefined ? {} : { lab: args.labProvenance }),
2482
2600
  ...(wantsFollow ? { completionTimeoutMs: 0 } : {}),
2483
2601
  ...(codexAppServer === undefined ? {} : { codexAppServer }),
2484
2602
  ...(wantsFollow