humanish 0.110.1 → 0.110.2

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 (85) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +8 -7
  4. package/dist/actors/local-agent/cli.d.ts +6 -0
  5. package/dist/actors/local-agent/cli.js +8 -0
  6. package/dist/actors/local-agent/readiness.js +2 -1
  7. package/dist/cli/commands/observe.js +15 -2
  8. package/dist/cli/commands/run-signals.js +13 -4
  9. package/dist/cli/commands/runs.js +36 -13
  10. package/dist/cli/commands/setup.js +23 -14
  11. package/dist/cli/commands/study-route-computer-use.js +7 -3
  12. package/dist/cli/commands/study-route-open.js +1 -0
  13. package/dist/cli/commands/study-route-preview.js +1 -0
  14. package/dist/cli/commands/study-route-shared-world.js +4 -2
  15. package/dist/cli/commands/watch.js +14 -4
  16. package/dist/cli/doctor.js +9 -6
  17. package/dist/cli/findings.js +7 -6
  18. package/dist/cli/first-run-path.d.ts +27 -7
  19. package/dist/cli/first-run-path.js +112 -75
  20. package/dist/cli/invocation.d.ts +7 -0
  21. package/dist/cli/invocation.js +32 -0
  22. package/dist/cli/io.d.ts +3 -1
  23. package/dist/cli/io.js +6 -14
  24. package/dist/cli/observer-follow.d.ts +20 -0
  25. package/dist/cli/observer-follow.js +36 -1
  26. package/dist/cli/orientation.js +16 -14
  27. package/dist/cli/version.d.ts +1 -0
  28. package/dist/cli/version.js +11 -0
  29. package/dist/evidence/encoded-text.d.ts +9 -6
  30. package/dist/evidence/encoded-text.js +84 -19
  31. package/dist/evidence/redaction.d.ts +2 -0
  32. package/dist/evidence/redaction.js +123 -21
  33. package/dist/feedback/export-bundle.js +2 -2
  34. package/dist/keys/key-resolution.js +6 -5
  35. package/dist/observer/exposure.js +1 -1
  36. package/dist/observer/library.js +3 -2
  37. package/dist/observer/render.js +2 -1
  38. package/dist/observer/run-routes.js +8 -1
  39. package/dist/observer/serve.js +2 -1
  40. package/dist/routes/computer-use/plan.js +5 -0
  41. package/dist/routes/shared-world/plan.js +5 -1
  42. package/dist/routes/terminal/bundle.js +2 -1
  43. package/dist/routes/terminal/credentials.d.ts +5 -5
  44. package/dist/routes/terminal/credentials.js +14 -6
  45. package/dist/routes/terminal/plan.js +8 -0
  46. package/dist/routes/terminal/route.js +5 -4
  47. package/dist/routes/terminal/runtime-auth.d.ts +6 -0
  48. package/dist/routes/terminal/runtime-auth.js +5 -0
  49. package/dist/run/dry-run.js +2 -1
  50. package/dist/run/judge.js +2 -1
  51. package/dist/run/local-only-files.d.ts +13 -0
  52. package/dist/run/local-only-files.js +35 -0
  53. package/dist/run/reclaim.d.ts +5 -0
  54. package/dist/run/reclaim.js +34 -2
  55. package/dist/run/run-not-found.js +3 -2
  56. package/dist/run/sandbox-creates.js +5 -3
  57. package/dist/run/sandbox-receipts.d.ts +6 -5
  58. package/dist/run/sandbox-receipts.js +12 -7
  59. package/dist/study/agents-section.d.ts +21 -0
  60. package/dist/study/agents-section.js +86 -0
  61. package/dist/study/doctor.js +5 -4
  62. package/dist/study/init-templates.js +0 -95
  63. package/dist/study/init.js +31 -34
  64. package/dist/study/local-browser-host.d.ts +11 -0
  65. package/dist/study/local-browser-host.js +42 -0
  66. package/dist/study/parse/actors.js +9 -1
  67. package/dist/study/parse/subject.js +12 -0
  68. package/dist/study/parse/url-credentials.d.ts +9 -0
  69. package/dist/study/parse/url-credentials.js +50 -0
  70. package/dist/study/preflight-probes.js +2 -1
  71. package/dist/study/types.d.ts +8 -6
  72. package/dist/study/url-credentials.d.ts +6 -0
  73. package/dist/study/url-credentials.js +34 -0
  74. package/dist/substrates/e2b/sandbox.js +33 -15
  75. package/dist/substrates/local/firecracker-desktop.js +3 -2
  76. package/dist/substrates/local/runtime-host.js +5 -4
  77. package/dist/substrates/local/runtime.js +5 -4
  78. package/dist/tui/contract.js +5 -4
  79. package/dist/verify/liveness.js +2 -1
  80. package/dist/verify/verify.js +2 -1
  81. package/docs/architecture/terminal-product-route.md +14 -12
  82. package/docs/contracts/run-bundle.md +20 -4
  83. package/docs/contracts/schemas.md +6 -5
  84. package/package.json +4 -2
  85. package/skills/humanish/SKILL.md +22 -18
