humanish 0.110.0 → 0.110.1

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 (132) hide show
  1. package/ARCHITECTURE.md +14 -11
  2. package/CHANGELOG.md +13 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +96 -47
  5. package/dist/actors/computer-use/loop/ending.js +4 -4
  6. package/dist/actors/local-agent/claude-participant.d.ts +74 -0
  7. package/dist/actors/local-agent/claude-participant.js +243 -0
  8. package/dist/actors/local-agent/claude-session.d.ts +16 -3
  9. package/dist/actors/local-agent/claude-session.js +64 -23
  10. package/dist/actors/local-agent/cli.d.ts +11 -0
  11. package/dist/actors/local-agent/cli.js +103 -24
  12. package/dist/actors/local-agent/readiness.d.ts +2 -2
  13. package/dist/actors/local-agent/readiness.js +18 -11
  14. package/dist/cli/commands/analyze.js +21 -5
  15. package/dist/cli/commands/run-signals.d.ts +2 -0
  16. package/dist/cli/commands/run-signals.js +96 -44
  17. package/dist/cli/commands/runs.d.ts +5 -0
  18. package/dist/cli/commands/runs.js +86 -70
  19. package/dist/cli/commands/study-route-computer-use.js +2 -0
  20. package/dist/cli/commands/study-route-scripted.js +2 -0
  21. package/dist/cli/commands/study-route-shared-world.js +2 -0
  22. package/dist/cli/commands/study-route-terminal.js +2 -0
  23. package/dist/cli/doctor.d.ts +4 -1
  24. package/dist/cli/doctor.js +42 -4
  25. package/dist/cli/findings.d.ts +103 -0
  26. package/dist/cli/findings.js +316 -0
  27. package/dist/cli/first-run-path.d.ts +5 -4
  28. package/dist/cli/first-run-path.js +14 -7
  29. package/dist/cli/io.js +2 -1
  30. package/dist/cli/product-sentence.d.ts +3 -3
  31. package/dist/cli/product-sentence.js +3 -3
  32. package/dist/keys/env-file.d.ts +4 -0
  33. package/dist/keys/env-file.js +17 -0
  34. package/dist/observer/data.d.ts +14 -1
  35. package/dist/observer/data.js +19 -9
  36. package/dist/observer/library.d.ts +4 -0
  37. package/dist/observer/library.js +18 -15
  38. package/dist/observer/render.js +8 -2
  39. package/dist/observer/run-routes.d.ts +2 -0
  40. package/dist/observer/run-routes.js +43 -34
  41. package/dist/observer-app.html +3 -3
  42. package/dist/routes/computer-use/bundle.js +3 -1
  43. package/dist/routes/computer-use/fanout-records.js +1 -1
  44. package/dist/routes/computer-use/participant-execution.js +3 -1
  45. package/dist/routes/computer-use/participant-model.js +5 -2
  46. package/dist/routes/computer-use/plan.js +4 -4
  47. package/dist/routes/computer-use/preflight.js +1 -1
  48. package/dist/routes/computer-use/result.js +17 -28
  49. package/dist/routes/computer-use/single-bundle.js +7 -1
  50. package/dist/routes/scripted/bundle.js +4 -2
  51. package/dist/routes/scripted/plan.js +2 -2
  52. package/dist/routes/scripted/result.d.ts +3 -5
  53. package/dist/routes/scripted/result.js +16 -22
  54. package/dist/routes/scripted/scenario.js +4 -4
  55. package/dist/routes/shared-world/bundle-records.js +6 -1
  56. package/dist/routes/shared-world/bundle.js +2 -0
  57. package/dist/routes/shared-world/plan.js +1 -1
  58. package/dist/routes/shared-world/result.js +22 -22
  59. package/dist/routes/terminal/bundle.js +4 -2
  60. package/dist/routes/terminal/dry-run.js +8 -12
  61. package/dist/routes/terminal/ledger.d.ts +3 -3
  62. package/dist/routes/terminal/ledger.js +6 -6
  63. package/dist/routes/terminal/live-finish.js +15 -13
  64. package/dist/routes/terminal/plan.js +4 -4
  65. package/dist/routes/terminal/result.d.ts +6 -7
  66. package/dist/routes/terminal/result.js +6 -22
  67. package/dist/routes/terminal/session.js +1 -1
  68. package/dist/run/bundle-shape.d.ts +3 -1
  69. package/dist/run/bundle-shape.js +25 -1
  70. package/dist/run/bundle.d.ts +24 -0
  71. package/dist/run/display.d.ts +59 -0
  72. package/dist/run/display.js +106 -0
  73. package/dist/run/dry-run.js +11 -7
  74. package/dist/run/judge.d.ts +3 -2
  75. package/dist/run/judge.js +10 -0
  76. package/dist/run/pricing.js +1 -1
  77. package/dist/run/projection.d.ts +0 -2
  78. package/dist/run/projection.js +0 -11
  79. package/dist/run/reclaim-outcomes.d.ts +89 -0
  80. package/dist/run/reclaim-outcomes.js +213 -0
  81. package/dist/run/reclaim.d.ts +18 -17
  82. package/dist/run/reclaim.js +180 -113
  83. package/dist/run/results.d.ts +10 -20
  84. package/dist/run/results.js +2 -2
  85. package/dist/run/run-index.d.ts +11 -1
  86. package/dist/run/run-index.js +11 -0
  87. package/dist/run/run.d.ts +34 -18
  88. package/dist/run/run.js +166 -67
  89. package/dist/run/sandbox-creates.d.ts +59 -0
  90. package/dist/run/sandbox-creates.js +138 -0
  91. package/dist/run/sandbox-receipts.d.ts +14 -0
  92. package/dist/run/sandbox-receipts.js +44 -0
  93. package/dist/run/stats.d.ts +5 -0
  94. package/dist/run/stats.js +8 -4
  95. package/dist/run/status.d.ts +6 -9
  96. package/dist/run/status.js +7 -23
  97. package/dist/run/stored-runs.d.ts +5 -14
  98. package/dist/run/stored-runs.js +31 -146
  99. package/dist/run/streams.d.ts +3 -2
  100. package/dist/study/doctor.d.ts +1 -1
  101. package/dist/study/doctor.js +5 -2
  102. package/dist/study/init.js +14 -3
  103. package/dist/study/keys.d.ts +12 -0
  104. package/dist/study/keys.js +32 -0
  105. package/dist/study/persona.d.ts +4 -0
  106. package/dist/study/persona.js +4 -2
  107. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  108. package/dist/substrates/e2b/peer-install.js +161 -0
  109. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  110. package/dist/substrates/e2b/sandbox.js +128 -35
  111. package/dist/substrates/e2b/sdk.d.ts +33 -0
  112. package/dist/substrates/e2b/sdk.js +21 -21
  113. package/dist/substrates/shell.d.ts +2 -0
  114. package/dist/substrates/shell.js +4 -0
  115. package/dist/tui-app.js +133 -133
  116. package/dist/verify/liveness.d.ts +19 -3
  117. package/dist/verify/liveness.js +46 -25
  118. package/dist/verify/verify.d.ts +3 -0
  119. package/dist/verify/verify.js +6 -2
  120. package/docs/architecture/actor-contract.md +2 -1
  121. package/docs/architecture/observer.md +22 -7
  122. package/docs/architecture/task-protocol-support.md +13 -13
  123. package/docs/architecture/terminal-product-route.md +1 -1
  124. package/docs/contracts/run-bundle.md +75 -6
  125. package/docs/contracts/schemas.md +39 -22
  126. package/docs/contracts/study-analysis.md +45 -0
  127. package/docs/contracts/study-costs.md +8 -0
  128. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  129. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  130. package/docs/principles/invariants-and-defaults.md +11 -8
  131. package/package.json +4 -3
  132. package/skills/humanish/SKILL.md +16 -2
