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
@@ -0,0 +1,73 @@
1
+ import type { LabListResult } from "./labs.js";
2
+ import type { RunDetail } from "./run-detail.js";
3
+ import type { ReadRunIndexOptions, RunIndexResult } from "./run-index.js";
4
+ import type { LaunchRunOptions, LaunchRunResult } from "./tui-launch.js";
5
+ /** The humanish version string shown in the frame, so a screenshot in a bug report is datable. */
6
+ export interface TuiVersionInfo {
7
+ cli: string;
8
+ }
9
+ /**
10
+ * What the surface may do to the project. Deliberately a small, explicit list rather than a handle
11
+ * to the whole library: the set of verbs a stakeholder surface can perform should be readable in
12
+ * one place, and anything absent here is something the TUI simply cannot do.
13
+ */
14
+ export interface TuiCapabilities {
15
+ /** Read every run in the project, cheapest source first. */
16
+ readRunIndex(cwd: string, options?: ReadRunIndexOptions): Promise<RunIndexResult>;
17
+ /**
18
+ * The labs DECLARED in this project. Listed separately from run history because neither side is
19
+ * the whole truth: a fresh project has manifests and no runs, and a long-lived one has runs from
20
+ * manifests since renamed or deleted.
21
+ */
22
+ listLabs(cwd: string): Promise<LabListResult>;
23
+ /**
24
+ * Start a run and return once it is running. The run is DETACHED: it outlives this surface, so
25
+ * quitting the TUI — or losing the connection it runs over — does not kill a study that costs
26
+ * real money. The surface then follows it through `status.json` like any other reader.
27
+ */
28
+ startRun(options: Omit<LaunchRunOptions, "spawn" | "cliPath" | "now">): Promise<LaunchRunResult>;
29
+ /** The tail of a launch log: the only account of a run that died before writing evidence. */
30
+ readLaunchLog(logPath: string): Promise<string>;
31
+ /**
32
+ * Who is in ONE run and what they are thinking. Opens that run's bundle, which the index
33
+ * deliberately does not — affordable because it is asked only for the run being watched.
34
+ * `null` when the run has not written a bundle yet.
35
+ */
36
+ readRunDetail(cwd: string, runId: string): Promise<RunDetail | null>;
37
+ }
38
+ export interface TuiOptions {
39
+ /** The project the surface is reading. Already resolved by the CLI. */
40
+ cwd: string;
41
+ version: TuiVersionInfo;
42
+ capabilities: TuiCapabilities;
43
+ /**
44
+ * Terminal streams, injected so a test can drive the surface without a real TTY. The CLI passes
45
+ * the real ones; both are known to be TTYs by the time this is called, because the command
46
+ * refuses to start otherwise.
47
+ */
48
+ stdin: NodeJS.ReadStream;
49
+ stdout: NodeJS.WriteStream;
50
+ /** Test seam: render one frame and resolve, instead of waiting for the operator to quit. */
51
+ exitAfterFirstFrame?: boolean;
52
+ }
53
+ /**
54
+ * Start the surface. Resolves with the process exit code when the operator quits — the TUI owns the
55
+ * screen until then, so the CLI must not write to stdout while this is pending.
56
+ */
57
+ export type StartTui = (options: TuiOptions) => Promise<number>;
58
+ /** The shape `dist/tui-app.js` exports. Asserted at the load boundary in program.ts. */
59
+ export interface TuiModule {
60
+ startTui: StartTui;
61
+ }
62
+ /** Node version the Ink runtime requires (ink@7 declares `engines.node >= 22`). */
63
+ export declare const TUI_MIN_NODE_MAJOR = 22;
64
+ /**
65
+ * Whether this runtime can host the surface. Kept here, beside the reason, so the CLI's refusal and
66
+ * `doctor`'s readiness row can never disagree about the answer.
67
+ */
68
+ export declare function nodeSupportsTui(versionString?: string): boolean;
69
+ /**
70
+ * Where the bundle sits relative to the compiled CLI. Shared so the command that loads it and the
71
+ * readiness row that reports it can never look in different places.
72
+ */
73
+ export declare function tuiBundleUrl(baseUrl: string): URL;
@@ -0,0 +1,32 @@
1
+ // The boundary between the CLI and the terminal UI (#455).
2
+ //
3
+ // `humanish tui` loads a PRE-BUILT bundle (dist/tui-app.js) that contains Ink, React and the
4
+ // screens — and nothing else. Everything the surface needs to KNOW is passed across this interface
5
+ // by the CLI, which imports it from the same modules every other command uses.
6
+ //
7
+ // The reason for the seam: a terminal UI that reads the filesystem itself would become a second
8
+ // implementation of "what is a run, which lab does it belong to, is it alive" — one that ships
9
+ // minified, is invisible to the root test suite, and drifts from `humanish runs` the first time
10
+ // either side changes. Injection keeps exactly one implementation, already unit-tested, and leaves
11
+ // the bundle a view layer that can be reasoned about as one.
12
+ //
13
+ // It also makes the UI testable without a terminal: a test hands `startTui` a fake index and reads
14
+ // the frames back.
15
+ /** Node version the Ink runtime requires (ink@7 declares `engines.node >= 22`). */
16
+ export const TUI_MIN_NODE_MAJOR = 22;
17
+ /**
18
+ * Whether this runtime can host the surface. Kept here, beside the reason, so the CLI's refusal and
19
+ * `doctor`'s readiness row can never disagree about the answer.
20
+ */
21
+ export function nodeSupportsTui(versionString = process.version) {
22
+ const major = Number.parseInt(versionString.replace(/^v/, "").split(".")[0] ?? "", 10);
23
+ return Number.isFinite(major) && major >= TUI_MIN_NODE_MAJOR;
24
+ }
25
+ /**
26
+ * Where the bundle sits relative to the compiled CLI. Shared so the command that loads it and the
27
+ * readiness row that reports it can never look in different places.
28
+ */
29
+ export function tuiBundleUrl(baseUrl) {
30
+ return new URL("./tui-app.js", baseUrl);
31
+ }
32
+ //# sourceMappingURL=tui-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-contract.js","sourceRoot":"","sources":["../src/tui-contract.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,6FAA6F;AAC7F,mGAAmG;AACnG,+EAA+E;AAC/E,EAAE;AACF,gGAAgG;AAChG,+FAA+F;AAC/F,gGAAgG;AAChG,mGAAmG;AACnG,6DAA6D;AAC7D,EAAE;AACF,mGAAmG;AACnG,mBAAmB;AAqEnB,mFAAmF;AACnF,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,gBAAwB,OAAO,CAAC,OAAO;IACrE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACvF,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,kBAAkB,CAAC;AAC/D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { spawn } from "node:child_process";
2
+ export declare function isSafeLabHandle(value: string): boolean;
3
+ export interface LaunchRunOptions {
4
+ cwd: string;
5
+ /** The manifest handle (filename stem), as `humanish lab run` takes it. */
6
+ lab: string;
7
+ mode: "dry-run" | "live";
8
+ /** Injected in tests; defaults to the real spawn. */
9
+ spawn?: typeof spawn;
10
+ /** Injected in tests; defaults to the CLI beside this module. */
11
+ cliPath?: string;
12
+ env?: NodeJS.ProcessEnv;
13
+ /** Injected clock so a test can pin the log filename. */
14
+ now?: () => Date;
15
+ }
16
+ export interface LaunchedRun {
17
+ /** The spawned CLI's pid. The run's `status.json` stamps the same value. */
18
+ pid: number;
19
+ /**
20
+ * When the launch happened. A pid ALONE cannot identify a run: pids are recycled by the OS and a
21
+ * finished run keeps its pid in `status.json` forever, so a week-old record can carry the pid the
22
+ * kernel just handed this child. Anything matching on pid must also require the record to be
23
+ * newer than this.
24
+ */
25
+ launchedAt: string;
26
+ /** Absolute path to the launch log; the only diagnosis when a run dies before writing evidence. */
27
+ logPath: string;
28
+ /** Exactly what was executed, so a failure can be reproduced by hand. */
29
+ command: readonly string[];
30
+ }
31
+ export type LaunchRunResult = {
32
+ ok: true;
33
+ run: LaunchedRun;
34
+ } | {
35
+ ok: false;
36
+ error: {
37
+ code: LaunchErrorCode;
38
+ message: string;
39
+ };
40
+ };
41
+ export type LaunchErrorCode = "HUMANISH_LAUNCH_INVALID_LAB" | "HUMANISH_LAUNCH_FAILED";
42
+ /**
43
+ * Start a run and return as soon as it is running. Never throws: a launch that cannot happen is a
44
+ * result the surface can render, not an exception that would tear down the screen.
45
+ */
46
+ export declare function launchRun(options: LaunchRunOptions): Promise<LaunchRunResult>;
47
+ /** Read the tail of a launch log — the only account of a run that died before writing evidence. */
48
+ export declare function readLaunchLogTail(logPath: string, maxBytes?: number): Promise<string>;
@@ -0,0 +1,159 @@
1
+ // Starting a run from the terminal surface (#455).
2
+ //
3
+ // THE RUN MUST OUTLIVE THE SURFACE. A study can take minutes and costs real money, so a run
4
+ // started from the TUI cannot be a child of the TUI's event loop: closing the surface, or losing
5
+ // the SSH session it is running over, must not kill it. So this spawns the CLI the same way a
6
+ // person would type it — detached, in its own process group, with its output going to a file
7
+ // rather than to a terminal that may be about to disappear.
8
+ //
9
+ // The surface then learns what happened the same way any other reader does: from `status.json`.
10
+ // It does not hold a handle to the run, parse its stdout, or track it in memory. That is what
11
+ // makes the surface restartable — quit the TUI mid-run, reopen it, and the run is still there,
12
+ // because the filesystem was always the source of truth rather than a process handle.
13
+ import { spawn } from "node:child_process";
14
+ import { constants as fsConstants } from "node:fs";
15
+ import { open } from "node:fs/promises";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { prepareManagedHumanishOutputDirectory } from "./selected-output-paths.js";
19
+ /**
20
+ * A lab handle is the manifest FILENAME, which is what `humanish lab run` resolves. Restricted to
21
+ * characters a manifest name can actually contain, and — the part that matters — never allowed to
22
+ * begin with `-`, because argv is positional: a lab called `--json` would otherwise be handed to
23
+ * the CLI as a flag. There is no shell involved, so this is the whole injection surface.
24
+ *
25
+ * A leading underscore IS allowed: `_wip.yaml` is an ordinary way to name a work-in-progress
26
+ * manifest, `humanish lab run _wip` resolves it, and refusing it here would leave the surface
27
+ * listing a lab it will not start. A leading dot stays out — that names a hidden file, not a lab.
28
+ */
29
+ const SAFE_LAB_HANDLE = /^[A-Za-z0-9_][A-Za-z0-9._:-]*$/;
30
+ export function isSafeLabHandle(value) {
31
+ return SAFE_LAB_HANDLE.test(value) && value.length <= 128;
32
+ }
33
+ /** Where the CLI lives, relative to this compiled module. */
34
+ function defaultCliPath() {
35
+ return fileURLToPath(new URL("./cli.js", import.meta.url));
36
+ }
37
+ /**
38
+ * Start a run and return as soon as it is running. Never throws: a launch that cannot happen is a
39
+ * result the surface can render, not an exception that would tear down the screen.
40
+ */
41
+ export async function launchRun(options) {
42
+ if (!isSafeLabHandle(options.lab)) {
43
+ return {
44
+ ok: false,
45
+ error: {
46
+ code: "HUMANISH_LAUNCH_INVALID_LAB",
47
+ message: `"${options.lab}" is not a usable lab handle. Run it by path with \`humanish lab run <path>\` instead.`
48
+ }
49
+ };
50
+ }
51
+ const cwd = path.resolve(options.cwd);
52
+ const now = options.now ?? (() => new Date());
53
+ const spawnFn = options.spawn ?? spawn;
54
+ const launchedAt = now().toISOString();
55
+ let logPath;
56
+ let handle;
57
+ try {
58
+ // Contained under `.humanish/`, through the same guard every other output path uses, so a
59
+ // symlinked directory cannot redirect the log somewhere outside the project.
60
+ const logDir = await prepareManagedHumanishOutputDirectory(cwd, "launches");
61
+ const stamp = launchedAt.replace(/[:.]/g, "-");
62
+ logPath = path.join(logDir.physicalPath, `${stamp}-${options.lab}.log`);
63
+ // O_NOFOLLOW so a symlink planted at this path cannot redirect a run's output — which may carry
64
+ // provider error text — outside the project, and cannot defeat the 0600 mode by pointing at a
65
+ // file that already exists with looser permissions. O_CREAT|O_APPEND keeps ordinary reuse
66
+ // working; only a symlink is refused (ELOOP).
67
+ handle = await open(logPath, fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_NOFOLLOW, 0o600);
68
+ }
69
+ catch (cause) {
70
+ return {
71
+ ok: false,
72
+ error: {
73
+ code: "HUMANISH_LAUNCH_FAILED",
74
+ message: `Could not open a launch log: ${cause instanceof Error ? cause.message : String(cause)}`
75
+ }
76
+ };
77
+ }
78
+ // `--json` because nothing reads this stream interactively; `--no-open` because the surface owns
79
+ // the operator's attention and must not have a browser thrown over it.
80
+ const args = [
81
+ options.cliPath ?? defaultCliPath(),
82
+ "lab",
83
+ "run",
84
+ "--cwd",
85
+ cwd,
86
+ "--json",
87
+ "--no-open",
88
+ ...(options.mode === "dry-run" ? ["--dry-run"] : []),
89
+ // `--` ends option parsing, so the handle can only ever be read as the positional argument.
90
+ "--",
91
+ options.lab
92
+ ];
93
+ const spawnOptions = {
94
+ cwd,
95
+ env: options.env ?? process.env,
96
+ // A new process group, so the SIGHUP that arrives when a terminal closes is not delivered here.
97
+ detached: true,
98
+ // stdin closed, output to the log: a detached process must never hold the terminal, and
99
+ // inheriting a pipe nobody reads is how a run blocks forever on a full buffer.
100
+ stdio: ["ignore", handle.fd, handle.fd]
101
+ };
102
+ try {
103
+ const child = spawnFn(process.execPath, args, spawnOptions);
104
+ // A spawn failure is delivered ASYNCHRONOUSLY as an 'error' event (EAGAIN, EMFILE, ENOMEM, a
105
+ // vanished node binary). An 'error' event with no listener is re-thrown by EventEmitter as an
106
+ // uncaught exception — which would tear down the whole surface, the one thing this module
107
+ // promises never to do. The run is already unref'd and unobserved, so recording it is all that
108
+ // is available; the operator learns about it from the launch log and the missing record.
109
+ child.on("error", () => {
110
+ // Deliberately empty: see above. The failure surfaces as a run that never reports in.
111
+ });
112
+ if (child.pid === undefined) {
113
+ await handle.close();
114
+ return {
115
+ ok: false,
116
+ error: { code: "HUMANISH_LAUNCH_FAILED", message: "The run process did not start." }
117
+ };
118
+ }
119
+ // Release the surface's hold: the parent can now exit whenever it likes and the run continues,
120
+ // reparented to init.
121
+ child.unref();
122
+ // The child owns the descriptor now.
123
+ await handle.close();
124
+ return {
125
+ ok: true,
126
+ run: { pid: child.pid, launchedAt, logPath, command: [process.execPath, ...args] }
127
+ };
128
+ }
129
+ catch (cause) {
130
+ await handle.close().catch(() => undefined);
131
+ return {
132
+ ok: false,
133
+ error: {
134
+ code: "HUMANISH_LAUNCH_FAILED",
135
+ message: cause instanceof Error ? cause.message : String(cause)
136
+ }
137
+ };
138
+ }
139
+ }
140
+ /** Read the tail of a launch log — the only account of a run that died before writing evidence. */
141
+ export async function readLaunchLogTail(logPath, maxBytes = 4_000) {
142
+ try {
143
+ const handle = await open(logPath, "r");
144
+ try {
145
+ const { size } = await handle.stat();
146
+ const start = Math.max(0, size - maxBytes);
147
+ const buffer = Buffer.alloc(Math.min(size, maxBytes));
148
+ await handle.read(buffer, 0, buffer.length, start);
149
+ return buffer.toString("utf8").trim();
150
+ }
151
+ finally {
152
+ await handle.close();
153
+ }
154
+ }
155
+ catch {
156
+ return "";
157
+ }
158
+ }
159
+ //# sourceMappingURL=tui-launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-launch.js","sourceRoot":"","sources":["../src/tui-launch.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,10 @@ runId: "<core run id>"
21
21
  mode: "dry-run|live"