@@ -10,6 +10,7 @@ import { resolvePhysicalCwd, runIdOf } from "../run/paths.js";
10
10
  import { plural } from "../run/text.js";
11
11
  import { shellQuote } from "../substrates/shell.js";
12
12
  import { analysisOutcomeText, wantsJson } from "./io.js";
13
+ import { cli } from "./invocation.js";
13
14
  export const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
14
15
  /** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
15
16
  function cwdFlag(cwd) {
@@ -18,7 +19,7 @@ function cwdFlag(cwd) {
18
19
  return ` --cwd ${/^[\w@%+=:,./-]+$/.test(cwd) ? cwd : shellQuote(cwd)}`;
19
20
  }
20
21
  function analyzeCommand(source, provider) {
21
- const run = `humanish analyze --run ${source.runId}${source.cwdFlag}`;
22
+ const run = cli(`analyze --run ${source.runId}${source.cwdFlag}`);
22
23
  return provider === "codex"
23
24
  ? `${run} --provider codex`
24
25
  : `${run} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
@@ -132,10 +133,10 @@ export function analysisFindings(source) {
132
133
  };
133
134
  }
134
135
  if (loaded.warnings.includes("ANALYSIS_SENSITIVE_TEXT_QUARANTINED"))
135
- return withoutFindings(source, "unavailable", "ANALYSIS_SENSITIVE_TEXT_QUARANTINED", `The analysis text matched a sensitive-text pattern, so it is not printed. humanish analyze show --run ${source.runId}${source.cwdFlag} --json prints the raw record.`, null);
136
+ return withoutFindings(source, "unavailable", "ANALYSIS_SENSITIVE_TEXT_QUARANTINED", `The analysis text matched a sensitive-text pattern, so it is not printed. ${cli(`analyze show --run ${source.runId}${source.cwdFlag} --json`)} prints the raw record.`, null);
136
137
  const job = loaded.automatic;
137
138
  if (job?.state === "queued" || job?.state === "running")
138
- return withoutFindings(source, "running", null, "The automatic analysis is still running. Check again when it finishes.", `humanish review --run ${source.runId}${source.cwdFlag}`);
139
+ return withoutFindings(source, "running", null, "The automatic analysis is still running. Check again when it finishes.", cli(`review --run ${source.runId}${source.cwdFlag}`));
139
140
  if (analysis && analysis.result === null)
140
141
  return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, analysis.provider));
141
142
  if (job?.state === "failed" || job?.state === "cancelled") {
@@ -149,7 +150,7 @@ export function analysisFindings(source) {
149
150
  : `${analyzeCommand(source, undefined)}${refused ? " --dry-run" : ""}`);
150
151
  }
151
152
  if (job !== undefined || loaded.state === "invalid")
152
- return withoutFindings(source, "unavailable", job?.reason ?? loaded.warnings[0] ?? null, "This run's analysis records could not be read or checked, so no findings are shown.", `humanish analyze list --run ${source.runId}${source.cwdFlag}`);
153
+ return withoutFindings(source, "unavailable", job?.reason ?? loaded.warnings[0] ?? null, "This run's analysis records could not be read or checked, so no findings are shown.", cli(`analyze list --run ${source.runId}${source.cwdFlag}`));
153
154
  return withoutFindings(source, "none", null, "No analysis has run for this run.", analyzeCommand(source, undefined));
154
155
  }
155
156
  /**
@@ -179,7 +180,7 @@ export function missingRunFindings(cwd, run) {
179
180
  warnings: ["ANALYSIS_RUN_NOT_FOUND"],
180
181
  };
181
182
  const flag = cwdFlag(cwd);
182
- return withoutFindings({ runId: run, mode: undefined, runRoot: "", loaded, cwdFlag: flag }, "unavailable", "ANALYSIS_RUN_NOT_FOUND", `Run ${run} was not found or its storage is unsafe.`, `humanish runs${flag}`);
183
+ return withoutFindings({ runId: run, mode: undefined, runRoot: "", loaded, cwdFlag: flag }, "unavailable", "ANALYSIS_RUN_NOT_FOUND", `Run ${run} was not found or its storage is unsafe.`, cli(`runs${flag}`));
183
184
  }
184
185
  /** The findings of a run's selected analysis, or why it has none. */
