humanish 0.55.0 → 0.57.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 (41) hide show
  1. package/dist/actor-contract.d.ts +3 -2
  2. package/dist/actor-contract.js.map +1 -1
  3. package/dist/agent-session.d.ts +11 -0
  4. package/dist/agent-session.js +43 -0
  5. package/dist/agent-session.js.map +1 -0
  6. package/dist/cua-actor-lab.d.ts +10 -0
  7. package/dist/cua-actor-lab.js +145 -21
  8. package/dist/cua-actor-lab.js.map +1 -1
  9. package/dist/e2b-terminal-lab.js +36 -1
  10. package/dist/e2b-terminal-lab.js.map +1 -1
  11. package/dist/lab-config.d.ts +29 -4
  12. package/dist/lab-config.js +47 -8
  13. package/dist/lab-config.js.map +1 -1
  14. package/dist/lab-engine.js +6 -0
  15. package/dist/lab-engine.js.map +1 -1
  16. package/dist/labs.d.ts +6 -0
  17. package/dist/labs.js +4 -1
  18. package/dist/labs.js.map +1 -1
  19. package/dist/program.d.ts +9 -0
  20. package/dist/program.js +79 -4
  21. package/dist/program.js.map +1 -1
  22. package/dist/reasoning-effort.js +4 -2
  23. package/dist/reasoning-effort.js.map +1 -1
  24. package/dist/run-projection.d.ts +4 -0
  25. package/dist/run-projection.js +1 -0
  26. package/dist/run-projection.js.map +1 -1
  27. package/dist/run.d.ts +9 -0
  28. package/dist/run.js +23 -15
  29. package/dist/run.js.map +1 -1
  30. package/dist/terminal-encoding.d.ts +17 -0
  31. package/dist/terminal-encoding.js +80 -0
  32. package/dist/terminal-encoding.js.map +1 -0
  33. package/dist/tui-app.js +118 -118
  34. package/dist/tui-contract.d.ts +17 -0
  35. package/dist/tui-contract.js +22 -0
  36. package/dist/tui-contract.js.map +1 -1
  37. package/docs/contracts/schemas.md +13 -5
  38. package/docs/goals/current.md +2 -2
  39. package/docs/principles/actor-fidelity.md +23 -1
  40. package/docs/ramp/README.md +1 -1
  41. package/package.json +1 -1
package/dist/program.js CHANGED
@@ -31,6 +31,8 @@ import { openObserverArtifact, stopRun } from "./tui-actions.js";
31
31
  import { readRunDetail } from "./run-detail.js";
32
32
  import { launchRun, readLaunchLogTail } from "./tui-launch.js";
33
33
  import { TUI_MIN_NODE_MAJOR, nodeSupportsTui, tuiBundleUrl } from "./tui-contract.js";
34
+ import { forTerminal } from "./terminal-encoding.js";
35
+ import { detectAgentSession } from "./agent-session.js";
34
36
  import { runCommsCatchHost } from "./comms-catch-host.js";
35
37
  import { DEFAULT_SANDBOX_CATCH_PORT } from "./comms-sandbox-catch.js";
36
38
  export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
@@ -43,9 +45,14 @@ const CLI_VERSION = readCliVersion();
43
45
  // Shared so the ~20 leaf commands that declare their own --json flag cannot drift
44
46
  // from each other in wording.
45
47
  const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
48
+ // Transcode ONLY for a terminal. A pipe carries bytes to another program — mangling those would
49
+ // corrupt a JSON payload for a reader that handles UTF-8 perfectly well — while a TTY carries them
50
+ // to a font, through a locale that may not decode them. See src/terminal-encoding.ts for what a
51
+ // participant actually read back off the screen.
52
+ const forStream = (stream, text) => stream.isTTY === true ? forTerminal(text) : text;
46
53
  const defaultIo = {
47
- writeOut: (text) => process.stdout.write(text),
48
- writeErr: (text) => process.stderr.write(text),
54
+ writeOut: (text) => process.stdout.write(forStream(process.stdout, text)),
55
+ writeErr: (text) => process.stderr.write(forStream(process.stderr, text)),
49
56
  setExitCode: (code) => {
50
57
  process.exitCode = code;
51
58
  }
@@ -142,6 +149,41 @@ function reportUnexpectedActionError(command, io, error) {
142
149
  }
143
150
  io.setExitCode(2);
144
151
  }