22
22
  simCount: 1
23
23
  createdAt: "<ISO timestamp>"
24
+ lab: # optional, additive: which manifest produced this run
25
+ id: "<lab id>"
26
+ path: "humanish/labs/<lab id>.yaml"
27
+ origin: "committed|ignored|explicit"
24
28
  cwd: "[target-cwd]"
25
29
  artifactRoot: ".humanish/runs/<run-id>"
26
30
  source:
@@ -360,6 +364,10 @@ History entries use:
360
364
  schema: humanish.run-history-entry.v1
361
365
  runId: "<run-id>"
362
366
  createdAt: "<ISO timestamp>"
367
+ lab: # optional, additive: which manifest produced this run
368
+ id: "<lab id>"
369
+ path: "humanish/labs/<lab id>.yaml"
370
+ origin: "committed|ignored|explicit"
363
371
  mode: "dry-run|live"
364
372
  path: ".humanish/runs/<run-id>"
365
373
  ```
@@ -3,7 +3,7 @@
3
3
  Date: 2026-06-02 (current-state note updated 2026-07-14)
4
4
 
5
5
  Status: reference map for the major contracts shipped through source version
6
- `0.49.0`; it is not an exhaustive inventory of command/result envelopes. Exported types,
6
+ `0.50.0`; it is not an exhaustive inventory of command/result envelopes. Exported types,
7
7
  schema constants, parsers, and validators in `src/` are authoritative. Rows
8
8
  marked "reserved" name layering intent only — no code emits or validates them
9
9
  yet. Do not emit a reserved schema.
@@ -371,6 +371,14 @@ Core-owned fields:
371
371
  - `artifacts`
372
372
  - `review`
373
373
  - `feedbackCandidates`
374
+ - `lab` (optional, additive): which manifest produced the run —
375
+ `{ id, path?, origin? }`, where `origin` is `committed` (humanish/labs),
376
+ `ignored` (a local overlay), or `explicit` (a path the operator passed).
377
+ Absent on bundles written before this contract and on library callers who
378
+ hand a `LabConfig` directly — the run is then honestly lab-less rather than
379
+ guessed. Readers wanting attribution for an older bundle may fall back to
380
+ `inferLegacyLabId`, which reads only the historical
381
+ `persona.source = "lab:<id>"` convention and nothing else.
374
382
  - `subject` (optional, additive): structured subject provenance —
375
383
  `{ source: clone | app-url | local-tree, repo?, commit?, archiveSha256?,
376
384
  dirty?, envNames?, state }` where `state` is `{ provenance: seeded |
@@ -879,6 +887,67 @@ measure) and never grant a green pass (they surface as unmeasured). `verifyRun`
879
887
  fails closed when a live bundle lacks the cost ledger or no-spend proof, when the
