humanish 0.110.2 → 0.110.3

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 (52) hide show
  1. package/ARCHITECTURE.md +10 -15
  2. package/CHANGELOG.md +13 -0
  3. package/CONTEXT.md +1 -1
  4. package/dist/actors/computer-use/actor.js +2 -2
  5. package/dist/actors/computer-use/loop/ending.js +1 -1
  6. package/dist/actors/registry.js +1 -1
  7. package/dist/analysis/automatic-completion.js +8 -0
  8. package/dist/cli/commands/observe.js +3 -2
  9. package/dist/cli/commands/study-run.js +9 -19
  10. package/dist/cli/findings.d.ts +5 -0
  11. package/dist/cli/findings.js +21 -0
  12. package/dist/cli/io.d.ts +2 -1
  13. package/dist/cli/io.js +3 -2
  14. package/dist/cli/observer-follow.d.ts +6 -2
  15. package/dist/cli/observer-follow.js +15 -5
  16. package/dist/evidence/redaction.js +15 -4
  17. package/dist/routes/computer-use/participant-runs.d.ts +6 -9
  18. package/dist/routes/computer-use/participant-runs.js +16 -36
  19. package/dist/routes/computer-use/plan.d.ts +2 -2
  20. package/dist/routes/computer-use/plan.js +1 -1
  21. package/dist/routes/computer-use/route.d.ts +5 -18
  22. package/dist/routes/computer-use/route.js +17 -78
  23. package/dist/routes/computer-use/setup.d.ts +3 -2
  24. package/dist/routes/computer-use/types.d.ts +6 -10
  25. package/dist/routes/scripted/route.d.ts +4 -17
  26. package/dist/routes/scripted/route.js +10 -42
  27. package/dist/routes/scripted/types.d.ts +1 -6
  28. package/dist/routes/shared-world/route.d.ts +5 -11
  29. package/dist/routes/shared-world/route.js +24 -63
  30. package/dist/routes/shared-world/types.d.ts +2 -6
  31. package/dist/routes/terminal/route.d.ts +3 -13
  32. package/dist/routes/terminal/route.js +19 -59
  33. package/dist/routes/terminal/session.d.ts +2 -3
  34. package/dist/routes/terminal/session.js +2 -3
  35. package/dist/routes/terminal/types.d.ts +2 -7
  36. package/dist/run/route-shell.d.ts +60 -0
  37. package/dist/run/route-shell.js +51 -0
  38. package/dist/run/run.d.ts +6 -1
  39. package/dist/run/run.js +11 -2
  40. package/dist/run/status.d.ts +2 -0
  41. package/dist/run/status.js +5 -0
  42. package/dist/study/composition-rules.js +1 -1
  43. package/dist/study/init-templates.d.ts +1 -1
  44. package/dist/study/init-templates.js +43 -34
  45. package/dist/study/init.js +1 -1
  46. package/dist/study/parse/subject.js +1 -1
  47. package/docs/architecture/actor-contract.md +1 -1
  48. package/docs/architecture/external-public-shared-world.md +1 -1
  49. package/docs/architecture/terminal-product-route.md +13 -13
  50. package/docs/contracts/study-analysis.md +5 -2
  51. package/docs/product/automatic-analysis.md +1 -1
  52. package/package.json +1 -1
@@ -44,31 +44,16 @@
44
44
  // humanish never calls Sandbox.list to prove cleanup, so a shared operator key never reaches a
45
45
  // sandbox it did not create. A live run that cannot prove teardown fails closed.
46
46
  // Enforced in sandbox.ts.
47
- import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
48
- import { completeAutomaticAnalysis } from "../../analysis/automatic-completion.js";
49
47
  import path from "node:path";
50
- import { runScope } from "../../run/run.js";
51
- import { planTerminalStudy } from "./plan.js";
48
+ import {} from "../../run/run.js";
49
+ import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
50
+ import {} from "./plan.js";
52
51
  import { runDryTerminalStudy } from "./dry-run.js";
53
52
  import { checkLiveTerminalMachine, runLiveTerminalSession } from "./session.js";
54
53
  import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
55
54
  import { withLateScorer } from "../../study/route-inputs.js";
56
55
  import {} from "./types.js";
57
56
  import { studyResultIdentity } from "../../run/study-result.js";
