humanish 0.110.1 → 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 (121) hide show
  1. package/ARCHITECTURE.md +11 -16
  2. package/CHANGELOG.md +26 -0
  3. package/CONTEXT.md +1 -1
  4. package/README.md +8 -7
  5. package/dist/actors/computer-use/actor.js +2 -2
  6. package/dist/actors/computer-use/loop/ending.js +1 -1
  7. package/dist/actors/local-agent/cli.d.ts +6 -0
  8. package/dist/actors/local-agent/cli.js +8 -0
  9. package/dist/actors/local-agent/readiness.js +2 -1
  10. package/dist/actors/registry.js +1 -1
  11. package/dist/analysis/automatic-completion.js +8 -0
  12. package/dist/cli/commands/observe.js +18 -4
  13. package/dist/cli/commands/run-signals.js +13 -4
  14. package/dist/cli/commands/runs.js +36 -13
  15. package/dist/cli/commands/setup.js +23 -14
  16. package/dist/cli/commands/study-route-computer-use.js +7 -3
  17. package/dist/cli/commands/study-route-open.js +1 -0
  18. package/dist/cli/commands/study-route-preview.js +1 -0
  19. package/dist/cli/commands/study-route-shared-world.js +4 -2
  20. package/dist/cli/commands/study-run.js +9 -19
  21. package/dist/cli/commands/watch.js +14 -4
  22. package/dist/cli/doctor.js +9 -6
  23. package/dist/cli/findings.d.ts +5 -0
  24. package/dist/cli/findings.js +28 -6
  25. package/dist/cli/first-run-path.d.ts +27 -7
  26. package/dist/cli/first-run-path.js +112 -75
  27. package/dist/cli/invocation.d.ts +7 -0
  28. package/dist/cli/invocation.js +32 -0
  29. package/dist/cli/io.d.ts +5 -2
  30. package/dist/cli/io.js +9 -16
  31. package/dist/cli/observer-follow.d.ts +24 -0
  32. package/dist/cli/observer-follow.js +48 -3
  33. package/dist/cli/orientation.js +16 -14
  34. package/dist/cli/version.d.ts +1 -0
  35. package/dist/cli/version.js +11 -0
  36. package/dist/evidence/encoded-text.d.ts +9 -6
  37. package/dist/evidence/encoded-text.js +84 -19
  38. package/dist/evidence/redaction.d.ts +2 -0
  39. package/dist/evidence/redaction.js +134 -21
  40. package/dist/feedback/export-bundle.js +2 -2
  41. package/dist/keys/key-resolution.js +6 -5
  42. package/dist/observer/exposure.js +1 -1
  43. package/dist/observer/library.js +3 -2
  44. package/dist/observer/render.js +2 -1
  45. package/dist/observer/run-routes.js +8 -1
  46. package/dist/observer/serve.js +2 -1
  47. package/dist/routes/computer-use/participant-runs.d.ts +6 -9
  48. package/dist/routes/computer-use/participant-runs.js +16 -36
  49. package/dist/routes/computer-use/plan.d.ts +2 -2
  50. package/dist/routes/computer-use/plan.js +6 -1
  51. package/dist/routes/computer-use/route.d.ts +5 -18
  52. package/dist/routes/computer-use/route.js +17 -78
  53. package/dist/routes/computer-use/setup.d.ts +3 -2
  54. package/dist/routes/computer-use/types.d.ts +6 -10
  55. package/dist/routes/scripted/route.d.ts +4 -17
  56. package/dist/routes/scripted/route.js +10 -42
  57. package/dist/routes/scripted/types.d.ts +1 -6
  58. package/dist/routes/shared-world/plan.js +5 -1
  59. package/dist/routes/shared-world/route.d.ts +5 -11
  60. package/dist/routes/shared-world/route.js +24 -63
  61. package/dist/routes/shared-world/types.d.ts +2 -6
  62. package/dist/routes/terminal/bundle.js +2 -1
  63. package/dist/routes/terminal/credentials.d.ts +5 -5
  64. package/dist/routes/terminal/credentials.js +14 -6
  65. package/dist/routes/terminal/plan.js +8 -0
  66. package/dist/routes/terminal/route.d.ts +3 -13
  67. package/dist/routes/terminal/route.js +24 -63
  68. package/dist/routes/terminal/runtime-auth.d.ts +6 -0
  69. package/dist/routes/terminal/runtime-auth.js +5 -0
  70. package/dist/routes/terminal/session.d.ts +2 -3
  71. package/dist/routes/terminal/session.js +2 -3
  72. package/dist/routes/terminal/types.d.ts +2 -7
  73. package/dist/run/dry-run.js +2 -1
  74. package/dist/run/judge.js +2 -1
  75. package/dist/run/local-only-files.d.ts +13 -0
  76. package/dist/run/local-only-files.js +35 -0
  77. package/dist/run/reclaim.d.ts +5 -0
  78. package/dist/run/reclaim.js +34 -2
  79. package/dist/run/route-shell.d.ts +60 -0
  80. package/dist/run/route-shell.js +51 -0
  81. package/dist/run/run-not-found.js +3 -2
  82. package/dist/run/run.d.ts +6 -1
  83. package/dist/run/run.js +11 -2
  84. package/dist/run/sandbox-creates.js +5 -3
  85. package/dist/run/sandbox-receipts.d.ts +6 -5
  86. package/dist/run/sandbox-receipts.js +12 -7
  87. package/dist/run/status.d.ts +2 -0
  88. package/dist/run/status.js +5 -0
  89. package/dist/study/agents-section.d.ts +21 -0
  90. package/dist/study/agents-section.js +86 -0
  91. package/dist/study/composition-rules.js +1 -1
  92. package/dist/study/doctor.js +5 -4
  93. package/dist/study/init-templates.d.ts +1 -1
  94. package/dist/study/init-templates.js +43 -129
  95. package/dist/study/init.js +32 -35
  96. package/dist/study/local-browser-host.d.ts +11 -0
  97. package/dist/study/local-browser-host.js +42 -0
  98. package/dist/study/parse/actors.js +9 -1
  99. package/dist/study/parse/subject.js +13 -1
  100. package/dist/study/parse/url-credentials.d.ts +9 -0
  101. package/dist/study/parse/url-credentials.js +50 -0
  102. package/dist/study/preflight-probes.js +2 -1
  103. package/dist/study/types.d.ts +8 -6
  104. package/dist/study/url-credentials.d.ts +6 -0
  105. package/dist/study/url-credentials.js +34 -0
  106. package/dist/substrates/e2b/sandbox.js +33 -15
  107. package/dist/substrates/local/firecracker-desktop.js +3 -2
  108. package/dist/substrates/local/runtime-host.js +5 -4
  109. package/dist/substrates/local/runtime.js +5 -4
  110. package/dist/tui/contract.js +5 -4
  111. package/dist/verify/liveness.js +2 -1
  112. package/dist/verify/verify.js +2 -1
  113. package/docs/architecture/actor-contract.md +1 -1
  114. package/docs/architecture/external-public-shared-world.md +1 -1
  115. package/docs/architecture/terminal-product-route.md +26 -24
  116. package/docs/contracts/run-bundle.md +20 -4
  117. package/docs/contracts/schemas.md +6 -5
  118. package/docs/contracts/study-analysis.md +5 -2
  119. package/docs/product/automatic-analysis.md +1 -1
  120. package/package.json +4 -2
  121. package/skills/humanish/SKILL.md +22 -18