@@ -20,9 +20,9 @@ export type RunInterruptSignal = "SIGINT" | "SIGTERM" | "SIGHUP";
20
20
  interface RunStatusOutcome {
21
21
  /** `review.verdict` verbatim. */
22
22
  verdict?: string;
23
- /** True when the run's own envelope reported success: the result's ok, recorded after finish. */
23
+ /** run.json's `outcome.ok`: the route result's ok. */
24
24
  ok?: boolean;
25
- /** Whether the run worked as an execution, apart from its verdict; recorded after finish. */
25
+ /** run.json's `outcome.execution`: whether the run worked as an execution, apart from its verdict. */
26
26
  execution?: ExecutionOutcome;
27
27
  /** `review.participants` counts, when the run recorded any. */
28
28
  participants?: {
@@ -72,14 +72,11 @@ export interface RunStatusHandle {
72
72
  /** Finalize: state `finished`, `completedAt`, and the derived outcome. Stops the cadence.
73
73
  * Idempotent: a second call is a no-op, so a route with several exit paths is safe. */
74
74
  finish(outcome?: RunStatusOutcome): Promise<void>;
75
- /** After finish, add the result's ok and execution outcome to the finished record, so status.json
76
- * and the result agree. Before finish it does nothing. */
77
- settle(result: {
78
- ok: boolean;
79
- execution: ExecutionOutcome;
80
- }): Promise<void>;
75
+ /** After finish, replace the finished record's outcome with one read from a rewritten run.json,
76
+ * so the two agree. Before finish, and after an interrupt, it does nothing. */
77
+ restate(outcome: RunStatusOutcome): Promise<void>;
81
78
  /** The process is being stopped by `signal`: state `interrupted`, `completedAt` and the signal.
82
- * It stops the cadence and makes a later finish or settle a no-op, so the route cannot write
79
+ * It stops the cadence and makes a later finish or restate a no-op, so the route cannot write
83
80
  * `running` or `finished` over it. When the run had finished it writes nothing, waits for the
84
81
  * writes already queued and resolves false. */
85
82
  interrupt(signal: RunInterruptSignal): Promise<boolean>;
@@ -4,15 +4,14 @@
4
4
  //
5
5
  // run.json remains the evidence of record. This file is a derived index and liveness record:
6
6
  // `verify` never gates on it, nothing here is a claim about what a participant did, and when the
7
- // two disagree run.json wins and this file can be rebuilt from it.
7
+ // two disagree run.json wins. A finished record copies run.json's outcome, so it can be rebuilt
8
+ // from it; a record written before run.json carried an outcome holds the only copy of the run's ok.
8
9
  //
9
10
  // Public safety: it holds only the run id, the study id/path/origin (the strings `humanish study list`
10
11
  // prints), the mode, a local pid and timestamps. It holds no hostname or user/path identity: an
11
12
  // operator may share the run directory, so a share-safety gate must have nothing to strip here.
12
13
  import { isProvenanceField, studyFields } from "./study-provenance.js";
13
14
  import { writeContainedOutputFile } from "./contained-output.js";
14
- import { redactText } from "../evidence/redaction.js";
15
- import { readRunSandboxIds, scrubSandboxIds } from "./sandbox-ids.js";
16
15
  export const RUN_STATUS_SCHEMA = "humanish.run-status.v1";
17
16
  /** The file, relative to the run directory. */
18
17
  export const RUN_STATUS_FILE = "status.json";
@@ -46,6 +45,9 @@ export function runStatusOutcome(bundle) {
46
45
  ...(typeof bundle.cost?.fullyEstimated === "boolean"
47
46
  ? { estimatedCostComplete: bundle.cost.fullyEstimated }
48
47
  : {}),
48
+ ...(bundle.outcome?.state === "finished"
49
+ ? { ok: bundle.outcome.ok, execution: bundle.outcome.execution }
50
+ : {}),
49
51
  };
50
52
  }
51
53
  /**
@@ -103,28 +105,10 @@ export function beginRunStatus(runPaths, options) {
103
105
  };
104
106
  await write(finishedRecord);
105
107
  },
106
- async settle(result) {
108
+ async restate(outcome) {
107
109
  if (finishedRecord === undefined)
108
110
  return;
109
- // The record is public-safe by construction, so each message passes the shape redaction
110
- // again even though the routes scrubbed it. A route settles with its own result, so an SDK
111
- // error here can still name a sandbox; the receipts give the ids to replace.
112
- const ids = await readRunSandboxIds(runPaths);
113
- const redacted = (failure) => ({
114
- kind: failure.kind,
115
- message: scrubSandboxIds(redactText(failure.message), ids),
116
- });
117
- const execution = {
118
- succeeded: result.execution.succeeded,
119
- failures: result.execution.failures.map(redacted),
120
- ...(result.execution.warnings === undefined
121
- ? {}
122
- : { warnings: result.execution.warnings.map(redacted) }),
123
- };
124
- finishedRecord = {
125
- ...finishedRecord,
126
- outcome: { ...finishedRecord.outcome, ok: result.ok, execution },
127
- };
111
+ finishedRecord = { ...finishedRecord, outcome };
128
112
  await write(finishedRecord);
129
113
  },
130
114
  async interrupt(signal) {
@@ -1,18 +1,7 @@
1
- import type { E2BDesktopModule } from "../substrates/e2b/sdk.js";
2
- import { type ReviewSummary, type RunBundle } from "./bundle.js";
3
- import { type CleanupAdapterResult, type CleanupResult } from "./results.js";
1
+ import { type ReviewSummary } from "./bundle.js";
2
+ import { type RunDisplay } from "./display.js";
4
3
  import { type VerifyResult } from "../verify/verify.js";
5
4
  declare const RUNS_SCHEMA = "humanish.runs-result.v1";
6
- export interface RunCleanupHooks {
7
- /** @deprecated Ignored. Stored provider ids are not authority to load or mutate a provider. */
8
- loadDesktopModule?: () => Promise<E2BDesktopModule>;
9
- cleanupAdapterResources?: (ctx: {
10
- cwd: string;
11
- runDir: string;
12
- bundle: RunBundle;
13
- }) => Promise<CleanupAdapterResult[]>;
14
- now?: () => Date;
15
- }
16
5
  export interface RunsResult {
17
6
  schema: typeof RUNS_SCHEMA;
18
7
  ok: boolean;
@@ -22,6 +11,8 @@ export interface RunsResult {
22
11
  createdAt: string | null;
23
12
  mode: string | null;
24
13
  path: string;
14
+ /** How the run reads (runDisplay in src/run/display.ts); absent when it has no readable run.json. */
15
+ display?: RunDisplay;
25
16
  }>;
26
17
  latest: string | null;
27
18
  error?: {
@@ -29,10 +20,10 @@ export interface RunsResult {
29
20
  message: string;
30
21
  };
31
22
  }
32
- export declare function cleanupRun(cwdInput: string, runInput: string, hooks?: RunCleanupHooks): Promise<CleanupResult>;
33
23
  export declare function listRuns(cwdInput: string): Promise<RunsResult>;
34
24
  export declare function readReview(cwdInput: string, runInput: string): Promise<VerifyResult | (ReviewSummary & {
35
25
  path: string;
36
26
  runId: string;
27
+ display?: RunDisplay;
37
28
  })>;
38
29
  export {};
@@ -1,157 +1,17 @@
1
- import { REDACTED_SANDBOX_ID, sandboxIdDigest } from "../evidence/redaction.js";
2
1
  import { lstat, readdir } from "node:fs/promises";
3
- import { runNotFoundMessage } from "./run-not-found.js";
4
2
  import path from "node:path";
5
3
  import { redactText } from "../evidence/redaction.js";
6
- import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, validatePreparedRunArtifactPaths, } from "./paths.js";
7
- import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, prepareContainedOutputFile, readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
8
- import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD, } from "./bundle.js";
9
- import { CLEANUP_SCHEMA, } from "./results.js";
10
- import { isReviewSummary, isRunBundle } from "./bundle-shape.js";
4
+ import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, } from "./paths.js";
5
+ import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, } from "./contained-output.js";
6
+ import { RUN_BUNDLE_FILE } from "./bundle.js";
7
+ import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
8
+ import { bundleDisplayFacts, runDisplay } from "./display.js";
9
+ import { RUN_STATUS_FILE } from "./status.js";
11
10
  import { readLatest, readRunJsonIfExists, resolveRunPath } from "./locate.js";