58
- /**
59
- * The config-taking entry point. It plans, returns a refusal with the same envelope and analysis
60
- * record the route has always returned, and otherwise runs the plan.
61
- */
62
- export async function runTerminalProductStudy(options) {
63
- const { config, dryRun, ...input } = options;
64
- const planned = planTerminalStudy(config, {
65
- dryRun,
66
- hasCostProbe: input.deps?.costProbe !== undefined,
67
- });
68
- if (planned.ok)
69
- return runTerminalPlan(planned.plan, input);
70
- return terminalStudyRefusal(options, planned.refusal);
71
- }
72
57
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
73
58
  export function terminalStudyRefusal(options, refusal) {
74
59
  const { config, dryRun } = options;
@@ -83,60 +68,35 @@ export function terminalStudyRefusal(options, refusal) {
83
68
  warnings: [],
84
69
  error: { code: refusal.code, message: refusal.message },
85
70
  };
86
- // A refusal starts no run, so a declared or default analysis is recorded as skipped.
87
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
88
- return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, options, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
89
- }
90
- /**
91
- * Run a terminal plan. The run scope gives a direct library caller the same run lifetime the CLI
92
- * gets: whichever of its fail-closed exits the run takes, the run it started is closed, and only a
93
- * run that published its final bundle reaches automatic analysis.
94
- */
95
- export async function runTerminalPlan(plan, input) {
96
- const admission = await admitTerminalRun(plan, input);
97
- return admission.ok ? runAdmittedTerminalRun(admission.admitted, input) : admission.result;
71
+ return completeRefusalAnalysis(refused, config, options);
98
72
  }
99
73
  /**
100
74
  * runStudyWith's step for a terminal plan. It runs a live plan's local checks (checkLiveTerminalMachine)
101
75
  * before any run scope opens, so the CLI can present their refusal before it loads a declared
102
76
  * scorer, and returns the run that continues from them with that scorer.
103
77
  */