880
888
  proof claims zero on a `null` line, or when known spend exceeds the declared cap.
881
889
 
890
+ ## Run Status (identity + liveness index)
891
+
892
+ `humanish.run-status.v1` — `status.json`, written inside each run directory by
893
+ every backend at run start, refreshed on a fixed cadence while the run is
894
+ alive, and finalized when it ends: `{ schema, runId, state: running |
895
+ finished, mode, lab?, pid, startedAt, updatedAt, completedAt?, outcome? }`.
896
+
897
+ It answers two questions the filesystem could not answer before: **which lab**
898
+ a run belongs to, and **whether it is still alive** — including for runs an
899
+ agent launched (`lab run --json`) or that were detached, which previously wrote
900
+ nothing at all until they completed.
901
+
902
+ It is a DERIVED INDEX, not evidence. `run.json` remains the evidence-of-record;
903
+ `verify` never gates on `status.json`, nothing in it is a claim about what a
904
+ participant did, and when the two disagree the bundle wins and the record is
905
+ rebuildable from it. A `running` record whose `updatedAt` is older than three
906
+ touch intervals is INTERRUPTED, not alive — a dropped connection or a killed
907
+ terminal leaves exactly that shape, and reading it as interrupted is the honest
908
+ outcome (`classifyRunStatus` is the one shared definition). Fields are
909
+ public-safe by construction: no hostname and no user paths, because a run
910
+ directory may be shared.
911
+
912
+ ## Run Index, Run Detail, And The Terminal Surface (#455)
913
+
914
+ Three derived projections that exist so a surface can list, classify and watch
915
+ runs without opening evidence for all of them. None is authoritative: `run.json`
916
+ remains the evidence-of-record, `verify` gates on none of these, and nothing in
917
+ them is a claim about what a participant did.
918
+
919
+ `humanish.run-index.v1` — the listing projection. One entry per run, read
920
+ cheapest-source-first: the `status.json` record, else the bundle, else the run
921
+ directory alone. `{ runId, derivedFrom: status | bundle | directory, liveness,
922
+ mode?, pid?, lab?, startedAt?, updatedAt?, completedAt?, verdict?,
923
+ participants?, estimatedCostUsd?, durationMs? }`. The point is cost: walking
924
+ every run tree and parsing every bundle measured 167ms on a 25-run project,
925
+ against 16ms cold and 2.8ms warm here, which is what makes a surface that
926
+ refreshes on a cadence affordable. `derivedFrom` is reported so a surprising row
927
+ can be traced to the file it came from. A run with receipts and no outcome is
928
+ `interrupted` — the shape a dropped connection leaves — and so is an
929
+ IN-PROGRESS bundle reached without a status record, because there is no
930
+ freshness to judge and "it started and nothing here says it finished" is the
931
+ honest reading.
932
+
933
+ `humanish.run-detail.v1` — the watching projection, for ONE run. Who is in it
934
+ and what they are thinking: `{ runId, participants: [{ id, label, personaId?,
935
+ traits, status?, completionReason?, thought?, turns?, actions?, thoughts?,
936
+ estimatedCostUsd? }], observerPath? }`. It reads the actor trace from
937
+ `stream.liveActor` while a run is in flight and `stream.actor` once it has
938
+ finished, preferring the live one, so a single screen renders a run the whole
939
+ way through. A reasoning item still being written is skipped rather than quoted
940
+ half-finished, and the text is carried verbatim — a surface may wrap it, nothing
941
+ paraphrases it. Unlike the index this DOES open the bundle, which is affordable
942
+ only because it is asked for the one run being watched.
943
+
944
+ `humanish.tui-result.v1` — what `humanish tui` emits when it refuses:
945
+ `{ schema, ok: false, error: { code, message } }` with `HUMANISH_TUI_REQUIRES_TTY`,
946
+ `HUMANISH_TUI_UNSUPPORTED_NODE`, or `HUMANISH_TUI_BUNDLE_MISSING`. Every other
947
+ command is built for an agent to drive; this one takes the screen and waits for
948
+ a person, so a non-interactive stdin or stdout fails closed naming the commands
949
+ that DO answer the question rather than rendering escape codes into a pipe.
950
+
882
951
  ## Run Cost Summary And Estimated Actor Cost