12
11
  import { withCuaReviewProvenance } from "./outcomes.js";
13
12
  import { isNodeError, isRecord } from "./type-guards.js";
14
13
  import { invalidRunStorageVerifyResult, verifyResolvedRun, } from "../verify/verify.js";
15
14
  const RUNS_SCHEMA = "humanish.runs-result.v1";
16
- /** A cleanup result that refused before inspecting any resource. */
17
- function cleanupRefusal(cwd, runInput, checkedAt, error, bundlePath) {
18
- return {
19
- schema: CLEANUP_SCHEMA,
20
- ok: false,
21
- cwd,
22
- run: runInput,
23
- ...(bundlePath === undefined ? {} : { bundlePath }),
24
- checkedAt,
25
- summary: { resources: 0, killed: 0, alreadyClean: 0, failed: 0, skipped: 0 },
26
- resources: [],
27
- adapterResults: [],
28
- warnings: [],
29
- error,
30
- };
31
- }
32
- /** What the recorded evidence says about one provider resource; nothing is killed from here. */
33
- function recordedResourceResult(resource) {
34
- // Named by digest: a run recorded before 0.110 holds the raw id here, which is digested.
35
- const raw = resource.id === REDACTED_SANDBOX_ID ? undefined : resource.id;
36
- const idDigest = resource.idDigest ?? (raw === undefined ? undefined : sandboxIdDigest(raw));
37
- const base = {
38
- provider: resource.provider,
39
- kind: resource.kind,
40
- id: REDACTED_SANDBOX_ID,
41
- ...(idDigest === undefined ? {} : { idDigest }),
42
- };
43
- if (resource.provider !== "e2b-desktop" || resource.kind !== "sandbox") {
44
- return {
45
- ...base,
46
- status: "skipped",
47
- message: "cleanup only supports e2b-desktop sandbox resources",
48
- };
49
- }
50
- if (resource.status === "killed" || resource.cleanup?.killed === true) {
51
- return {
52
- ...base,
53
- status: "already_clean",
54
- message: "resource was already recorded as killed",
55
- };
56
- }
57
- return {
58
- ...base,
59
- status: "failed",
60
- message: "automatic provider cleanup requires a verified resource lease",
61
- };
62
- }
63
- export async function cleanupRun(cwdInput, runInput, hooks = {}) {
64
- const cwd = path.resolve(cwdInput);
65
- const checkedAt = (hooks.now ?? (() => new Date()))().toISOString();
66
- let resolved;
67
- try {
68
- resolved = await resolveRunPath(cwd, runInput);
69
- }
70
- catch {
71
- return cleanupRefusal(cwd, runInput, checkedAt, {
72
- code: "HUMANISH_INVALID_RUN_BUNDLE",
73
- message: "Run storage failed containment validation.",
74
- });
75
- }
76
- if (!resolved) {
77
- return cleanupRefusal(cwd, runInput, checkedAt, {
78
- code: "HUMANISH_RUN_NOT_FOUND",
79
- message: await runNotFoundMessage(cwd, runInput),
80
- });
81
- }
82
- const runPaths = resolved;
83
- const bundlePath = path.join(runPaths.absoluteRunRoot, RUN_BUNDLE_FILE);
84
- const cleanupPath = path.join(runPaths.absoluteRunRoot, "cleanup.json");
85
- await prepareContainedOutputFile(runPaths, "cleanup.json");
86
- const bundleBytes = await readContainedRegularFile(runPaths, RUN_BUNDLE_FILE);
87
- let bundle = null;
88
- if (bundleBytes) {
89
- try {
90
- bundle = JSON.parse(bundleBytes.toString("utf8"));
91
- }
92
- catch {
93
- bundle = null;
94
- }
95
- }
96
- if (!isRunBundle(bundle)) {
97
- return cleanupRefusal(cwd, runInput, checkedAt, {
98
- code: "HUMANISH_INVALID_RUN_BUNDLE",
99
- message: "Run bundle failed cleanup shape validation.",
100
- }, path.relative(cwd, bundlePath));
101
- }
102
- const warnings = [];
103
- const providerResources = bundle.providerResources ?? [];
104
- const resources = providerResources.map(recordedResourceResult);
105
- let adapterResults = [];
106
- if (hooks.cleanupAdapterResources) {
107
- try {
108
- adapterResults = await hooks.cleanupAdapterResources({
109
- cwd,
110
- runDir: runPaths.physicalRunRoot,
111
- bundle,
112
- });
113
- }
114
- catch (error) {
115
- adapterResults = [
116
- {
117
- id: "adapter-cleanup",
118
- ok: false,
119
- message: error instanceof Error ? error.message : String(error),
120
- },
121
- ];
122
- }
123
- await validatePreparedRunArtifactPaths(runPaths);
124
- }
125
- if (providerResources.length === 0 && adapterResults.length === 0) {
126
- warnings.push("Run bundle recorded no provider resource evidence; nothing to inspect.");
127
- }
128
- const summary = {
129
- resources: resources.length,
130
- killed: 0,
131
- alreadyClean: resources.filter((resource) => resource.status === "already_clean").length,
132
- failed: resources.filter((resource) => resource.status === "failed").length +
133
- adapterResults.filter((result) => !result.ok).length,
134
- skipped: resources.filter((resource) => resource.status === "skipped").length,
135
- };
136
- const ok = summary.failed === 0;
137
- const result = {
138
- schema: CLEANUP_SCHEMA,
139
- ok,
140
- cwd: PUBLIC_TARGET_CWD,
141
- run: runInput,
142
- runId: bundle.runId,
143
- bundlePath: path.relative(cwd, bundlePath),
144
- cleanupPath: path.relative(cwd, cleanupPath),
145
- checkedAt,
146
- summary,
147
- resources,
148
- adapterResults,
149
- warnings,
150
- };
151
- await validatePreparedRunArtifactPaths(runPaths);
152
- await writeContainedOutputFile(runPaths, "cleanup.json", `${JSON.stringify(result, null, 2)}\n`, "utf8");
153
- return result;
154
- }
155
15
  export async function listRuns(cwdInput) {
156
16
  const cwd = path.resolve(cwdInput);
157
17
  const runsRootPath = resolveRunsRoot(cwd);
@@ -216,6 +76,7 @@ export async function listRuns(cwdInput) {
216
76
  createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
217
77
  mode: isRecord(bundle) && typeof bundle.mode === "string" ? bundle.mode : null,
218
78
  path: path.join(RUNS_RELATIVE_ROOT, entryName),
79
+ ...(await listedRunDisplay(entryRunPaths, bundle)),
219
80
  });