104
- export async function admitTerminalPlan(plan, input) {
105
- const admission = await admitTerminalRun(plan, input);
106
- if (!admission.ok)
107
- return { ok: false, outcome: terminalOutcome(admission.result) };
108
- return {
109
- ok: true,
110
- run: async (scorer) => terminalOutcome(await runAdmittedTerminalRun(admission.admitted, withLateScorer(input, scorer, terminalRouteScorer))),
111
- };
112
- }
113
- function terminalOutcome(result) {
114
- return { route: "terminal", result };
78
+ export function admitTerminalPlan(plan, input) {
79
+ return admitRoute({
80
+ route: "terminal",
81
+ analysis: plan.analysis,
82
+ input,
83
+ admit: () => admitTerminalRun(plan, input),
84
+ withScorer: (base, scorer) => withLateScorer(base, scorer, terminalRouteScorer),
85
+ runInScope: runTerminalPlanInScope,
86
+ });
115
87
  }
116
- /**
117
- * A live plan's local checks, made outside any run scope. A refusal is the result runTerminalPlan
118
- * returns for it, with the analysis record of a run that never started.
119
- */
120
- async function admitTerminalRun(plan, input) {
88
+ /** A live plan's local checks, made outside any run scope. */
89
+ function admitTerminalRun(plan, input) {
121
90
  const warnings = [];
122
91
  if (plan.dryRun)
123
92
  return { ok: true, admitted: { plan, warnings } };
124
93
  const checked = checkLiveTerminalMachine(plan, input, warnings);
125
94
  if (checked.ok)
126
95
  return { ok: true, admitted: { plan, warnings, runtimeEnv: checked.runtimeEnv } };
127
- const refused = terminalFailure(plan, input, warnings)(checked.code, checked.message);
128
- return { ok: false, result: await completeTerminalAnalysis(plan, input, refused, undefined) };
129
- }
130
- /** Runs an admitted plan in its own run scope, then its automatic analysis. */
131
- async function runAdmittedTerminalRun(admitted, input) {
132
- const { result, finished } = await runScope((scope) => runTerminalPlanInScope(admitted, input, scope));
133
- return completeTerminalAnalysis(admitted.plan, input, result, finished);
134
- }
135
- function completeTerminalAnalysis(plan, input, result, finished) {
136
- return completeAutomaticAnalysis(result, finished, plan.analysis?.config, input, {
137
- ...(plan.analysis === undefined ? {} : { trigger: plan.analysis.trigger }),
138
- preferLargerOutput: plan.analysis?.preferLargerOutput === true,
139
- });
96
+ return {
97
+ ok: false,
98
+ result: terminalFailure(plan, input, warnings)(checked.code, checked.message),
99
+ };
140
100
  }
141
101
  /** The route's envelope for a run that stops before its bundle. */
142
102
  function terminalFailure(plan, input, warnings) {
@@ -13,9 +13,8 @@ export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input:
13
13
  readonly message: string;
14
14
  };
15
15
  /**
16
- * The live in-sandbox agent session orchestrator (mirror of runCuaActorStudy's E2B branch). Enforces
17
- * the 8-point safety contract by construction; fails closed before any sandbox/key/spend on any
18
- * precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
16
+ * The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
17
+ * construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
19
18
  * the redacted terminal event stream + normalized transcript, the agent report, and the
20
19
  * provider-neutral actor trace; tears the sandbox down in a finally and proves the teardown.
21
20
  */
@@ -38,9 +38,8 @@ export function checkLiveTerminalMachine(plan, input, warnings) {
38
38
  return { ok: true, runtimeEnv };
39
39
  }
40
40
  /**
41
- * The live in-sandbox agent session orchestrator (mirror of runCuaActorStudy's E2B branch). Enforces
42
- * the 8-point safety contract by construction; fails closed before any sandbox/key/spend on any
43
- * precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
41
+ * The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
42
+ * construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
44
43
  * the redacted terminal event stream + normalized transcript, the agent report, and the
45
44
  * provider-neutral actor trace; tears the sandbox down in a finally and proves the teardown.
46
45
  */
@@ -6,7 +6,6 @@ import type { RunScope } from "../../run/run.js";
6
6
  import type { buildRuntimeAuth } from "./credentials.js";
7
7
  import type { TerminalPlan } from "../../study/plan-types.js";
8
8
  import type { StudyDeps } from "../../study/study-deps.js";
9
- import type { StudyConfig } from "../../study/types.js";
10
9
  import type { ObserverResult } from "../../observer/render.js";
11
10
  import { type RunAdapterScore, type RunBundle, type RunFeedbackCandidate, type RunScorerProvenance } from "../../run/bundle.js";
12
11
  import { type StudyResultIdentity } from "../../run/study-result.js";
@@ -97,15 +96,13 @@ export interface TerminalScorer {
97
96
  */
98
97
  deriveFeedback?: (ctx: TerminalProductScoringContext) => RunFeedbackCandidate[] | Promise<RunFeedbackCandidate[]>;
99
98
  }
100
- export interface RunTerminalProductStudyOptions {
99
+ /** What a terminal run takes besides its plan. The plan carries the config, dry run and study. */
100
+ export interface TerminalRunInput {
101
101
  /** Cancels post-run analysis only. */
102
102
  analysisSignal?: AbortSignal;
103
103
  /** Reports the analysis window to onEvent; built by normalizeRunStudyOptions. */
104
104
  emit?: (event: StudyEvent) => void;
105
105
  cwd: string;
106
- config: StudyConfig;
107
- /** Resolved upstream (scenario.mode + CLI override); defaults safe (dry-run). */
108
- dryRun: boolean;
109
106
  open?: boolean;
110
107
  runId?: string;
111
108
  /**
@@ -312,8 +309,6 @@ export interface TerminalLedgers {
312
309
  /** The no-spend proof derived from `cost`. Never an independent assertion. */
313
310
  noSpendProof: NoSpendProof;
314
311
  }
315
- /** What a terminal run takes besides its plan. The plan carries the config, dry run and study. */
316
- export type TerminalRunInput = Omit<RunTerminalProductStudyOptions, "config" | "dryRun">;
317
312
  /** A live terminal plan: its caps are the fail-closed ones planTerminalStudy required. */
318
313
  export type LiveTerminalPlan = Extract<TerminalPlan, {
319
314
  readonly dryRun: false;
@@ -0,0 +1,60 @@
1
+ import { type AnalysisInput } from "../analysis/automatic-completion.js";
2
+ import type { AutomaticAnalysisDeps } from "../analysis/automatic.js";
3
+ import type { AdmittedPlan, StudyResult } from "../run-study.js";
4
+ import type { PlannedAnalysis } from "../study/plan-types.js";
5
+ import type { LateScorer } from "../study/route-inputs.js";
6
+ import type { StudyRoute } from "../study/routing.js";
7
+ import type { StudyConfig } from "../study/types.js";
8
+ import { type RunScope } from "./run.js";
9
+ type ShellRoute = Exclude<StudyRoute, "preview">;
10
+ /** A route's local checks: the refused result, or what its run continues from. */
11
+ export type RouteAdmission<R extends ShellRoute, A> = {
12
+ readonly ok: false;
13
+ readonly result: StudyResult<R>;
14
+ } | {
15
+ readonly ok: true;
16
+ readonly admitted: A;
17
+ };
18
+ /** A route's own parts of its run. Everything around them is admitRoute's. */
19
+ export interface RouteShell<R extends ShellRoute, I extends AnalysisInput, A> {
20
+ readonly route: R;
21
+ /** The plan's automatic analysis, which both the refused result and the run complete. */
22
+ readonly analysis: PlannedAnalysis | undefined;
23
+ /** The route's input as the checks admitted it. */
24
+ readonly input: I;
25
+ /** The checks that need no scorer, made before any run scope opens. */
26
+ admit(): Promise<RouteAdmission<R, A>> | RouteAdmission<R, A>;
27
+ /** The run inside its run scope, with the input a late scorer joined. */
28
+ runInScope(admitted: A, input: I, scope: RunScope): Promise<StudyResult<R>>;
29
+ /** Layers a scorer the CLI loaded after the checks over the input. Scripted takes no scorer. */
30
+ readonly withScorer?: (input: I, scorer: LateScorer | undefined) => I;
31
+ /** Computer use's reason to skip analysis: a local VM study whose cleanup is unconfirmed. */
32
+ readonly analysisRefusal?: AutomaticAnalysisDeps["refusal"];
33
+ /**
34
+ * Computer use and shared world receive email. The run and its analysis share one scope for the
35
+ * secrets it registers. The other routes run outside any scope, where scrubbing changes nothing.
36
+ */
37
+ readonly commsSecrets?: boolean;
38
+ }
39
+ /**
40
+ * Runs a route's checks and returns the refusal they made or the route's run. A refusal carries the
41
+ * analysis record of a run that never started. The run opens its own run scope, so whichever exit
42
+ * it takes closes the run it started, and only a run that published its final bundle is analyzed.
43
+ */
44
+ export declare function admitRoute<R extends ShellRoute, I extends AnalysisInput, A>(shell: RouteShell<R, I, A>): Promise<AdmittedPlan<R>>;
45
+ /**
46
+ * A refused study's result with the analysis record a refusal gets: it starts no run, so a declared
47
+ * or default analysis is recorded as skipped.
48
+ */
49
+ export declare function completeRefusalAnalysis<T extends {
50
+ cwd: string;
51
+ runId: string;
52
+ dryRun: boolean;
53
+ }>(refused: T, config: StudyConfig, input: AnalysisInput): Promise<T>;
54
+ /** What a refused study's envelope reads besides the route's input. */
55
+ export interface RefusedStudy {
56
+ readonly config: StudyConfig;
57
+ /** The run's resolved dry run. */
58
+ readonly dryRun: boolean;
59
+ }
60
+ export {};
@@ -0,0 +1,51 @@
1
+ // The shell the computer-use, scripted, terminal and shared-world routes share. A route supplies
2
+ // its local checks and the body it runs inside the run scope. admitRoute completes the automatic
3
+ // analysis of a refusal and of a run, and wraps the result as the route's outcome, so a change to
4
+ // either is made here once. The preview route records no run and has no shell.
5
+ import { completeAutomaticAnalysis } from "../analysis/automatic-completion.js";
6
+ import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
7
+ import { runScope } from "./run.js";
8
+ import { withTransientCommsSecrets } from "./transient-comms-secrets.js";
9
+ /**
10
+ * Runs a route's checks and returns the refusal they made or the route's run. A refusal carries the
11
+ * analysis record of a run that never started. The run opens its own run scope, so whichever exit
12
+ * it takes closes the run it started, and only a run that published its final bundle is analyzed.
13
+ */
14
+ export async function admitRoute(shell) {
15
+ const complete = (input, result, finished) => completeAutomaticAnalysis(result, finished, shell.analysis?.config, input, {
16
+ ...(shell.analysis === undefined ? {} : { trigger: shell.analysis.trigger }),
17
+ preferLargerOutput: shell.analysis?.preferLargerOutput === true,
18
+ ...(shell.analysisRefusal === undefined ? {} : { refusal: shell.analysisRefusal }),
19
+ });
20
+ const admission = await shell.admit();
21
+ if (!admission.ok)
22
+ return {
23
+ ok: false,
24
+ outcome: routeOutcome(shell.route, await complete(shell.input, admission.result, undefined)),
25
+ };
26
+ const { admitted } = admission;
27
+ return {
28
+ ok: true,
29
+ run: async (scorer) => {
30
+ // The scorer joins the input only; whatever the checks admitted stays what the run uses.
31
+ const input = shell.withScorer?.(shell.input, scorer) ?? shell.input;
32
+ const run = async () => {
33
+ const { result, finished } = await runScope((scope) => shell.runInScope(admitted, input, scope));
34
+ return complete(input, result, finished);
35
+ };
36
+ return routeOutcome(shell.route, await (shell.commsSecrets ? withTransientCommsSecrets(run) : run()));
37
+ },
38
+ };
39
+ }
40
+ /**
41
+ * A refused study's result with the analysis record a refusal gets: it starts no run, so a declared
42
+ * or default analysis is recorded as skipped.
43
+ */
44
+ export function completeRefusalAnalysis(refused, config, input) {
45
+ const analysis = resolveAutomaticAnalysis(config.review?.analysis);
46
+ return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, input, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
47
+ }
48
+ /** A result as its route's outcome. StudyOutcome pairs each route with its result type. */
49
+ function routeOutcome(route, result) {
50
+ return { route, result };
51
+ }
package/dist/run/run.d.ts CHANGED
@@ -92,7 +92,12 @@ export declare class FinishedRun {
92
92
  readonly runId: string;
93
93
  /** The paths created by startRun and validated at the start of `finish`. */
94
94
  readonly paths: PreparedRunArtifactPaths;
95
- constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>);
95
+ constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean);
96
+ /**
97
+ * A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
98
+ * when asked, since the signal can arrive after the bundle was published.
99
+ */
100
+ get interrupted(): boolean;
96
101
  static isIssued(value: unknown): value is FinishedRun;
97
102
  /**
98
103
  * The run's ok and execution outcome as run.json records them: what the route passed to
package/dist/run/run.js CHANGED
@@ -27,11 +27,12 @@ const issueKey = Symbol("FinishedRun");
27
27
  export class FinishedRun {
28
28
  #observer;
29
29
  #recordFailure;
30
+ #interrupted;
30
31
  #outcome;
31
32
  runId;
32
33
  /** The paths created by startRun and validated at the start of `finish`. */
33
34
  paths;
34
- constructor(key, runId, paths, observer, outcome, recordFailure) {
35
+ constructor(key, runId, paths, observer, outcome, recordFailure, interrupted) {
35
36
  if (key !== issueKey)
36
37
  throw new Error("Only Run.finish issues a FinishedRun.");
37
38
  this.runId = runId;
@@ -39,6 +40,14 @@ export class FinishedRun {
39
40
  this.#observer = observer;
40
41
  this.#outcome = outcome;
41
42
  this.#recordFailure = recordFailure;
43
+ this.#interrupted = interrupted;
44
+ }
45
+ /**
46
+ * A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
47
+ * when asked, since the signal can arrive after the bundle was published.
48
+ */
49
+ get interrupted() {
50
+ return this.#interrupted();
42
51
  }
43
52
  static isIssued(value) {
44
53
  return typeof value === "object" && value !== null && #observer in value;
@@ -167,7 +176,7 @@ function runPublisher(args) {
167
176
  await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
168
177
  return current;
169
178
  }));
170
- return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure);
179
+ return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, () => runStatus.interrupted);
171
180
  };
172
181
  const interrupt = async (signal) => {
173
182
  interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
@@ -80,6 +80,8 @@ export interface RunStatusHandle {
80
80
  * `running` or `finished` over it. When the run had finished it writes nothing, waits for the
81
81
  * writes already queued and resolves false. */
82
82
  interrupt(signal: RunInterruptSignal): Promise<boolean>;
83
+ /** True from the moment `interrupt` accepts a signal, before its write lands. */
84
+ readonly interrupted: boolean;
83
85
  }
84
86
  export interface BeginRunStatusOptions {
85
87
  runId: string;
@@ -69,6 +69,7 @@ export function beginRunStatus(runPaths, options) {
69
69
  updatedAt: startedAt,
70
70
  };
71
71
  let finished = false;
72
+ let interrupted = false;
72
73
  let finishedRecord;
73
74
  let writing = Promise.resolve();
74
75
  const write = (record) => {
@@ -119,11 +120,15 @@ export function beginRunStatus(runPaths, options) {
119
120
  return false;
120
121
  }
121
122
  finished = true;
123
+ interrupted = true;
122
124
  clearInterval(timer);
123
125
  const completedAt = iso();
124
126
  await write({ ...base, state: "interrupted", updatedAt: completedAt, completedAt, signal });
125
127
  return true;
126
128
  },
129
+ get interrupted() {
130
+ return interrupted;
131
+ },
127
132
  };
128
133
  return handle;
129
134
  }
@@ -51,7 +51,7 @@ function thisRepoValidationReason(config) {
51
51
  // desktop), and no public-target policy (it is always loopback; the loopback shape was already
52
52
  // enforced in parseSubject). The actual "no inProcess executor supplied" case is inherently an
53
53
  // engine-time decision (the parser cannot know whether a library caller will pass one), so
54
- // it fails closed in runCuaActorStudy with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
54
+ // planComputerUseStudy refuses it with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
55
55
  function localAppValidationReason(config) {
56
56
  if (config.subject.source === "local-app") {
57
57
  const type = config.actors[0]?.type ?? "";
@@ -24,5 +24,5 @@ export declare const humanishScripts: Record<string, string>;
24
24
  * a signed-in Codex and no provider key, the study runs with the operator's own agent and needs
25
25
  * only E2B. Writing the other one there would hand someone homework instead of a first run.
26
26
  */
27
- export declare function starterFilesFor(actor: StarterActor, localBrowser?: LocalBrowserStarter, localAgent?: LocalAgentId): StarterFile[];
27
+ export declare function starterFilesFor(actor: StarterActor, localBrowser?: LocalBrowserStarter, localAgent?: LocalAgentId, command?: string): StarterFile[];
28
28
  export {};
@@ -34,6 +34,44 @@ defaults:
34
34
  `,
35
35
  };
36
36
  }
37
+ /**
38
+ * The `humanish/README.md` init writes. Its commands start with `command`, the invocation init
39
+ * chose for this install (src/cli/invocation.ts), as in the section init writes to `AGENTS.md`.
40
+ */
41
+ function readmeFile(command) {
42
+ return {
43
+ path: "humanish/README.md",
44
+ plane: "source",
45
+ contents: `# humanish
46
+
47
+ This directory holds the committed definitions of this app's humanish studies: study files,
48
+ personas and scenarios.
49
+
50
+ Keep this directory public-safe:
51
+
52
+ - synthetic personas only;
53
+ - synthetic fixtures only;
54
+ - env var names only, never values;
55
+ - no PII, PHI, secrets, raw private transcripts, private screenshots, customer data, or patient data.
56
+
57
+ Generated run bundles, screenshots, traces, logs, and local overrides belong in ignored \`.humanish/\`.
58
+
59
+ Studies:
60
+
61
+ - committed reusable studies live in humanish/studies/*.yaml;
62
+ - private or machine-local studies live in ignored .humanish/studies/*.yaml or
63
+ .humanish/local/studies/*.yaml;
64
+ - run a study with \`${command} run <study>\`, or \`${command} watch <study>\` to stay attached;
65
+ - files under humanish/labs/ move there with \`${command} migrate\`.
66
+
67
+ Format standard:
68
+
69
+ - human-authored humanish source uses .yaml;
70
+ - generated artifacts, synthetic fixtures, and event streams use .json or .ndjson;
71
+ - .yml is reserved for outside ecosystem files such as GitHub Actions, not humanish source.
72
+ `,
73
+ };
74
+ }
37
75
  /**
38
76
  * The flagship live study. A ChatGPT-account Codex participant has no API-dollar price, so preflight
39
77
  * refuses a dollar cap on it (HUMANISH_COMPUTER_USE_UNPRICED_CAP); that variant is bounded by the
@@ -113,38 +151,7 @@ defaults:
113
151
  };
114
152
  }
115
153
  export const starterFiles = [
116
- {
117
- path: "humanish/README.md",
118
- plane: "source",
119
- contents: `# humanish
120
-
121
- This directory holds the committed definitions of this app's humanish studies: study files,
122
- personas and scenarios.
123
-
124
- Keep this directory public-safe:
125
-
126
- - synthetic personas only;
127
- - synthetic fixtures only;
128
- - env var names only, never values;
129
- - no PII, PHI, secrets, raw private transcripts, private screenshots, customer data, or patient data.
130
-
131
- Generated run bundles, screenshots, traces, logs, and local overrides belong in ignored \`.humanish/\`.
132
-
133
- Studies:
134
-
135
- - committed reusable studies live in humanish/studies/*.yaml;
136
- - private or machine-local studies live in ignored .humanish/studies/*.yaml or
137
- .humanish/local/studies/*.yaml;
138
- - run a study with \`humanish run <study>\`, or \`humanish watch <study>\` to stay attached;
139
- - files under humanish/labs/ move there with \`humanish migrate\`.
140
-
141
- Format standard:
142
-
143
- - human-authored humanish source uses .yaml;
144
- - generated artifacts, synthetic fixtures, and event streams use .json or .ndjson;
145
- - .yml is reserved for outside ecosystem files such as GitHub Actions, not humanish source.
146
- `,
147
- },
154
+ readmeFile("humanish"),
148
155
  {
149
156
  path: "humanish/personas/synthetic-new-user.yaml",
150
157
  plane: "source",
@@ -317,10 +324,12 @@ export const humanishScripts = {
317
324
  * a signed-in Codex and no provider key, the study runs with the operator's own agent and needs
318
325
  * only E2B. Writing the other one there would hand someone homework instead of a first run.
319
326
  */
320
- export function starterFilesFor(actor, localBrowser = DEFAULT_LOCAL_BROWSER_STARTER, localAgent) {
327
+ export function starterFilesFor(actor, localBrowser = DEFAULT_LOCAL_BROWSER_STARTER, localAgent, command = "humanish") {
321
328
  return starterFiles.map((file) => file.path === "humanish/studies/local-browser.yaml"
322
329
  ? localBrowserStudy(localBrowser)
323
330
  : file.path === "humanish/studies/try-live.yaml"
324
331
  ? tryLiveStudy(actor, localAgent)
325
- : file);
332
+ : file.path === "humanish/README.md"
333
+ ? readmeFile(command)
334
+ : file);
326
335
  }
@@ -47,7 +47,7 @@ export async function runInit(options) {
47
47
  const machine = await firstRunEnvironment(options.env ?? process.env, requestedCwd);
48
48
  await planAgentsFile(preparedProjectRoot, cwd, plan, humanishCommand());
49
49
  const starterActor = starterActorFor(machine);
50
- await planStarterFiles(preparedProjectRoot, cwd, starterFilesFor(starterActor, localBrowser.value, starterLocalAgentFor(machine)), options.localBrowser !== undefined, plan);
50
+ await planStarterFiles(preparedProjectRoot, cwd, starterFilesFor(starterActor, localBrowser.value, starterLocalAgentFor(machine), machine.humanish), options.localBrowser !== undefined, plan);
51
51
  await planRuntimeDirectories(preparedProjectRoot, cwd, plan);
52
52
  const gitignorePlan = await planGitignore(preparedProjectRoot, cwd);
53
53
  plan.changes.push(gitignorePlan.change);
@@ -176,7 +176,7 @@ function parseServedApp(raw, subject) {
176
176
  return stateResult;
177
177
  }
178
178
  if (stateResult.value) {
179
- // Semantic validation is shared with the engine (runCuaActorStudy re-enforces it for
179
+ // Semantic validation is shared with the engine (planComputerUseStudy re-enforces it for
180
180
  // configs that arrive through the library API without the parser).
181
181
  const reason = subjectStateInvalidReason(stateResult.value, subject.env);
182
182
  if (reason) {
@@ -17,7 +17,7 @@ capture-vs-publish rule in
17
17
 
18
18
  `codex-exec` is a real dispatch key for terminal-product studies, but the exported
19
19
  descriptor `runSession` is a fail-closed compatibility entry. Live execution is
20
- owned by `runTerminalProductStudy`, which coordinates sandbox creation,
20
+ owned by `runLiveTerminalSession` (`src/routes/terminal/session.ts`), which coordinates sandbox creation,
21
21
  command-scoped runtime auth, evidence, caps, and by-id cleanup.
22
22
 
23
23
  The `pi-agent-core` and `claude-agent-sdk` descriptors, the `app` run kind and the
@@ -64,7 +64,7 @@ public `runComputerUseLoop` refuses them. The actor passes them through
64
64
  `runComputerUseLoopWithTaps`. A library caller wraps the executor's `observe` (for `url` and
65
65
  `screenshot`) or the provider's `nextTurn` (for `reasoning` and `message`).
66
66
 
67
- Flow, a host-first barrier inside `runConcurrentSharedWorld`'s fan-out:
67
+ Flow, a host-first barrier inside the shared-world route's fan-out:
68
68
 
69
69
  1. **Designated host.** Exactly one roster entry carries `host: true` (validated). Its mission = create
70
70
  the shared lobby; its browser opens `subject.appUrl`.
@@ -24,19 +24,19 @@ fail-closed cross-validation, and forward-declared warnings.
24
24
 
25
25
  ## The composition
26
26
 
27
- | Axis | Value |
28
- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | `subject.source` | `terminal-product` |
30
- | `subject.product` | `{ name, publicSurfaces[], install?, workdir?, upload? }`: the only world the agent sees |
31
- | `execution.target` | `e2b-terminal` (or absent → implied) |
32
- | `execution.terminal` | `{ transport: exec-stream, stdin: disabled }` |
33
- | `execution.runtimeAuth` | `openai-egress` (default) or opt-in `openai-env`; names-only durable evidence |
34
- | `execution.runtime.version` | Optional exact `@openai/codex` version; observed before keyed execution |
35
- | `actor.model` / `reasoningEffort` | Passed to Codex as `--model` (default `gpt-5.6-sol`) and `-c model_reasoning_effort`; declarations, not observed provider identity |
36
- | `caps` | `{ maxUsd, maxJobs, maxMinutes }`: the blast-radius budget; `maxUsd > 0` is refused: no adopter cost source exists until issue 347 lands |
37
- | `policies` | `allowPrivateRepoAccess` / `allowProviderCredentials` / `allowPaymentCredentials` / `allowGitHubMutation`, all default false |
38
- | `actor.type` | `codex-exec`: a registered terminal actor (`keyPlacement: in-sandbox-command-scoped`) |
39
- | route | `terminal` → `runTerminalProductStudy` ([`src/routes/terminal/route.ts`](https://github.com/danielgwilson/humanish/blob/main/src/routes/terminal/route.ts)) |
27
+ | Axis | Value |
28
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `subject.source` | `terminal-product` |
30
+ | `subject.product` | `{ name, publicSurfaces[], install?, workdir?, upload? }`: the only world the agent sees |
31
+ | `execution.target` | `e2b-terminal` (or absent → implied) |
32
+ | `execution.terminal` | `{ transport: exec-stream, stdin: disabled }` |
33
+ | `execution.runtimeAuth` | `openai-egress` (default) or opt-in `openai-env`; names-only durable evidence |
34
+ | `execution.runtime.version` | Optional exact `@openai/codex` version; observed before keyed execution |
35
+ | `actor.model` / `reasoningEffort` | Passed to Codex as `--model` (default `gpt-5.6-sol`) and `-c model_reasoning_effort`; declarations, not observed provider identity |
36
+ | `caps` | `{ maxUsd, maxJobs, maxMinutes }`: the blast-radius budget; `maxUsd > 0` is refused: no adopter cost source exists until issue 347 lands |
37
+ | `policies` | `allowPrivateRepoAccess` / `allowProviderCredentials` / `allowPaymentCredentials` / `allowGitHubMutation`, all default false |
38
+ | `actor.type` | `codex-exec`: a registered terminal actor (`keyPlacement: in-sandbox-command-scoped`) |
39
+ | route | `terminal` → `admitTerminalPlan` ([`src/routes/terminal/route.ts`](https://github.com/danielgwilson/humanish/blob/main/src/routes/terminal/route.ts)) |
40
40
 
41
41
  Routing is `routeOf` (`src/study/plan.ts`). It sends every `terminal-product` subject to
42
42
  this route, even with an unregistered actor, so this route refuses the actor.
@@ -321,10 +321,13 @@ The states without findings:
321
321
 
322
322
  - `running`: the automatic analysis is queued or running. `next` is `review` again.
323
323
  - `skipped`: the automatic analysis was skipped or refused, for example without
324
- `OPENAI_API_KEY`. `next` is `analyze --max-cost 3`.
324
+ `OPENAI_API_KEY`, or the run's study file sets `review.analysis: false`
325
+ (`AUTOMATIC_ANALYSIS_DISABLED`, read from the file when the view is built). `next` is
326
+ `analyze --max-cost 3`, which runs an analysis anyway.
325
327
  - `failed`: the latest attempt failed or was cancelled and no usable version exists. `next`
326
328
  reruns it with the same analyst, `--provider codex` for the Codex account analyst.
327
- - `none`: no analysis ran for this live run, for example with `review.analysis: false`.
329
+ - `none`: no analysis ran for this live run and nothing records why, for example a run that
330
+ started from a library call, or one whose study file has moved or changed id.
328
331
  - `dry_run`: a dry run has no participant evidence to analyze.
329
332
  - `unavailable`: the run or its analysis records could not be read or checked, or the analysis
330
333
  text matched a sensitive-text pattern (`ANALYSIS_SENSITIVE_TEXT_QUARANTINED`), which the
@@ -127,7 +127,7 @@ outcomes and the deterministic review verdict are never rewritten by analysis.
127
127
  | `AUTOMATIC_ANALYSIS_SOURCE_CHANGED` | `failed` | The run directory changed after the run published its bundle |
128
128
  | `AUTOMATIC_ANALYSIS_KEY_MISSING` | `skipped` | No OpenAI API key |
129
129
  | `AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE` | `skipped` | A default analysis found no participant evidence |
130
- | `AUTOMATIC_ANALYSIS_ACTOR_CANCELLED` | `skipped` | The harness cancelled the participants |
130
+ | `AUTOMATIC_ANALYSIS_ACTOR_CANCELLED` | `skipped` | The harness cancelled the participants, or a signal stopped the run |
131
131
  | `AUTOMATIC_ANALYSIS_CLEANUP_UNCONFIRMED` | `skipped` | A local VM study could not confirm its cleanup; the command exits 2 |
132
132
  | `AUTOMATIC_ANALYSIS_ALREADY_REQUESTED` | `skipped` | Another request already claimed this run's analysis |
133
133
  | `AUTOMATIC_ANALYSIS_BUSY` | `skipped` | Another analysis is running |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.110.2",
3
+ "version": "0.110.3",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",