883
952
 
884
953
  The computer-use (CUA) lane surfaces an ADVISORY, additive cost ESTIMATE. It is
@@ -29,7 +29,7 @@ study completed, reproduced, and produced a real accessibility finding via a
29
29
  keyboard-first participant
30
30
  ([docs/goals/email-gated-signup/receipts/](email-gated-signup/receipts/)).
31
31
 
32
- ## Current Program Truth (source `0.49.0`)
32
+ ## Current Program Truth (source `0.50.0`)
33
33
 
34
34
  The package source and repository implementation in this tree agree on these
35
35
  points:
@@ -45,7 +45,8 @@ The immutable 2026-06-10 proof-roadmap packet is paired with a
45
45
  | Subject sources/routes | Six declared sources: `this-repo`, `clone`, `app-url`, `local-app`, `terminal-product`, and `local-tree`; support is route-specific and `this-repo` remains dry-run-only | One centralized run/resource lifecycle boundary across all routes |
46
46
  | Public proof | A legible four-persona Observer hero from a verified real public-application study (commit-pinned drawDB) shipped in the npm payload (`0.16.0`) | Coverage beyond a single studied subject; the stratified breadth panel remains unbuilt |
47
47
  | OSS meta-lab | Dry-run contract and separate disposable smoke harness | Live meta-lab execution; disabled until repository instructions and actor credentials have an isolated boundary |