185
186
  export async function readRunFindings(cwd, run, analysisId) {
@@ -307,7 +308,7 @@ export async function writeRunFindings(command, io, cwd, result) {
307
308
  return;
308
309
  try {
309
310
  const view = await readRunFindings(cwd, result.runId, analysisId);
310
- const full = `humanish review --run ${view.runId}${cwdFlag(cwd)}`;
311
+ const full = cli(`review --run ${view.runId}${cwdFlag(cwd)}`);
311
312
  io.writeOut(`${formatFindingsSummary(view, full).join("\n")}\n`);
312
313
  }
313
314
  catch {
@@ -25,10 +25,25 @@ export interface FirstRunEnvironment {
25
25
  hasProviderKey: boolean;
26
26
  /** The coding agents already signed in locally: Codex, Claude Code, or both. */
27
27
  localAgents: readonly SignedInAgent[];
28
- /** Host shape only; `doctor --study local-browser` owns exact read-only readiness. */
28
+ /**
29
+ * init's quick read of this host for a local browser study: Linux x64 with `/dev/kvm`,
30
+ * `/dev/net/tun` and Docker,
31
+ * or an M3-or-newer Mac. Undefined when it was not read. `doctor --study local-browser` owns the
32
+ * full check.
33
+ */
34
+ localBrowserHost?: LocalBrowserHost;
35
+ /** Host shape only, for the prerequisites a local step names. */
29
36
  platform?: NodeJS.Platform;
30
- arch?: string;
37
+ /** How a suggested command invokes humanish; `npx humanish` when unset. */
38
+ humanish?: string;
31
39
  }
40
+ /** Whether this host passed init's quick checks for a local browser study, and why not. */
41
+ export type LocalBrowserHost = {
42
+ ok: true;
43
+ } | {
44
+ ok: false;
45
+ reason: string;
46
+ };
32
47
  export type FirstRunActor = "openai-computer-use" | "local-agent";
33
48
  /**
34
49
  * Which brain the starter live study should be written for. A machine with a signed-in coding agent
@@ -41,7 +56,7 @@ export declare function starterActorFor(env: FirstRunEnvironment): FirstRunActor
41
56
  * refuse to run on a machine where only Claude Code is signed in.
42
57
  */
43
58
  export declare function starterLocalAgentFor(env: FirstRunEnvironment): LocalAgentId | undefined;
44
- /** Hosts that run local browser studies: Linux x64 and Apple Silicon Macs. */
59
+ /** Host shapes the local browser route is built for: Linux x64 and Apple Silicon Macs. */
45
60
  export declare function supportsLocalBrowser(platform: NodeJS.Platform | undefined, arch: string | undefined): boolean;
46
61
  export interface FirstRunStep {
47
62
  /** The exact command to run. */
@@ -50,14 +65,18 @@ export interface FirstRunStep {
50
65
  why: string;
51
66
  }
52
67
  /**
53
- * The next one or two commands, in order. Deliberately short: a list of twelve options is the same
54
- * as no guidance, and the reader here has just been handed twenty files.
68
+ * The next two or three commands, in order: the dry run, the hosted try-live study when keys or a
69
+ * signed-in agent allow it, and the local browser study where this host passed its quick checks.
70
+ * Deliberately short: a list of twelve options is the same as no guidance, and the reader here has
71
+ * just been handed a directory of new files.
55
72
  */
56
73
  export declare function firstRunSteps(env: FirstRunEnvironment): FirstRunStep[];
57
74
  /** The block init prints after its changes. */
58
75
  export declare function firstRunGuidance(env: FirstRunEnvironment): string[];
59
- /** Lets init recognise its own section without rewriting a file someone else wrote. */
76
+ /** Starts humanish's section of `AGENTS.md`, on its heading line. */
60
77
  export declare const AGENTS_SECTION_MARKER = "<!-- humanish:agents-guide -->";
78
+ /** Ends humanish's section, so init can replace the section and nothing after it. */
79
+ export declare const AGENTS_SECTION_END_MARKER = "<!-- /humanish:agents-guide -->";
61
80
  /**
62
81
  * What the next coding agent needs to know about humanish in this project.
63
82
  *
@@ -65,6 +84,7 @@ export declare const AGENTS_SECTION_MARKER = "<!-- humanish:agents-guide -->";
65
84
  * read on arrival. Increasingly the thing that ran `humanish init` was itself an agent working for
66
85
  * someone, and the agent that shows up tomorrow finds a `humanish/` directory with no idea what it
67
86
  * is for. Deliberately short and command-first, because an agent acts on the commands it is given.
87
+ * Commands use `humanish`, the prefix init chose for where humanish is installed.
68
88
  */
69
- export declare function agentsSection(): string;
89
+ export declare function agentsSection(humanish?: string): string;
70
90
  export {};
@@ -33,87 +33,111 @@ function preferredAgent(env) {
33
33
  export function starterLocalAgentFor(env) {
34
34
  return starterActorFor(env) === "local-agent" ? preferredAgent(env)?.id : undefined;
35
35
  }
36
- /**
37
- * How each hint invokes the CLI. \`npx humanish\` works from a project install and from an npx cache;
38
- * a dev-dependency install puts no \`humanish\` on \`PATH\`, so a bare name can reach a stale global one.
39
- */
40
- const HUMANISH = "npx humanish";
41
- /** Hosts that run local browser studies: Linux x64 and Apple Silicon Macs. */
36
+ /** Host shapes the local browser route is built for: Linux x64 and Apple Silicon Macs. */
42
37
  export function supportsLocalBrowser(platform, arch) {
43
38
  return (platform === "linux" && arch === "x64") || (platform === "darwin" && arch === "arm64");
44
39
  }
45
40
  /**
46
- * The next one or two commands, in order. Deliberately short: a list of twelve options is the same
47
- * as no guidance, and the reader here has just been handed twenty files.
41
+ * The next two or three commands, in order: the dry run, the hosted try-live study when keys or a
42
+ * signed-in agent allow it, and the local browser study where this host passed its quick checks.
43
+ * Deliberately short: a list of twelve options is the same as no guidance, and the reader here has
44
+ * just been handed a directory of new files.
48
45
  */
49
46
  export function firstRunSteps(env) {
47
+ const humanish = env.humanish ?? "npx humanish";
50
48
  const steps = [
51
49
  {
52
- command: `${HUMANISH} run first-run`,
50
+ command: `${humanish} run first-run`,
53
51
  why: "a dry run: no browser or model runs, no keys, no spend",
54
52
  },
55
53
  ];
56
- if (supportsLocalBrowser(env.platform, env.arch)) {
57
- const prerequisites = env.platform === "darwin"
58
- ? "M3-or-newer Mac, native ARM64 Node, Lima 2.2+, and a supported signed-in Codex CLI"
59
- : "local rootful Docker, KVM, TUN, and a supported signed-in Codex CLI";
60
- steps.push({
61
- command: `${HUMANISH} doctor --study local-browser`,
62
- why: `check the local browser study (${prerequisites}); no resources or quota used. Run \`${HUMANISH} runtime setup\` to prepare it, then start your app and run \`${HUMANISH} run local-browser\`; no E2B or model API key`,
63
- });
64
- return steps;
54
+ const local = localBrowserStep(env, humanish);
55
+ const hostedRunnable = env.hasE2bKey && (env.hasProviderKey || env.localAgents.length > 0);
56
+ if (hostedRunnable) {
57
+ steps.push(hostedStep(env, humanish, ""));
58
+ if (local !== undefined)
59
+ steps.push(local);
65
60
  }
66
- const localUnavailable = env.platform === undefined
67
- ? ""
68
- : " Local browsers are unavailable on this host; they support Linux x64 or M3-or-newer Apple Silicon Macs.";
69
- if (!env.hasE2bKey) {
70
- steps.push({
71
- command: `${HUMANISH} keys set e2b`,
72
- why: `the hosted starter needs an E2B desktop.${localUnavailable}`,
73
- });
74
- return steps;
61
+ else if (local !== undefined) {
62
+ steps.push(local);
75
63
  }
76
- if (env.hasProviderKey || env.localAgents.length > 0) {
77
- const brain = env.hasProviderKey
78
- ? "your provider key"
79
- : `${preferredAgent(env)?.label} (already signed in, so no API key is needed)`;
80
- // Everything the step needs, in one line. Splitting it across two commands means the second
81
- // one fails, which is the same dead end this guidance exists to remove.
82
- if (!env.hasDesktopSdk && env.desktopPeerCommand === undefined) {
83
- steps.push({
84
- command: `${HUMANISH} doctor`,
85
- why: "the hosted starter needs the desktop SDK, and installing it from here would remove what this directory's node_modules holds; doctor names the command to run from your project's directory",
86
- });
87
- return steps;
88
- }
89
- const command = env.hasDesktopSdk
90
- ? `${HUMANISH} run try-live`
91
- : `${env.desktopPeerCommand} && ${HUMANISH} run try-live`;
92
- steps.push({
93
- command,
94
- why: `a live study: one participant drives a real app in a hosted desktop, using ${brain}` +
95
- (env.hasDesktopSdk ? "" : " (the desktop SDK is an optional peer, so it installs first)") +
96
- (!env.hasProviderKey
97
- ? "; automatic findings analysis is skipped without OPENAI_API_KEY"
98
- : "") +
99
- localUnavailable,
100
- });
101
- return steps;
64
+ else {
65
+ steps.push(hostedStep(env, humanish, localUnavailableNote(env)));
102
66
  }
103
- steps.push({
104
- command: `${HUMANISH} keys set openai`,
105
- why: "openai-computer-use needs an API key; to use a Codex or Claude Code login instead, sign in and change the study to actor.type: local-agent" +
106
- localUnavailable,
107
- });
108
67
  return steps;
109
68
  }
69
+ /** The local browser step, where the host passed init's checks and Codex is signed in. */
70
+ function localBrowserStep(env, humanish) {
71
+ if (env.localBrowserHost?.ok !== true)
72
+ return undefined;
73
+ if (!env.localAgents.some((agent) => agent.id === "codex"))
74
+ return undefined;
75
+ const prerequisites = env.platform === "darwin"
76
+ ? "native ARM64 Node, Lima 2.2+, and a supported signed-in Codex CLI"
77
+ : "local rootful Docker, KVM, TUN, and a supported signed-in Codex CLI";
78
+ return {
79
+ command: `${humanish} doctor --study local-browser`,
80
+ why: `check the local browser study (${prerequisites}); no resources or quota used. Run \`${humanish} runtime setup\` to prepare it, then start your app and run \`${humanish} run local-browser\`; no E2B or model API key`,
81
+ };
82
+ }
83
+ /** Why the local route is not offered, appended to a key step so the reader knows it was considered. */
84
+ function localUnavailableNote(env) {
85
+ const host = env.localBrowserHost;
86
+ if (host === undefined)
87
+ return "";
88
+ if (!host.ok)
89
+ return ` Local browser studies are unavailable on this host: ${host.reason}.`;
90
+ return " A local browser study also runs on this host once Codex is signed in (`codex login`).";
91
+ }
92
+ /** The hosted try-live step, or the key or setup step it still needs. */
93
+ function hostedStep(env, humanish, note) {
94
+ if (!env.hasE2bKey) {
95
+ return {
96
+ command: `${humanish} keys set e2b`,
97
+ why: `the hosted starter needs an E2B desktop.${note}`,
98
+ };
99
+ }
100
+ if (!env.hasProviderKey && env.localAgents.length === 0) {
101
+ return {
102
+ command: `${humanish} keys set openai`,
103
+ why: "openai-computer-use needs an API key; to use a Codex or Claude Code login instead, sign in and change the study to actor.type: local-agent." +
104
+ note,
105
+ };
106
+ }
107
+ const brain = env.hasProviderKey
108
+ ? "your provider key"
109
+ : `${preferredAgent(env)?.label} (already signed in, so no API key is needed)`;
110
+ // Everything the step needs, in one line. Splitting it across two commands means the second
111
+ // one fails, which is the same dead end this guidance exists to remove.
112
+ if (!env.hasDesktopSdk && env.desktopPeerCommand === undefined) {
113
+ return {
114
+ command: `${humanish} doctor`,
115
+ why: "the hosted starter needs the desktop SDK, and installing it from here would remove what this directory's node_modules holds; doctor names the command to run from your project's directory",
116
+ };
117
+ }
118
+ // After the peer install, humanish is a dependency of this project, where `npx humanish` finds it.
119
+ const command = env.hasDesktopSdk
120
+ ? `${humanish} run try-live`
121
+ : `${env.desktopPeerCommand} && npx humanish run try-live`;
122
+ return {
123
+ command,
124
+ why: `a live study: one participant drives a real app in a hosted desktop, using ${brain}` +
125
+ (env.hasDesktopSdk ? "" : " (the desktop SDK is an optional peer, so it installs first)") +
126
+ (!env.hasProviderKey
127
+ ? "; automatic findings analysis is skipped without OPENAI_API_KEY"
128
+ : "") +
129
+ note,
130
+ };
131
+ }
110
132
  /** The block init prints after its changes. */
111
133
  export function firstRunGuidance(env) {
112
134
  const steps = firstRunSteps(env);
113
135
  return ["", "next:", ...steps.flatMap((step) => [` ${step.command}`, ` ${step.why}`])];
114
136
  }
115
- /** Lets init recognise its own section without rewriting a file someone else wrote. */
137
+ /** Starts humanish's section of `AGENTS.md`, on its heading line. */
116
138
  export const AGENTS_SECTION_MARKER = "<!-- humanish:agents-guide -->";
139
+ /** Ends humanish's section, so init can replace the section and nothing after it. */
140
+ export const AGENTS_SECTION_END_MARKER = "<!-- /humanish:agents-guide -->";
117
141
  /**
118
142
  * What the next coding agent needs to know about humanish in this project.
119
143
  *
@@ -121,8 +145,23 @@ export const AGENTS_SECTION_MARKER = "<!-- humanish:agents-guide -->";
121
145
  * read on arrival. Increasingly the thing that ran `humanish init` was itself an agent working for
122
146
  * someone, and the agent that shows up tomorrow finds a `humanish/` directory with no idea what it
123
147
  * is for. Deliberately short and command-first, because an agent acts on the commands it is given.
148
+ * Commands use `humanish`, the prefix init chose for where humanish is installed.
124
149
  */
125
- export function agentsSection() {
150
+ export function agentsSection(humanish = "npx humanish") {
151
+ const commands = [
152
+ ["study list --json", "the studies in this project"],
153
+ ["run first-run --no-open", "dry run: no browser, model, keys, or spend"],
154
+ ["doctor --study try-live", "what the hosted starter study still needs"],
155
+ [
156
+ "run try-live --no-open",
157
+ "demo app study: E2B plus the selected participant's authentication",
158
+ ],
159
+ ["doctor --study local-browser", "local Docker/Firecracker + Codex-account readiness"],
160
+ ["run local-browser --no-open", "your loopback app; no E2B or model API key"],
161
+ ["verify --run <id> --json", "is the evidence share-safe"],
162
+ ["review --run <id>", "the run's outcome and its analysis findings"],
163
+ ];
164
+ const width = Math.max(...commands.map(([command]) => command.length));
126
165
  return [
127
166
  "",
128
167
  `## humanish ${AGENTS_SECTION_MARKER}`,
@@ -130,30 +169,28 @@ export function agentsSection() {
130
169
  "This project uses humanish: synthetic participants use the product and leave evidence.",
131
170
  "",
132
171
  "```bash",
133
- "humanish doctor --study try-live # requirements for the selected participant and analysis",
134
- "humanish study list --json # the studies in this project",
135
- "humanish run first-run # dry run: no browser, model, keys, or spend",
136
- "humanish doctor --study local-browser # local Docker/Firecracker + Codex-account readiness",
137
- "humanish run local-browser # your loopback app; no E2B or model API key",
138
- "humanish run try-live # demo app study: E2B plus the selected participant's authentication",
139
- "humanish verify --run latest --json # is the evidence share-safe",
140
- "humanish review --run <id> # the run's outcome and its analysis findings",
172
+ ...commands.map(([command, why]) => `${humanish} ${command.padEnd(width)} # ${why}`),
141
173
  "```",
142
174
  "",
143
- "- After a live run, run `humanish review --run <id>` (or `--json`) and report its findings to the",
144
- " person you are working for: each one's title, impact, confidence, recovery and cited captures.",
145
- " When it shows no findings, report its message and the command it names.",
175
+ `- After a live run, run \`${humanish} review --run <id>\` (or \`--json\`) and report its findings`,
176
+ " to the person you are working for: each one's title, impact, confidence, recovery and cited",
177
+ " captures. When it shows no findings, report its message and the command it names. Every run",
178
+ " prints its id; `latest` moves with each run, dry runs included.",
146
179
  "- Studies are declared in `humanish/studies/*.yaml`. Edit `try-live.yaml`'s `subject` to point",
147
180
  " at this project's own app once you have seen a run work.",
148
- "- Configure the local study without editing YAML: `humanish init --yes --local-browser",
181
+ `- Configure the local study without editing YAML: \`${humanish} init --yes --local-browser`,
149
182
  ' http://127.0.0.1:3000 --local-mission "Complete the primary flow"` on first setup.',
150
183
  "- Evidence lands in gitignored `.humanish/runs/`. Never commit it, and never paste raw run",
151
- " bundles into an issue; `humanish feedback issue` writes a redacted, share-safe draft.",
152
- "- `humanish tui` is for people and refuses to run in an agent session. Use the `--json`",
153
- " commands above instead, and tell the person you are working for that `humanish tui` exists.",
184
+ ` bundles into an issue; \`${humanish} feedback issue\` writes a redacted, share-safe draft.`,
185
+ `- \`${humanish} tui\` is for people and refuses to run in an agent session. Use the commands`,
186
+ " above instead, and tell the person you are working for that the TUI exists.",
154
187
  "- A live run spends money. `caps.maxUsd` in each study caps estimated model spend: the run",
155
188
  " stops before its next request once the estimate passes it, so the last request can go over, and",
156
189
  " hosted desktop time is billed on top. Do not raise it without asking the person you are working for.",
190
+ `- \`${humanish} init --yes\` rewrites this section, between its two markers, to match the installed`,
191
+ " humanish. Write your own notes outside it.",
192
+ "",
193
+ AGENTS_SECTION_END_MARKER,
157
194
  "",
158
195
  ].join("\n");
159
196
  }
@@ -0,0 +1,7 @@
1
+ import { type HumanishInstall } from "../substrates/e2b/peer-install.js";
2
+ /** The prefix for a suggested command, by install. */
3
+ export declare function invocationFor(install: HumanishInstall, version: string): string;
4
+ /** How this process's suggested commands invoke humanish, read once per process. */
5
+ export declare function humanishCommand(): string;
6
+ /** A suggested command: `cli("review --run x")` is `npx humanish review --run x` in a project. */
7
+ export declare function cli(rest: string): string;
@@ -0,0 +1,32 @@
1
+ // How the commands humanish suggests should invoke humanish, chosen from where the running copy is
2
+ // installed. A dev-dependency install puts no `humanish` on `PATH`, so a bare name there reaches a
3
+ // stale global install when one exists, or nothing. `npx humanish` with no version, run where no
4
+ // project declares humanish, also resolves a global install before it fetches one. A one-shot run
5
+ // therefore names its own version, so the next command runs the same CLI.
6
+ import { humanishInstall } from "../substrates/e2b/peer-install.js";
7
+ import { CLI_VERSION } from "./version.js";
8
+ /** The prefix for a suggested command, by install. */
9
+ export function invocationFor(install, version) {
10
+ switch (install.kind) {
11
+ case "project":
12
+ case "other":
13
+ return "npx humanish";
14
+ case "one-shot":
15
+ return `npx humanish@${version}`;
16
+ // A global install is on `PATH`. A source checkout runs through a link or `pnpm humanish`, and
17
+ // keeping the bare name there keeps test output the same on every machine.
18
+ case "global":
19
+ case "checkout":
20
+ return "humanish";
21
+ }
22
+ }
23
+ let cached;
24
+ /** How this process's suggested commands invoke humanish, read once per process. */
25
+ export function humanishCommand() {
26
+ cached ??= invocationFor(humanishInstall(), CLI_VERSION);
27
+ return cached;
28
+ }
29
+ /** A suggested command: `cli("review --run x")` is `npx humanish review --run x` in a project. */
30
+ export function cli(rest) {
31
+ return `${humanishCommand()} ${rest}`;
32
+ }
package/dist/cli/io.d.ts CHANGED
@@ -3,7 +3,7 @@ import { discoverProviderKeys, type DotenvLoad } from "../keys/key-resolution.js
3
3
  import { type TelemetryProperties } from "./telemetry.js";
4
4
  import type { RunResult } from "../run/results.js";
5
5
  export declare const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
6
- export declare const CLI_VERSION: string;
6
+ export { CLI_VERSION } from "./version.js";
7
7
  export interface CliIo {
8
8
  writeOut(text: string): void;
9
9
  writeErr(text: string): void;
@@ -41,6 +41,8 @@ export interface StudyCommandOptions {
41
41
  port?: string | undefined;
42
42
  rerunFailedFrom?: string | undefined;
43
43
  runId?: string | undefined;
44
+ /** watch only: serve and follow the Observer even without an interactive terminal. */
45
+ serve?: boolean | undefined;
44
46
  /** Repo-relative path to an adopter scorer module; overrides review.scorer.ref when set. */
45
47
  scorer?: string | undefined;
46
48
  expose?: boolean | undefined;
package/dist/cli/io.js CHANGED
@@ -1,23 +1,14 @@
1
1
  import { automaticAnalysisSucceeded, defaultAnalysisOverBudget, } from "../analysis/automatic-completion.js";
2
2
  import { DEFAULT_ANALYSIS_MAX_COST_USD } from "../analysis/automatic-config.js";
3
- import { readFileSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
3
  import { Command, Option } from "commander";
7
4
  import { loadEnvFile, recordDotenvNames } from "../keys/env-file.js";
8
5
  import { discoverProviderKeys } from "../keys/key-resolution.js";
9
6
  import { deriveRunFacts } from "./telemetry.js";
10
7
  import { warnAndQueue, withQueuedWarnings } from "./deprecations.js";
11
8
  import { forTerminal } from "../routes/terminal/encoding.js";
9
+ import { cli } from "./invocation.js";
12
10
  export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
13
- function readCliVersion() {
14
- const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
15
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
16
- return typeof packageJson.version === "string" && packageJson.version.trim()
17
- ? packageJson.version
18
- : "0.0.0";
19
- }
20
- export const CLI_VERSION = readCliVersion();
11
+ export { CLI_VERSION } from "./version.js";
21
12
  // One text per shared flag, so the same flag reads the same on every command.
22
13
  // tests/cli/shared-options.test.ts walks the program and holds every use to these.
23
14
  export const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
@@ -147,11 +138,12 @@ export function parseParticipantIds(value) {
147
138
  * command: the run-not-found and missing-study messages do.
148
139
  */
149
140
  const NEXT_COMMAND = {
150
- HUMANISH_STUDY_NOT_FOUND: "humanish study list",
141
+ HUMANISH_STUDY_NOT_FOUND: "study list",
151
142
  };
152
143
  /** `<command> failed: <message>`, then `code: <CODE>`, then `next: <command>` when one is known. */
153
144
  export function formatCliError(command, error) {
154
- const next = error.code === undefined ? undefined : NEXT_COMMAND[error.code];
145
+ const rest = error.code === undefined ? undefined : NEXT_COMMAND[error.code];
146
+ const next = rest === undefined ? undefined : cli(rest);
155
147
  return ([
156
148
  `${command} failed: ${error.message}`,
157
149
  ...(error.code === undefined ? [] : [`code: ${error.code}`]),
@@ -316,7 +308,7 @@ function overBudgetAnalysisHint(result) {
316
308
  const estimate = result.automaticAnalysis?.result?.admission?.estimatedCostUsd ?? null;
317
309
  const suggested = Math.ceil(estimate ?? DEFAULT_ANALYSIS_MAX_COST_USD + 1);
318
310
  const shown = estimate === null ? "" : ` ($${estimate.toFixed(2)})`;
319
- return `analysis: its estimate${shown} is over the default $${DEFAULT_ANALYSIS_MAX_COST_USD} cap, so no request was sent. To analyze this run: humanish analyze --run ${result.runId ?? "latest"} --max-cost ${suggested}\n`;
311
+ return `analysis: its estimate${shown} is over the default $${DEFAULT_ANALYSIS_MAX_COST_USD} cap, so no request was sent. To analyze this run: ${cli(`analyze --run ${result.runId ?? "latest"} --max-cost ${suggested}`)}\n`;
320
312
  }
321
313
  export function automaticAnalysisEnvelope(result) {
322
314
  if (result === null ||
@@ -8,7 +8,26 @@ export interface ObserverPlan {
8
8
  /** Open the page: the served URL when following, the static file otherwise. */
9
9
  shouldOpen: boolean;
10
10
  wantsFollow: boolean;
11
+ /** Set when the command would have followed, but nobody is at a terminal to stop it. */
12
+ unattended?: boolean;
11
13
  }
14
+ /**
15
+ * Whether a person can stop a served Observer: stdin and stdout are terminals, and no agent runner
16
+ * claims the session. An agent's shell or a pipe has nobody to press Ctrl-C, so a server there
17
+ * waits forever. A TTY alone is not enough, since Codex gives the commands it runs a terminal.
18
+ */
19
+ export declare function personAtTerminal(env?: NodeJS.ProcessEnv): boolean;
20
+ /** Whether `watch` or `observe` serves the Observer and stays attached. */
21
+ export declare function followDecision(args: {
22
+ wantsMachine: boolean;
23
+ detach?: boolean | undefined;
24
+ serve?: boolean | undefined;
25
+ }): {
26
+ follow: boolean;
27
+ unattended: boolean;
28
+ };
29
+ /** The warning a command adds when it printed the Observer and exited because nobody was attached. */
30
+ export declare function unattendedObserverWarning(command: "watch" | "observe"): string;
12
31
  /**
13
32
  * Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
14
33
  * code 2 and returns null, so a caller refuses before starting a run.
@@ -20,6 +39,7 @@ export declare function planObserver(args: {
20
39
  io: CliIo;
21
40
  open?: boolean | undefined;
22
41
  port: string;
42
+ serve?: boolean | undefined;
23
43
  }): ObserverPlan | null;
24
44
  /** The `open` a run's static Observer render takes: a followed Observer opens its served URL. */
25
45
  export declare function staticObserverOpen(plan: ObserverPlan): boolean;
@@ -1,6 +1,32 @@
1
1
  import { Command } from "commander";
2
2
  import { serveObserver } from "../observer/render.js";
3
+ import { detectAgentSession } from "../actors/agent-session.js";
3
4
  import { formatRunHuman, parseObserverPort, wantsJson, writeResult, humanError, } from "./io.js";
5
+ /**
6
+ * Whether a person can stop a served Observer: stdin and stdout are terminals, and no agent runner
7
+ * claims the session. An agent's shell or a pipe has nobody to press Ctrl-C, so a server there
8
+ * waits forever. A TTY alone is not enough, since Codex gives the commands it runs a terminal.
9
+ */
10
+ export function personAtTerminal(env = process.env) {
11
+ return (process.stdin.isTTY === true &&
12
+ process.stdout.isTTY === true &&
13
+ detectAgentSession(env) === undefined);
14
+ }
15
+ /** Whether `watch` or `observe` serves the Observer and stays attached. */
16
+ export function followDecision(args) {
17
+ if (args.detach === true)
18
+ return { follow: false, unattended: false };
19
+ if (args.serve === true)
20
+ return { follow: true, unattended: false };
21
+ if (args.wantsMachine)
22
+ return { follow: false, unattended: false };
23
+ const attended = personAtTerminal();
24
+ return { follow: attended, unattended: !attended };
25
+ }
26
+ /** The warning a command adds when it printed the Observer and exited because nobody was attached. */
27
+ export function unattendedObserverWarning(command) {
28
+ return `No interactive terminal, so ${command} printed the Observer path and exited instead of serving it until Ctrl-C. Open the file, or pass --serve to keep serving.`;
29
+ }
4
30
  /**
5
31
  * Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
6
32
  * code 2 and returns null, so a caller refuses before starting a run.
@@ -28,7 +54,13 @@ export function planObserver(args) {
28
54
  : args.open === true
29
55
  ? true
30
56
  : !wantsMachine && process.stdout.isTTY === true;
31
- return { port, shouldOpen, wantsFollow: !wantsMachine && args.detach !== true };
57
+ const decision = followDecision({ wantsMachine, detach: args.detach, serve: args.serve });
58
+ return {
59
+ port,
60
+ shouldOpen,
61
+ wantsFollow: decision.follow,
62
+ ...(decision.unattended ? { unattended: true } : {}),
63
+ };
32
64
  }
33
65
  /** The `open` a run's static Observer render takes: a followed Observer opens its served URL. */
34
66
  export function staticObserverOpen(plan) {
@@ -43,6 +75,9 @@ export async function showObserver(args) {
43
75
  server = await serveObserver(rendered, { open: plan.shouldOpen, port: plan.port });
44
76
  result = withObserverServer(rendered, server);
45
77
  }
78
+ else if (rendered.ok && plan.unattended === true) {
79
+ result = { ...rendered, warnings: [...rendered.warnings, unattendedObserverWarning("watch")] };
80
+ }
46
81
  writeResult(args.command, args.io, result, formatObserverHuman);
47
82
  args.io.setExitCode(result.ok ? 0 : 2);
48
83
  if (result.ok && server) {