@@ -1,20 +1,10 @@
1
+ import { type RefusedStudy } from "../../run/route-shell.js";
1
2
  import { type TerminalRefusal } from "./plan.js";
2
3
  import type { TerminalPlan } from "../../study/plan-types.js";
3
4
  import type { AdmittedPlan } from "../../run-study.js";
4
- import { type RunTerminalProductStudyOptions, type TerminalProductStudyResult, type TerminalRunInput } from "./types.js";
5
- /**
6
- * The config-taking entry point. It plans, returns a refusal with the same envelope and analysis
7
- * record the route has always returned, and otherwise runs the plan.
8
- */
9
- export declare function runTerminalProductStudy(options: RunTerminalProductStudyOptions): Promise<TerminalProductStudyResult>;
5
+ import { type TerminalProductStudyResult, type TerminalRunInput } from "./types.js";
10
6
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
11
- export declare function terminalStudyRefusal(options: RunTerminalProductStudyOptions, refusal: TerminalRefusal): Promise<TerminalProductStudyResult>;
12
- /**
13
- * Run a terminal plan. The run scope gives a direct library caller the same run lifetime the CLI
14
- * gets: whichever of its fail-closed exits the run takes, the run it started is closed, and only a
15
- * run that published its final bundle reaches automatic analysis.
16
- */
17
- export declare function runTerminalPlan(plan: TerminalPlan, input: TerminalRunInput): Promise<TerminalProductStudyResult>;
7
+ export declare function terminalStudyRefusal(options: TerminalRunInput & RefusedStudy, refusal: TerminalRefusal): Promise<TerminalProductStudyResult>;
18
8
  /**
19
9
  * runStudyWith's step for a terminal plan. It runs a live plan's local checks (checkLiveTerminalMachine)
20
10
  * before any run scope opens, so the CLI can present their refusal before it loads a declared
@@ -11,10 +11,11 @@
11
11
  // The safety contract (`docs/history/goals/terminal-product-lane/goal.md`) is enforced by construction in
12
12
  // the files each item names, and checked by the verifier (verify/actor.ts
13
13
  // validateTerminalProductEvidence):
14
- // 1. Explicit key placement. openai-env (default) injects the raw runtime key command-scoped,
15
- // never through Sandbox.create({envs}). Opt-in openai-egress sends it only in the host-side E2B
16
- // header transform and passes an inert command placeholder. The proxy is spendable by every
17
- // sandbox process from creation; this protects the raw key, not provider spending.
14
+ // 1. Explicit key placement. openai-egress (default) sends the raw runtime key only in the
15
+ // host-side E2B header transform and passes an inert command placeholder. The proxy is
16
+ // spendable by every sandbox process from creation; this protects the raw key, not provider
17
+ // spending. Opt-in openai-env injects the raw key command-scoped, never through
18
+ // Sandbox.create({envs}).
18
19
  // Enforced in credentials.ts and runtime-auth.ts.
19
20
  // 2. Fail-closed cap. The live key is never exercised without scenario.caps in force: maxUsd
20
21
  // (default/require 0 = no-spend) + maxMinutes (wall-clock kill of the codex command).
@@ -43,31 +44,16 @@
43
44
  // humanish never calls Sandbox.list to prove cleanup, so a shared operator key never reaches a
44
45
  // sandbox it did not create. A live run that cannot prove teardown fails closed.
45
46
  // Enforced in sandbox.ts.
46
- import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
47
- import { completeAutomaticAnalysis } from "../../analysis/automatic-completion.js";
48
47
  import path from "node:path";
49
- import { runScope } from "../../run/run.js";
50
- 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";
51
51
  import { runDryTerminalStudy } from "./dry-run.js";
52
52
  import { checkLiveTerminalMachine, runLiveTerminalSession } from "./session.js";
53
53
  import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
54
54
  import { withLateScorer } from "../../study/route-inputs.js";
55
55
  import {} from "./types.js";
56
56
  import { studyResultIdentity } from "../../run/study-result.js";
57
- /**
58
- * The config-taking entry point. It plans, returns a refusal with the same envelope and analysis
59
- * record the route has always returned, and otherwise runs the plan.
60
- */
61
- export async function runTerminalProductStudy(options) {
62
- const { config, dryRun, ...input } = options;
63
- const planned = planTerminalStudy(config, {
64
- dryRun,
65
- hasCostProbe: input.deps?.costProbe !== undefined,
66
- });
67
- if (planned.ok)
68
- return runTerminalPlan(planned.plan, input);
69
- return terminalStudyRefusal(options, planned.refusal);
70
- }
71
57
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
72
58
  export function terminalStudyRefusal(options, refusal) {
73
59
  const { config, dryRun } = options;
@@ -82,60 +68,35 @@ export function terminalStudyRefusal(options, refusal) {
82
68
  warnings: [],
83
69
  error: { code: refusal.code, message: refusal.message },
84
70
  };
85
- // A refusal starts no run, so a declared or default analysis is recorded as skipped.
86
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
87
- return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, options, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
88
- }
89
- /**
90
- * Run a terminal plan. The run scope gives a direct library caller the same run lifetime the CLI
91
- * gets: whichever of its fail-closed exits the run takes, the run it started is closed, and only a
92
- * run that published its final bundle reaches automatic analysis.
93
- */
94
- export async function runTerminalPlan(plan, input) {
95
- const admission = await admitTerminalRun(plan, input);
96
- return admission.ok ? runAdmittedTerminalRun(admission.admitted, input) : admission.result;
71
+ return completeRefusalAnalysis(refused, config, options);
97
72
  }