48
- | Observer serving | `watch`/`observe` loopback servers plus `serve` — the run-library surface with loopback default, capability-link exposure, `share_ready`-gated open mode, and optional operator-run tunnel; streams never served remotely | A remote live-stream (`--live-streams`) design; a persistent capability-link store; a control plane that can start runs |
48
+ | Observer serving | `watch`/`observe` loopback servers plus `serve` — the run-library surface with loopback default, capability-link exposure, `share_ready`-gated open mode, and optional operator-run tunnel; streams never served remotely | A remote live-stream (`--live-streams`) design; a persistent capability-link store |
49
+ | Stakeholder terminal surface | `humanish tui` (`0.50.0`): labs -> lab -> run, arrow-key navigation, and starting a dry or live run from the lab screen. The run is DETACHED and outlives the terminal — live-proven by killing the terminal 42s into a real run that then ran on for ~3.5 minutes and finished `pass` at $0.639751. The run screen leads with the participant and their recorded thinking, live-proven mid-flight against a real computer-use run. Ships as one bundled file loaded on demand; refuses a non-interactive stdin/stdout naming the JSON commands instead | Cancelling a run from the surface; per-user persisted config (#470); export (#471); aggregate stats (#472); agent-authored labs (#473); a reusable persona panel (#474) |
49
50
  | Off-app comms | Vendor-neutral in-sandbox email/SMS catch, a minimal persona inbox surface, and digest-only `humanish.comms-thread.v1` evidence; wired into the computer-use and shared-world routes over both HTTP and SMTP; live-proven end to end on 2026-08-08 — a persona signed up for a public app, read the emailed link in its inbox, and reached the signed-in product (`docs/goals/email-gated-signup/receipts/signup-verify-live-2026-08-08.md`); the adopter-hosted / app-url ingress plane is wired on the CUA and concurrent external-public routes (#387/#380, 2026-08-11) | Real-provider delivery; a live adopter-hosted receipt |
50
51
 
51
52
  Capability proof and adopter replacement are different gates. A deterministic
@@ -2,7 +2,7 @@
2
2
 
3
3
  Status: public-safe contributor and agent ramp.
4
4
 
5
- Package/source version in this tree: `0.49.0` (2026-08-18). The Observer is phone-usable as a stated requirement (observer/AGENTS.md); interactive primitives start from Base UI. The Observer renderer is the observer/ workspace artifact only; the legacy string-concat renderer was deleted at cutover (#426), and rollback is a version pin to 0.42.0. The containment boundary introduced in
5
+ Package/source version in this tree: `0.50.0` (2026-08-20). The Observer is phone-usable as a stated requirement (observer/AGENTS.md); interactive primitives start from Base UI. The Observer renderer is the observer/ workspace artifact only; the legacy string-concat renderer was deleted at cutover (#426), and rollback is a version pin to 0.42.0. The containment boundary introduced in
6
6
  `0.15.1` remains in force: managed run and output paths bind to validated
7
7
  physical filesystem identities, and stored provider IDs are evidence, not
8
8
  cleanup authority. The bundled OSS meta-lab is dry-run only until
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Open-source-safe CLI for persona simulation, observer review, and public-safe feedback drafts.",
5
5
  "author": "Daniel G Wilson <daniel@danielgwilson.com>",
6
6
  "keywords": [
@@ -49,8 +49,8 @@
49
49
  "access": "public"
50
50
  },
51
51
  "scripts": {
52
- "build": "tsc -p tsconfig.build.json && pnpm --filter humanish-observer build && node --eval \"import('node:fs').then(({ chmodSync, copyFileSync }) => { chmodSync('dist/cli.js', 0o755); copyFileSync('observer/dist/index.html', 'dist/observer-app.html'); })\"",
53
- "check": "pnpm typecheck && pnpm test && pnpm build",
52
+ "build": "tsc -p tsconfig.build.json && pnpm --filter humanish-observer build && pnpm --filter humanish-tui build && node scripts/finish-build.mjs",
53
+ "check": "pnpm typecheck && pnpm test && pnpm tui:test && pnpm build && pnpm tui:smoke",
54
54
  "humanish": "tsx src/cli.ts",
55
55
  "pack:dry-run": "npm pack --dry-run",
56
56
  "prepack": "pnpm build",
@@ -58,7 +58,7 @@
58
58
  "release:check": "pnpm check && pnpm public-surface:scan && pnpm skill:check && npm pack --dry-run",
59
59
  "skill:check": "DISABLE_TELEMETRY=1 npx skills add . --list",
60
60
  "test": "vitest run",
61
- "typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter humanish-observer typecheck",
61
+ "typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter humanish-observer typecheck && pnpm --filter humanish-tui typecheck",
62
62
  "humanish:doctor": "pnpm humanish -- doctor",
63
63
  "humanish:run": "pnpm humanish -- run --dry-run",
64
64
  "humanish:watch": "pnpm humanish -- watch",
@@ -69,7 +69,10 @@
69
69
  "humanish:lab:list": "pnpm humanish -- lab list",
70
70
  "humanish:lab:oss": "pnpm humanish -- lab run oss",
71
71
  "humanish:lab:oss:ci": "pnpm humanish -- lab run oss --dry-run --json --no-open",
72
- "humanish:lab:oss:smoke": "pnpm humanish -- lab run oss-smoke"
72
+ "humanish:lab:oss:smoke": "pnpm humanish -- lab run oss-smoke",
73
+ "tui:typecheck": "pnpm --filter humanish-tui typecheck",
74
+ "tui:smoke": "node scripts/tui-smoke.mjs",
75
+ "tui:test": "pnpm --filter humanish-tui test"
73
76
  },
74
77
  "repository": {
75
78
  "type": "git",
@@ -21,6 +21,25 @@ assume broad inherited job env is safe. Stop before live provider spend,
21
21
  hosted execution, deploys, public tunnels, or GitHub mutation unless the user
22
22
  explicitly approves that exact action.
23
23
 
24
+ ## Not For You: `humanish tui`
25
+
26
+ `humanish tui` is a human-only surface. It takes over the terminal and waits for
27
+ keystrokes, so it will block you and produce nothing you can read. It refuses a
28
+ non-interactive stdin or stdout with `HUMANISH_TUI_REQUIRES_TTY` rather than
29
+ rendering escape codes into your transcript — but do not invoke it at all.
30
+
31
+ Everything it shows has a machine-readable equivalent, which is what you want:
32
+
33
+ | Instead of the TUI | Use |
34
+ | --- | --- |
35
+ | browsing labs | `npx humanish lab list --json` |
36
+ | browsing runs | `npx humanish runs --json` |
37
+ | starting a run | `npx humanish lab run <lab> --json --no-open` |
38
+ | a run's outcome | `npx humanish review --run <id> --json` |
39
+
40
+ If a human asks you to "open the TUI", tell them the command to type; do not run
41
+ it on their behalf.
42
+
24
43
  ## Setup Workflow
25
44
 
26
45
  1. Inspect public target-repo files only: `package.json`, docs, route/app