220
81
  }
221
82
  if (runsRoot) {
@@ -234,6 +95,29 @@ export async function listRuns(cwdInput) {
234
95
  latest: latest?.runId ?? null,
235
96
  };
236
97
  }
98
+ /**
99
+ * A listed run's display, from its run.json and, for a bundle without an outcome, its status
100
+ * record. Like the listing's other fields it reads the file without verifying it; a run.json
101
+ * that is not an object naming a run gets none.
102
+ */
103
+ async function listedRunDisplay(runPaths, bundle) {
104
+ if (!isRecord(bundle) || typeof bundle.runId !== "string")
105
+ return {};
106
+ const outcome = isRunOutcome(bundle.outcome) ? bundle.outcome : undefined;
107
+ const status = outcome === undefined ? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE) : undefined;
108
+ const facts = bundleDisplayFacts({
109
+ runId: bundle.runId,
110
+ ...(typeof bundle.mode === "string" ? { mode: bundle.mode } : {}),
111
+ ...(isRecord(bundle.review) && typeof bundle.review.verdict === "string"
112
+ ? { review: { verdict: bundle.review.verdict } }
113
+ : {}),
114
+ ...(Array.isArray(bundle.simulations)
115
+ ? { simulations: bundle.simulations.filter(isRecord) }
116
+ : {}),
117
+ ...(outcome === undefined ? {} : { outcome }),
118
+ }, status);
119
+ return { display: runDisplay(facts) };
120
+ }
237
121
  function runsUnavailableResult(cwd, error) {
238
122
  return {
239
123
  schema: RUNS_SCHEMA,
@@ -279,5 +163,6 @@ export async function readReview(cwdInput, runInput) {
279
163
  ...projected,
280
164
  path: path.relative(cwd, path.join(runPaths.absoluteRunRoot, "review.json")),
281
165
  runId: path.basename(runPaths.absoluteRunRoot),
166
+ ...(runPaths ? await listedRunDisplay(runPaths, bundle) : {}),
282
167
  };
283
168
  }
@@ -171,8 +171,9 @@ export interface RunStream {
171
171
  /**
172
172
  * The judge's status for this participant (`judgedStatus` in src/run/judge.ts). It differs from
173
173
  * the trace status when a goal_satisfied session reported a blocker or never engaged. Computer-use
174
- * fan-out writes it for a finished live participant; rerun selection reads it. Absent elsewhere,
175
- * and in bundles written before it existed.
174
+ * and shared-world runs write it for a participant whose session finished; rerun selection and
175
+ * the Observer read it. Absent on the terminal and scripted routes, whose judge has no blocker
176
+ * rule, and in bundles written before it existed.
176
177
  */
177
178
  judgedStatus?: ActorStatus;
178
179
  transport: "snapshot" | "polling" | "sse" | "pty" | "app-server";
@@ -1,7 +1,7 @@
1
1
  import { type LocalRuntimeStatus } from "../substrates/local/runtime.js";
2
2
  import type { StudyConfig } from "./types.js";
3
3
  import type { PlanResult } from "./plan-types.js";
4
- import type { DetectedLocalAgent } from "../actors/local-agent/cli.js";
4
+ import { type DetectedLocalAgent } from "../actors/local-agent/cli.js";
5
5
  import type { ReasoningEffort } from "../actors/reasoning-effort.js";
6
6
  import type { DoctorCheckDraft } from "../cli/doctor.js";
7
7
  import { type CodexInstallation } from "../actors/codex/codex-admission.js";
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
3
3
  import { localRuntimeStatus } from "../substrates/local/runtime.js";
4
4
  import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
5
+ import { localAgentVersionRefusal } from "../actors/local-agent/cli.js";
5
6
  import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
6
7
  import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
7
8
  import { externalCatchHealthy } from "../comms/sandbox-catch.js";
@@ -254,12 +255,14 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
254
255
  // that the operator's config, release and model admit a launch.
255
256
  if (choice === "codex" && agent?.authStatus === "authenticated")
256
257
  return [await hostedCodexParticipantCheck(args, config.actors[0])];
258
+ const outdated = agent === undefined ? undefined : localAgentVersionRefusal(agent);
257
259
  return [
258
260
  {
259
261
  name: "local participant authentication",
260
- ok: agent?.authStatus === "authenticated",
262
+ ok: agent?.authStatus === "authenticated" && outdated === undefined,
261
263
  message: agent?.authStatus === "authenticated"
262
- ? `${agent.label} reports authenticated on the host. E2B supplies the desktop; no OpenAI API key is required for this participant.`
264
+ ? (outdated ??
265
+ `${agent.label}${agent.version === undefined ? "" : ` ${agent.version}`} reports authenticated on the host. E2B supplies the desktop; no OpenAI API key is required for this participant.`)
263
266
  : agent
264
267
  ? `${agent.label} ${agent.authStatus === "unauthenticated" ? "reports not signed in" : "authentication could not be checked"}. Run \`${choice === "codex" ? "codex login status" : "claude auth status"}\`; sign in or update the CLI before running.`
265
268
  : `${choice} is not on this process's PATH. Install and sign in to that CLI, or choose openai-computer-use with OPENAI_API_KEY.`,
@@ -5,6 +5,7 @@ import { detectLocalAgents } from "../actors/local-agent/cli.js";
5
5
  import { probeKeySources } from "../keys/key-resolution.js";
6
6
  import { DEFAULT_LOCAL_BROWSER_STARTER, runtimeDirectories, starterFilesFor, } from "./init-templates.js";
7
7
  import { assertPreparedSelectedOutputDirectory, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../run/contained-output.js";
8
+ import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
8
9
  import { validateCwd } from "../run/project.js";
9
10
  import { pathExists, readTextIfExists, validateInitProjectPaths } from "./init-paths.js";
10
11
  import { planGitignore, planPackageJson } from "./init-plan.js";
@@ -270,6 +271,18 @@ function validateLocalBrowserStarter(input) {
270
271
  return { ok: false, message: "--local-mission must be 4,000 characters or fewer." };
271
272
  return { ok: true, value };
272
273
  }
274
+ /**
275
+ * The first-run hint's peer install, ahead of `npx humanish`: beside humanish in its project, else
276
+ * humanish and the peer together here, where `npx humanish` then finds them. None where `npm i`
277
+ * would prune a node_modules that no package.json declares.
278
+ */
279
+ function initDesktopPeerCommand(install) {
280
+ if (install.kind === "project")
281
+ return desktopPeerAdvice(install).command;
282
+ if ((install.kind === "one-shot" || install.kind === "other") && install.unmanagedCwd)
283
+ return undefined;
284
+ return "npm i -D humanish @e2b/desktop";
285
+ }
273
286
  /**
274
287
  * What this machine can run, for the starter study and the next steps. Local CLI status is
275
288
  * classified without returning its output or reading its credential file. Provider keys go
@@ -297,10 +310,8 @@ async function firstRunEnvironment(env, cwd) {
297
310
  catch {
298
311
  hasDesktopSdk = false;
299
312
  }
300
- const { fileURLToPath } = await import("node:url");
301
- const here = fileURLToPath(import.meta.url);
302
313
  return {
303
- installedInProject: here.startsWith(path.join(process.cwd(), "node_modules") + path.sep),
314
+ desktopPeerCommand: initDesktopPeerCommand(humanishInstall()),
304
315
  hasDesktopSdk,
305
316
  hasE2bKey: present("E2B_API_KEY"),
306
317
  hasProviderKey: present("OPENAI_API_KEY"),
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Whether a dotted key path, such as `caps.maxUsd` or `participants[0].persona`, names a key a
3
+ * humanish.study.v3 file can set. A path below a leaf is not one: `scenario.ref`, where `scenario`
4
+ * is a string, and `review.analysis.question`, whose keys resolveAutomaticAnalysis checks.
5
+ */
6
+ export declare function isStudyKeyPath(path: string): boolean;
7
+ /**
8
+ * The dotted path of every key a humanish.study.v3 file can set, down to `depth` levels:
9
+ * `route`, `caps.maxUsd`, `participants.persona`. scripts/check-doc-study-fields.ts holds the
10
+ * study file reference to these paths.
11
+ */
12
+ export declare function studyKeyPaths(depth: number): string[];
1
13
  /** The first key in `raw` that V2_KEYS does not list, as an error message; undefined if none. */
2
14
  export declare function findUnknownV2Key(raw: unknown): string | undefined;
3
15
  /** The first key in a humanish.study.v3 document that the format does not have; undefined if none. */
@@ -230,6 +230,38 @@ const STUDY_KEYS = {
230
230
  defaults: V2_KEYS.defaults,
231
231
  comms: V2_KEYS.comms,
232
232
  };
233
+ /**
234
+ * Whether a dotted key path, such as `caps.maxUsd` or `participants[0].persona`, names a key a
235
+ * humanish.study.v3 file can set. A path below a leaf is not one: `scenario.ref`, where `scenario`
236
+ * is a string, and `review.analysis.question`, whose keys resolveAutomaticAnalysis checks.
237
+ */
238
+ export function isStudyKeyPath(path) {
239
+ let shape = STUDY_KEYS;
240
+ for (const segment of path.replaceAll(/\[\d*\]/g, "").split(".")) {
241
+ if (shape === true || !Object.hasOwn(shape, segment))
242
+ return false;
243
+ shape = shape[segment];
244
+ }
245
+ return true;
246
+ }
247
+ /**
248
+ * The dotted path of every key a humanish.study.v3 file can set, down to `depth` levels:
249
+ * `route`, `caps.maxUsd`, `participants.persona`. scripts/check-doc-study-fields.ts holds the
250
+ * study file reference to these paths.
251
+ */
252
+ export function studyKeyPaths(depth) {
253
+ const paths = [];
254
+ const visit = (shape, prefix, level) => {
255
+ for (const [key, child] of Object.entries(shape)) {
256
+ const path = prefix ? `${prefix}.${key}` : key;
257
+ paths.push(path);
258
+ if (child !== true && level < depth)
259
+ visit(child, path, level + 1);
260
+ }
261
+ };
262
+ visit(STUDY_KEYS, "", 1);
263
+ return paths;
264
+ }
233
265
  /** The first key in `raw` that V2_KEYS does not list, as an error message; undefined if none. */
234
266
  export function findUnknownV2Key(raw) {
235
267
  return walk(raw, V2_KEYS, "", "study");
@@ -26,6 +26,10 @@ export interface PersonaDirectives {
26
26
  /** Directives included in the prompt, not proof of behavioral adherence. */
27
27
  traitsApplied: string[];
28
28
  }
29
+ /** The fields a persona file can set; any other warns and is not sent. The study file reference lists each. */
30
+ export declare const PERSONA_FIELDS: Set<string>;
31
+ /** The traits under `traits:` that become directives; any other warns and is not sent. */
32
+ export declare const TRAIT_FIELDS: Set<string>;
29
33
  /** Legacy short fields keep their bounds, but every discarded value is diagnosed. */
30
34
  export declare function parseResolvedPersona(raw: unknown, fallback: {
31
35
  id: string;
@@ -9,7 +9,8 @@ export class PersonaConfigError extends Error {
9
9
  }
10
10
  }
11
11
  const LEVELS = ["low", "medium", "high"];
12
- const PERSONA_FIELDS = new Set([
12
+ /** The fields a persona file can set; any other warns and is not sent. The study file reference lists each. */
13
+ export const PERSONA_FIELDS = new Set([
13
14
  "schema",
14
15
  "id",
15
16
  "name",
@@ -18,7 +19,8 @@ const PERSONA_FIELDS = new Set([
18
19
  "traits",
19
20
  "constraints",
20
21
  ]);
21
- const TRAIT_FIELDS = new Set(["patience", "technical_confidence", "accessibility_needs"]);
22
+ /** The traits under `traits:` that become directives; any other warns and is not sent. */
23
+ export const TRAIT_FIELDS = new Set(["patience", "technical_confidence", "accessibility_needs"]);
22
24
  function cleanText(value) {
23
25
  return value
24
26
  .replace(/\r\n?/g, "\n")
@@ -0,0 +1,51 @@
1
+ type Manager = "npm" | "pnpm";
2
+ /** Where the running humanish is installed, read from its own file's real path. */
3
+ export type HumanishInstall =
4
+ /** The nearest package.json declaring humanish: the working directory's, or `up` levels above. */
5
+ {
6
+ kind: "project";
7
+ up: number;
8
+ manager: Manager;
9
+ }
10
+ /** A one-shot runner's cache: `npx humanish` or `pnpm dlx humanish`. */
11
+ | {
12
+ kind: "one-shot";
13
+ unmanagedCwd: boolean;
14
+ }
15
+ /** The global directory npm installs to from here, or pnpm's global directory. */
16
+ | {
17
+ kind: "global";
18
+ manager: Manager;
19
+ }
20
+ /** A source checkout of humanish, run with node or tsx. */
21
+ | {
22
+ kind: "checkout";
23
+ }
24
+ /** Anywhere else, such as another project or a global prefix npm is not configured for here. */
25
+ | {
26
+ kind: "other";
27
+ unmanagedCwd: boolean;
28
+ };
29
+ export interface InstallContext {
30
+ cwd: string;
31
+ /** The directory npm's global installs go to from this environment. */
32
+ npmGlobalRoot: string;
33
+ /** Whether the package.json in `directory` declares humanish as a dependency. */
34
+ declaresHumanish: (directory: string) => boolean;
35
+ /** Whether `npm i` from `directory` installs into a node_modules no package.json declares. */
36
+ unmanaged: (directory: string) => boolean;
37
+ }
38
+ /** The install `modulePath` belongs to. */
39
+ export declare function classifyHumanishInstall(modulePath: string, context: InstallContext): HumanishInstall;
40
+ /** The running humanish's install, from this file's real path. */
41
+ export declare function humanishInstall(cwd?: string): HumanishInstall;
42
+ /**
43
+ * Where this humanish is installed, the command that puts `spec` (`@e2b/desktop`, or a version of
44
+ * it) where it will resolve, and that command as one or two sentences of advice.
45
+ */
46
+ export declare function desktopPeerAdvice(install: HumanishInstall, spec?: string): {
47
+ where: string;
48
+ command: string;
49
+ advice: string;
50
+ };
51
+ export {};