98
73
  /**
99
74
  * runStudyWith's step for a terminal plan. It runs a live plan's local checks (checkLiveTerminalMachine)
100
75
  * before any run scope opens, so the CLI can present their refusal before it loads a declared
101
76
  * scorer, and returns the run that continues from them with that scorer.
102
77
  */
103
- export async function admitTerminalPlan(plan, input) {
104
- const admission = await admitTerminalRun(plan, input);
105
- if (!admission.ok)
106
- return { ok: false, outcome: terminalOutcome(admission.result) };
107
- return {
108
- ok: true,
109
- run: async (scorer) => terminalOutcome(await runAdmittedTerminalRun(admission.admitted, withLateScorer(input, scorer, terminalRouteScorer))),
110
- };
111
- }
112
- function terminalOutcome(result) {
113
- 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
+ });
114
87
  }
115
- /**
116
- * A live plan's local checks, made outside any run scope. A refusal is the result runTerminalPlan
117
- * returns for it, with the analysis record of a run that never started.
118
- */
119
- async function admitTerminalRun(plan, input) {
88
+ /** A live plan's local checks, made outside any run scope. */
89
+ function admitTerminalRun(plan, input) {
120
90
  const warnings = [];
121
91
  if (plan.dryRun)
122
92
  return { ok: true, admitted: { plan, warnings } };
123
93
  const checked = checkLiveTerminalMachine(plan, input, warnings);
124
94
  if (checked.ok)
125
95
  return { ok: true, admitted: { plan, warnings, runtimeEnv: checked.runtimeEnv } };
126
- const refused = terminalFailure(plan, input, warnings)(checked.code, checked.message);
127
- return { ok: false, result: await completeTerminalAnalysis(plan, input, refused, undefined) };
128
- }
129
- /** Runs an admitted plan in its own run scope, then its automatic analysis. */
130
- async function runAdmittedTerminalRun(admitted, input) {
131
- const { result, finished } = await runScope((scope) => runTerminalPlanInScope(admitted, input, scope));
132
- return completeTerminalAnalysis(admitted.plan, input, result, finished);
133
- }
134
- function completeTerminalAnalysis(plan, input, result, finished) {
135
- return completeAutomaticAnalysis(result, finished, plan.analysis?.config, input, {
136
- ...(plan.analysis === undefined ? {} : { trigger: plan.analysis.trigger }),
137
- preferLargerOutput: plan.analysis?.preferLargerOutput === true,
138
- });
96
+ return {
97
+ ok: false,
98
+ result: terminalFailure(plan, input, warnings)(checked.code, checked.message),
99
+ };
139
100
  }
140
101
  /** The route's envelope for a run that stops before its bundle. */
141
102
  function terminalFailure(plan, input, warnings) {
@@ -1,4 +1,10 @@
1
+ import type { StudyRuntimeAuth } from "../../study/types.js";
1
2
  import type { E2BNetworkOptions } from "../../substrates/e2b/sdk.js";
3
+ /**
4
+ * The runtime auth a study gets when it declares none. A shell agent can read its own command env,
5
+ * so the raw key stays in the host-side E2B request unless a study declares openai-env.
6
+ */
7
+ export declare const DEFAULT_RUNTIME_AUTH: StudyRuntimeAuth;
2
8
  /** E2B envd installs the sandbox-specific proxy CA into this system bundle before routing. */
3
9
  export declare const E2B_SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
4
10
  /** An inert nonsecret value satisfies Codex exec's auth prerequisite. E2B replaces the header. */
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The runtime auth a study gets when it declares none. A shell agent can read its own command env,
3
+ * so the raw key stays in the host-side E2B request unless a study declares openai-env.
4
+ */
5
+ export const DEFAULT_RUNTIME_AUTH = "openai-egress";
1
6
  const OPENAI_EGRESS_HOST = "api.openai.com";
2
7
  /** E2B envd installs the sandbox-specific proxy CA into this system bundle before routing. */
3
8
  export const E2B_SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
@@ -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;
@@ -11,6 +11,7 @@ import { implicitProjectDirectoryExists, readPackageName, validateCwd } from "./
11
11
  import { loadDryRunInputs } from "./dry-run-inputs.js";
12
12
  import { judgeExecution, judgePreview, OUTCOME_POLICIES, resultOk, verdictText, } from "./judge.js";
13
13
  import { reviewOutcome } from "./display.js";
14
+ import { cli } from "../cli/invocation.js";
14
15
  /**
15
16
  * The preview route's run. The run scope closes the run it started on every exit, including
16
17
  * the fail-closed ones. It renders an Observer only when `options.observer` asks for one.
@@ -49,7 +50,7 @@ async function runDryRunInScope(options, scope) {
49
50
  if (!options.dryRun) {
50
51
  return refused(requestedCwd, warnings, {
51
52
  code: "HUMANISH_LIVE_RUN_UNIMPLEMENTED",
52
- message: "humanish run needs a study. List studies with humanish study list, or run humanish run --dry-run for a sample bundle.",
53
+ message: `humanish run needs a study. List studies with ${cli("study list")}, or run ${cli("run --dry-run")} for a sample bundle.`,
53
54
  });
54
55
  }
55
56
  const createdAt = new Date().toISOString();
package/dist/run/judge.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // 4. foldScorerFailures, which every route with a scorer applies before the run finishes;
13
13
  // 5. the outcome: judgeExecution applies the route's policy to its execution failures, and
14
14
  // resultOk gives the result's ok.
15
+ import { cli } from "../cli/invocation.js";
15
16
  /**
16
17
  * The participant said it reached the goal having taken no action and said nothing, and no stop
17
18
  * condition ended the session. It most likely saw a blank or loading screen, so it is not a pass.
@@ -126,7 +127,7 @@ export const EXECUTION_FAILURE_KINDS = [
126
127
  export function sandboxCleanupFailure(owner, warning, runId, recovery) {
127
128
  return {
128
129
  kind: "sandbox-cleanup",
129
- message: `${owner}: ${warning ?? "Sandbox release is unconfirmed."} ${recovery ?? `Reclaim it by recorded id with \`humanish reclaim --run ${runId}\`.`}`,
130
+ message: `${owner}: ${warning ?? "Sandbox release is unconfirmed."} ${recovery ?? `Reclaim it by recorded id with \`${cli(`reclaim --run ${runId}`)}\`.`}`,
130
131
  };
131
132
  }
132
133
  /** Each route's outcome policy, in one place. */
@@ -0,0 +1,13 @@
1
+ /** Each local-only file, relative to the run directory, with why a shared copy omits it. */
2
+ export declare const LOCAL_ONLY_RUN_FILES: ReadonlyMap<string, string>;
3
+ /**
4
+ * Whether a run-relative posix path names a local-only file. Case is ignored, so a request that
5
+ * spells the name differently cannot reach the file on a case-insensitive filesystem.
6
+ */
7
+ export declare function isLocalOnlyRunFile(relativePath: string): boolean;
8
+ /**
9
+ * Whether `filePath` is one of the local-only files of the run at `runRoot` under another
10
+ * spelling: the same device and inode. A case-insensitive filesystem also folds Unicode, so a name
11
+ * check alone cannot rule every spelling out.
12
+ */
13
+ export declare function isLocalOnlyRunFileIdentity(runRoot: string, filePath: string): Promise<boolean>;
@@ -0,0 +1,35 @@
1
+ // Run files that mean something only on the machine that recorded the run. sandbox-receipts.ndjson
2
+ // holds the raw sandbox ids reclaim kills by, and status.json holds the recording process's pid.
3
+ // Bundle export leaves both out of a shared copy, and the Observer servers never hand them out.
4
+ import { lstat } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { SANDBOX_RECEIPTS_ARTIFACT } from "./sandbox-receipts.js";
7
+ import { RUN_STATUS_FILE } from "./status.js";
8
+ /** Each local-only file, relative to the run directory, with why a shared copy omits it. */
9
+ export const LOCAL_ONLY_RUN_FILES = new Map([
10
+ [RUN_STATUS_FILE, "local process status is not a new attempt"],
11
+ [SANDBOX_RECEIPTS_ARTIFACT, "operational journal does not confer a derivative resource lease"],
12
+ ]);
13
+ /**
14
+ * Whether a run-relative posix path names a local-only file. Case is ignored, so a request that
15
+ * spells the name differently cannot reach the file on a case-insensitive filesystem.
16
+ */
17
+ export function isLocalOnlyRunFile(relativePath) {
18
+ return LOCAL_ONLY_RUN_FILES.has(relativePath.toLowerCase());
19
+ }
20
+ /**
21
+ * Whether `filePath` is one of the local-only files of the run at `runRoot` under another
22
+ * spelling: the same device and inode. A case-insensitive filesystem also folds Unicode, so a name
23
+ * check alone cannot rule every spelling out.
24
+ */
25
+ export async function isLocalOnlyRunFileIdentity(runRoot, filePath) {
26
+ const requested = await lstat(filePath, { bigint: true }).catch(() => null);
27
+ if (requested === null)
28
+ return false;
29
+ for (const name of LOCAL_ONLY_RUN_FILES.keys()) {
30
+ const local = await lstat(path.join(runRoot, name), { bigint: true }).catch(() => null);
31
+ if (local !== null && local.dev === requested.dev && local.ino === requested.ino)
32
+ return true;
33
+ }
34
+ return false;
35
+ }
@@ -18,6 +18,11 @@ export interface ReclaimResult {
18
18
  tagSearch: ReclaimTagSearch;
19
19
  /** Creates that had not returned when reclaim finished; only the signal handler sees any. */
20
20
  createsInFlight: number;
21
+ /**
22
+ * Why there was nothing to look for: `dry-run` when run.json or status.json records the run as
23
+ * a dry run, which creates no sandboxes. Reclaim then contacts E2B for nothing.
24
+ */
25
+ reason?: "dry-run";
21
26
  warnings: string[];
22
27
  error?: {
23
28
  code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
@@ -11,12 +11,16 @@
11
11
  import { E2B_DEBUG_ENV, e2bDebugMode, loadE2BDesktopModule, } from "../substrates/e2b/sdk.js";
12
12
  import { containedPathAbsent, readContainedRegularFile, } from "./contained-output.js";
13
13
  import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecision, } from "./preflight-receipts.js";
14
- import { resolveRunPath } from "./locate.js";
14
+ import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
15
+ import { RUN_BUNDLE_FILE } from "./bundle.js";
16
+ import { RUN_STATUS_FILE } from "./status.js";
17
+ import { isRecord } from "./type-guards.js";
15
18
  import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
16
19
  import { sandboxOwnerTags, } from "./sandbox-creates.js";
17
20
  import { earlierOutcomes, GONE, RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, receiptWriter, rootState, searchTagSets, settledWithin, targetSet, } from "./reclaim-outcomes.js";
18
21
  import { redactText, toErrorMessage } from "../evidence/redaction.js";
19
22
  import { runIdOf } from "./paths.js";
23
+ import { cli } from "../cli/invocation.js";
20
24
  export { RECLAIM_RECEIPT_ARTIFACT };
21
25
  export function reclaimRunSandboxes(cwd, runInput, hooks = {}) {
22
26
  return reclaimRun(cwd, runInput, () => resolveRunPath(cwd, runInput), hooks);
@@ -54,11 +58,14 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
54
58
  ...base,
55
59
  error: {
56
60
  code: "HUMANISH_RECLAIM_RUN_NOT_FOUND",
57
- message: `No run found for "${runInput}" (use \`humanish runs\` to list runs).`,
61
+ message: `No run found for "${runInput}" (use \`${cli("runs")}\` to list runs).`,
58
62
  },
59
63
  };
60
64
  }
61
65
  const runId = runIdOf(runPaths);
66
+ // The signal handler passes the creates it stopped and decides from what they reported.
67
+ if (hooks.creates === undefined && (await recordedDryRun(runPaths)))
68
+ return { ...base, ok: true, state: "clean", runId, reason: "dry-run" };
62
69
  const reclaimed = await reclaimRoot(runPaths, runId, hooks, warnings);
63
70
  if (reclaimed.kind === "module-unavailable")
64
71
  return {
@@ -85,6 +92,31 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
85
92
  createsInFlight,
86
93
  };
87
94
  }
95
+ /**
96
+ * Whether the run records that it was a dry run: run.json and status.json, each one present, say
97
+ * `dry-run`. A run with neither record is not taken for one. A journal with any line in it, or an
98
+ * earlier reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a
99
+ * live one whatever its records say.
100
+ */
101
+ async function recordedDryRun(runPaths) {
102
+ const modes = [];
103
+ for (const file of [RUN_BUNDLE_FILE, RUN_STATUS_FILE]) {
104
+ const record = await readRunJsonIfExists(runPaths, file);
105
+ if (isRecord(record))
106
+ modes.push(record.mode);
107
+ }
108
+ if (modes.length === 0 || modes.some((mode) => mode !== "dry-run"))
109
+ return false;
110
+ if (!(await containedPathAbsent(runPaths, RECLAIM_RECEIPT_ARTIFACT))) {
111
+ const earlier = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
112
+ if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
113
+ return false;
114
+ }
115
+ if (await containedPathAbsent(runPaths, SANDBOX_RECEIPTS_ARTIFACT))
116
+ return true;
117
+ const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT);
118
+ return journal !== null && journal.toString("utf8").trim() === "";
119
+ }
88
120
  /**
89
121
  * Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
90
122
  * .humanish/preflight/<probe-id>/ and removes the journal after a confirmed kill, so what is left
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveRunsRoot } from "./paths.js";
4
+ import { cli } from "../cli/invocation.js";
4
5
  /**
5
6
  * The message for a run that is not there: how to start one when the project has no runs, or how
6
7
  * to list them when it has. Only the runs directory's entry names are read.
@@ -10,6 +11,6 @@ export async function runNotFoundMessage(cwdInput, runInput) {
10
11
  const entries = await readdir(resolveRunsRoot(cwd)).catch(() => []);
11
12
  const hasRun = entries.some((name) => !name.startsWith(".") && !name.endsWith(".json"));
12
13
  return hasRun
13
- ? `No run ${runInput}; humanish runs lists them.`
14
- : `No runs in ${cwd} yet; start one with humanish run first-run.`;
14
+ ? `No run ${runInput}; ${cli("runs")} lists them.`
15
+ : `No runs in ${cwd} yet; start one with ${cli("run first-run")}.`;
15
16
  }
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() };
@@ -1,7 +1,9 @@
1
1
  // The sandboxes this process is creating for each run directory, and the E2B tags that name the
2
- // directory. A receipt reaches sandbox-receipts.ndjson only after `Sandbox.create` resolves, and
3
- // on @e2b/desktop that is after desktop startup, seconds after E2B has created the sandbox. The
4
- // CLI's signal handler (src/cli/commands/run-signals.ts) uses this registry to cover that window:
2
+ // directory. E2B creates a sandbox before its id reaches this process: on @e2b/desktop the startup
3
+ // guard reports the id, and its receipt is written, only once the SDK has the handle, and a
4
+ // create from an SDK without the guard reports it when `Sandbox.create` resolves, after desktop
5
+ // startup. The CLI's signal handler (src/cli/commands/run-signals.ts) uses this registry to cover
6
+ // the window before a receipt lands:
5
7
  // it stops new creates, kills every id a create has reported, and kills each in-flight create's
6
8
  // sandbox as soon as its id arrives. The tags let `humanish reclaim` find a sandbox whose id
7
9
  // never reached this process at all.
@@ -23,12 +23,13 @@ export interface ParsedSandboxReceipt extends Omit<SandboxReceipt, "provider"> {
23
23
  /** The ids this process has receipted for `root`, whether or not the journal write landed. */