152
+ /** `unknown option '--x'` -> the sibling commands that DO declare `--x`. */
153
+ function commandsDeclaring(root, flag) {
154
+ const found = [];
155
+ const walk = (command, trail) => {
156
+ const names = [...trail, command.name()];
157
+ if (trail.length > 0 && command.options.some((option) => option.long === flag || option.short === flag)) {
158
+ found.push(names.slice(1).join(" "));
159
+ }
160
+ for (const child of command.commands)
161
+ walk(child, names);
162
+ };
163
+ walk(root, []);
164
+ return found;
165
+ }
166
+ /**
167
+ * Enrich commander's flag rejections with where the flag actually lives. A bare "unknown option"
168
+ * is accurate and unhelpful in the same way `no labs here yet` was: it reports a fact about this
169
+ * command and says nothing the reader can act on. Silence is preserved when no sibling has it —
170
+ * inventing a suggestion would be worse than none.
171
+ */
172
+ export function withSiblingFlagHint(text, root) {
173
+ const match = /unknown option '([^']+)'/.exec(text);
174
+ if (match === null)
175
+ return text;
176
+ const owners = commandsDeclaring(root, match[1]);
177
+ if (owners.length === 0)
178
+ return text;
179
+ // Truncation is REPORTED, never silent: a list that quietly drops owners would send a reader
180
+ // looking in the wrong place and think it had answered them.
181
+ const shown = owners.slice(0, 3);
182
+ const list = shown.map((owner) => `\`humanish ${owner}\``).join(", ");
183
+ const rest = owners.length - shown.length;
184
+ const tail = rest > 0 ? ` (and ${rest} more)` : "";
185
+ return `${text.replace(/\n+$/, "")}\n${match[1]} is an option of ${list}${tail}, not of this command.\n`;
186
+ }
145
187
  export function createProgram(io = {}) {
146
188
  const cliIo = { ...defaultIo, ...io };
147
189
  keyDiscoveryFn = io.keyDiscovery ?? discoverProviderKeys;
@@ -167,7 +209,14 @@ export function createProgram(io = {}) {
167
209
  .option("--json", "Print machine-readable JSON responses where supported.")
168
210
  .configureOutput({
169
211
  writeOut: (text) => cliIo.writeOut(text),
170
- writeErr: (text) => cliIo.writeErr(text)
212
+ writeErr: (text) => cliIo.writeErr(text),
213
+ // A rejected flag should name the command that WOULD have taken it. Found by a real
214
+ // first-contact study (labs/first-contact.yaml): a participant reached for
215
+ // `humanish run --no-open` by analogy with `lab run`, got a bare "unknown option", and
216
+ // filed it as a documentation mismatch. The flag is genuinely absent — `run` opens
217
+ // nothing — but "unknown" says that badly, because the reader's actual question is
218
+ // "then where does it live?".
219
+ outputError: (text, write) => write(withSiblingFlagHint(text, program))
171
220
  })
172
221
  .addHelpText("after", [
173
222
  "",
@@ -249,6 +298,7 @@ function registerDoctorCommand(parent, io) {
249
298
  const defaultTuiRuntime = {
250
299
  stdin: process.stdin,
251
300
  stdout: process.stdout,
301
+ env: process.env,
252
302
  nodeVersion: process.version,
253
303
  loadTui: async (bundle) => {
254
304
  if (!existsSync(bundle))
@@ -283,9 +333,31 @@ function registerTuiCommand(parent, io) {
283
333
  .description("Open the interactive terminal surface for browsing labs and runs (humans only).")
284
334
  .summary("Open the interactive terminal surface.")
285
335
  .option("--cwd <path>", "Target project directory.", ".")
336
+ .option("--force", "Open it anyway in a session that looks like an agent's.")
286
337
  .option("--json", JSON_OPTION_DESCRIPTION)
287
338
  .action(async (options, command) => {
288
339
  const { stdin, stdout } = tuiRuntime;
340
+ // An agent runner, even with a real terminal. `codex exec` allocates a PTY for the commands
341
+ // it runs, so the TTY check below passes and the surface used to open: a study watched an
342
+ // agent navigate the labs list and start a run it did not mean to start
343
+ // (labs/handed-a-human-surface.yaml). A TTY says a terminal exists, not that anyone is
344
+ // reading it. `--force` is the escape for the person who really is at this keyboard —
345
+ // capturing frames from inside an agent session is exactly that case.
346
+ const agent = options.force === true ? undefined : detectAgentSession(tuiRuntime.env);
347
+ if (agent !== undefined) {
348
+ refuseTui(command, io, {
349
+ schema: TUI_RESULT_SCHEMA,
350
+ ok: false,
351
+ error: {
352
+ code: "HUMANISH_TUI_AGENT_SESSION",
353
+ message: `humanish tui is a surface for a person, and ${agent.marker} says this session belongs to ${agent.runner}. `
354
+ + "It renders frames of escape codes into a transcript, and its keys can start runs. "
355
+ + "`humanish runs --json` lists runs, `humanish lab list --json` lists the studies in this project, "
356
+ + "and `humanish lab run <lab> --json` starts one. If you are a person at this keyboard, add --force."
357
+ }
358
+ });
359
+ return;
360
+ }
289
361
  if (stdin.isTTY !== true || stdout.isTTY !== true) {
290
362
  refuseTui(command, io, {
291
363
  schema: TUI_RESULT_SCHEMA,
@@ -2811,7 +2883,10 @@ function formatDoctorHuman(result) {
2811
2883
  return [
2812
2884
  `humanish doctor ${result.ok ? "ok" : "needs setup"}`,
2813
2885
  `cwd: ${result.cwd}`,
2814
- ...result.checks.map((check) => `- ${check.ok ? "ok" : "missing"} ${check.name}: ${check.message}`)
2886
+ // "missing" is a VERDICT, and a row that never ran has none. A participant reading doctor on a
2887
+ // fresh desktop got `- missing package.json: package.json is present and safe to read`, which
2888
+ // contradicts itself in eleven words (labs/tui-self-study.yaml).
2889
+ ...result.checks.map((check) => `- ${check.ok ? "ok" : check.checked === false ? "not checked" : "missing"} ${check.name}: ${check.message}`)
2815
2890
  ].join("\n") + "\n";
2816
2891
  }
2817
2892
  function formatRunHuman(result) {