24
24
  export declare function appendedSandboxIds(root: PreparedOutputRoot): string[];
25
25
  /**
26
- * Append one receipt. Best-effort by design: the receipt exists to protect the run, so a failed
27
- * receipt write must never fail the participant. The only cost of a miss is that `reclaim` cannot see
28
- * this id and the TTL backstop covers it instead. Containment is the same prepare step every
29
- * artifact write uses; append (not atomic-replace) keeps racing participants' receipts intact.
26
+ * Append one receipt; false when the write failed. Best-effort by design: the receipt exists to
27
+ * protect the run, so a failed receipt write must never fail the participant. The only cost of a
28
+ * miss is that `reclaim` cannot see this id and the TTL backstop covers it instead. Containment is
29
+ * the same prepare step every artifact write uses; append (not atomic-replace) keeps racing
30
+ * participants' receipts intact.
30
31
  */
31
- export declare function appendSandboxReceipt(root: PreparedOutputRoot, receipt: SandboxReceipt): Promise<void>;
32
+ export declare function appendSandboxReceipt(root: PreparedOutputRoot, receipt: SandboxReceipt): Promise<boolean>;
32
33
  /** Append one owner line; false when the write failed, which never fails the create. */
33
34
  export declare function appendSandboxOwner(root: PreparedOutputRoot, owner: SandboxOwnerTags): Promise<boolean>;
34
35
  /** The owner tags every sandbox carries; sandboxOwnerTags in src/run/sandbox-creates.ts. */