humanish 0.110.0 → 0.110.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/ARCHITECTURE.md +15 -12
  2. package/CHANGELOG.md +26 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +103 -53
  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 +17 -0
  11. package/dist/actors/local-agent/cli.js +111 -24
  12. package/dist/actors/local-agent/readiness.d.ts +2 -2
  13. package/dist/actors/local-agent/readiness.js +20 -12
  14. package/dist/cli/commands/analyze.js +21 -5
  15. package/dist/cli/commands/observe.js +15 -2
  16. package/dist/cli/commands/run-signals.d.ts +2 -0
  17. package/dist/cli/commands/run-signals.js +106 -45
  18. package/dist/cli/commands/runs.d.ts +5 -0
  19. package/dist/cli/commands/runs.js +113 -74
  20. package/dist/cli/commands/setup.js +23 -14
  21. package/dist/cli/commands/study-route-computer-use.js +9 -3
  22. package/dist/cli/commands/study-route-open.js +1 -0
  23. package/dist/cli/commands/study-route-preview.js +1 -0
  24. package/dist/cli/commands/study-route-scripted.js +2 -0
  25. package/dist/cli/commands/study-route-shared-world.js +6 -2
  26. package/dist/cli/commands/study-route-terminal.js +2 -0
  27. package/dist/cli/commands/watch.js +14 -4
  28. package/dist/cli/doctor.d.ts +4 -1
  29. package/dist/cli/doctor.js +51 -10
  30. package/dist/cli/findings.d.ts +103 -0
  31. package/dist/cli/findings.js +317 -0
  32. package/dist/cli/first-run-path.d.ts +32 -11
  33. package/dist/cli/first-run-path.js +113 -69
  34. package/dist/cli/invocation.d.ts +7 -0
  35. package/dist/cli/invocation.js +32 -0
  36. package/dist/cli/io.d.ts +3 -1
  37. package/dist/cli/io.js +8 -15
  38. package/dist/cli/observer-follow.d.ts +20 -0
  39. package/dist/cli/observer-follow.js +36 -1
  40. package/dist/cli/orientation.js +16 -14
  41. package/dist/cli/product-sentence.d.ts +3 -3
  42. package/dist/cli/product-sentence.js +3 -3
  43. package/dist/cli/version.d.ts +1 -0
  44. package/dist/cli/version.js +11 -0
  45. package/dist/evidence/encoded-text.d.ts +9 -6
  46. package/dist/evidence/encoded-text.js +84 -19
  47. package/dist/evidence/redaction.d.ts +2 -0
  48. package/dist/evidence/redaction.js +123 -21
  49. package/dist/feedback/export-bundle.js +2 -2
  50. package/dist/keys/env-file.d.ts +4 -0
  51. package/dist/keys/env-file.js +17 -0
  52. package/dist/keys/key-resolution.js +6 -5
  53. package/dist/observer/data.d.ts +14 -1
  54. package/dist/observer/data.js +19 -9
  55. package/dist/observer/exposure.js +1 -1
  56. package/dist/observer/library.d.ts +4 -0
  57. package/dist/observer/library.js +21 -17
  58. package/dist/observer/render.js +10 -3
  59. package/dist/observer/run-routes.d.ts +2 -0
  60. package/dist/observer/run-routes.js +51 -35
  61. package/dist/observer/serve.js +2 -1
  62. package/dist/observer-app.html +3 -3
  63. package/dist/routes/computer-use/bundle.js +3 -1
  64. package/dist/routes/computer-use/fanout-records.js +1 -1
  65. package/dist/routes/computer-use/participant-execution.js +3 -1
  66. package/dist/routes/computer-use/participant-model.js +5 -2
  67. package/dist/routes/computer-use/plan.js +9 -4
  68. package/dist/routes/computer-use/preflight.js +1 -1
  69. package/dist/routes/computer-use/result.js +17 -28
  70. package/dist/routes/computer-use/single-bundle.js +7 -1
  71. package/dist/routes/scripted/bundle.js +4 -2
  72. package/dist/routes/scripted/plan.js +2 -2
  73. package/dist/routes/scripted/result.d.ts +3 -5
  74. package/dist/routes/scripted/result.js +16 -22
  75. package/dist/routes/scripted/scenario.js +4 -4
  76. package/dist/routes/shared-world/bundle-records.js +6 -1
  77. package/dist/routes/shared-world/bundle.js +2 -0
  78. package/dist/routes/shared-world/plan.js +6 -2
  79. package/dist/routes/shared-world/result.js +22 -22
  80. package/dist/routes/terminal/bundle.js +6 -3
  81. package/dist/routes/terminal/credentials.d.ts +5 -5
  82. package/dist/routes/terminal/credentials.js +14 -6
  83. package/dist/routes/terminal/dry-run.js +8 -12
  84. package/dist/routes/terminal/ledger.d.ts +3 -3
  85. package/dist/routes/terminal/ledger.js +6 -6
  86. package/dist/routes/terminal/live-finish.js +15 -13
  87. package/dist/routes/terminal/plan.js +12 -4
  88. package/dist/routes/terminal/result.d.ts +6 -7
  89. package/dist/routes/terminal/result.js +6 -22
  90. package/dist/routes/terminal/route.js +5 -4
  91. package/dist/routes/terminal/runtime-auth.d.ts +6 -0
  92. package/dist/routes/terminal/runtime-auth.js +5 -0
  93. package/dist/routes/terminal/session.js +1 -1
  94. package/dist/run/bundle-shape.d.ts +3 -1
  95. package/dist/run/bundle-shape.js +25 -1
  96. package/dist/run/bundle.d.ts +24 -0
  97. package/dist/run/display.d.ts +59 -0
  98. package/dist/run/display.js +106 -0
  99. package/dist/run/dry-run.js +13 -8
  100. package/dist/run/judge.d.ts +3 -2
  101. package/dist/run/judge.js +12 -1
  102. package/dist/run/local-only-files.d.ts +13 -0
  103. package/dist/run/local-only-files.js +35 -0
  104. package/dist/run/pricing.js +1 -1
  105. package/dist/run/projection.d.ts +0 -2
  106. package/dist/run/projection.js +0 -11
  107. package/dist/run/reclaim-outcomes.d.ts +89 -0
  108. package/dist/run/reclaim-outcomes.js +213 -0
  109. package/dist/run/reclaim.d.ts +23 -17
  110. package/dist/run/reclaim.js +213 -114
  111. package/dist/run/results.d.ts +10 -20
  112. package/dist/run/results.js +2 -2
  113. package/dist/run/run-index.d.ts +11 -1
  114. package/dist/run/run-index.js +11 -0
  115. package/dist/run/run-not-found.js +3 -2
  116. package/dist/run/run.d.ts +34 -18
  117. package/dist/run/run.js +166 -67
  118. package/dist/run/sandbox-creates.d.ts +59 -0
  119. package/dist/run/sandbox-creates.js +140 -0
  120. package/dist/run/sandbox-receipts.d.ts +20 -5
  121. package/dist/run/sandbox-receipts.js +56 -7
  122. package/dist/run/stats.d.ts +5 -0
  123. package/dist/run/stats.js +8 -4
  124. package/dist/run/status.d.ts +6 -9
  125. package/dist/run/status.js +7 -23
  126. package/dist/run/stored-runs.d.ts +5 -14
  127. package/dist/run/stored-runs.js +31 -146
  128. package/dist/run/streams.d.ts +3 -2
  129. package/dist/study/agents-section.d.ts +21 -0
  130. package/dist/study/agents-section.js +86 -0
  131. package/dist/study/doctor.d.ts +1 -1
  132. package/dist/study/doctor.js +8 -4
  133. package/dist/study/init-templates.js +0 -95
  134. package/dist/study/init.js +45 -37
  135. package/dist/study/keys.d.ts +12 -0
  136. package/dist/study/keys.js +32 -0
  137. package/dist/study/local-browser-host.d.ts +11 -0
  138. package/dist/study/local-browser-host.js +42 -0
  139. package/dist/study/parse/actors.js +9 -1
  140. package/dist/study/parse/subject.js +12 -0
  141. package/dist/study/parse/url-credentials.d.ts +9 -0
  142. package/dist/study/parse/url-credentials.js +50 -0
  143. package/dist/study/persona.d.ts +4 -0
  144. package/dist/study/persona.js +4 -2
  145. package/dist/study/preflight-probes.js +2 -1
  146. package/dist/study/types.d.ts +8 -6
  147. package/dist/study/url-credentials.d.ts +6 -0
  148. package/dist/study/url-credentials.js +34 -0
  149. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  150. package/dist/substrates/e2b/peer-install.js +161 -0
  151. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  152. package/dist/substrates/e2b/sandbox.js +141 -30
  153. package/dist/substrates/e2b/sdk.d.ts +33 -0
  154. package/dist/substrates/e2b/sdk.js +21 -21
  155. package/dist/substrates/local/firecracker-desktop.js +3 -2
  156. package/dist/substrates/local/runtime-host.js +5 -4
  157. package/dist/substrates/local/runtime.js +5 -4
  158. package/dist/substrates/shell.d.ts +2 -0
  159. package/dist/substrates/shell.js +4 -0
  160. package/dist/tui/contract.js +5 -4
  161. package/dist/tui-app.js +133 -133
  162. package/dist/verify/liveness.d.ts +19 -3
  163. package/dist/verify/liveness.js +47 -25
  164. package/dist/verify/verify.d.ts +3 -0
  165. package/dist/verify/verify.js +8 -3
  166. package/docs/architecture/actor-contract.md +2 -1
  167. package/docs/architecture/observer.md +22 -7
  168. package/docs/architecture/task-protocol-support.md +13 -13
  169. package/docs/architecture/terminal-product-route.md +15 -13
  170. package/docs/contracts/run-bundle.md +95 -10
  171. package/docs/contracts/schemas.md +44 -26
  172. package/docs/contracts/study-analysis.md +45 -0
  173. package/docs/contracts/study-costs.md +8 -0
  174. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  175. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  176. package/docs/principles/invariants-and-defaults.md +11 -8
  177. package/package.json +6 -3
  178. package/skills/humanish/SKILL.md +38 -20
@@ -1,6 +1,8 @@
1
- // Every created sandbox id is journaled to the run dir the moment create returns, before any work
2
- // happens in it. The participant loop lives in the caller's local process, so a sleeping laptop, crash or
3
- // kill loses every in-memory id; this append-only file keeps them for `humanish reclaim`.
1
+ // Every created sandbox id is journaled to the run dir as soon as it reaches this process: when the
2
+ // desktop startup guard reports it, or when create returns, before any work happens in it. The
3
+ // participant loop lives in the caller's local process, so a sleeping laptop, crash or kill loses
4
+ // every in-memory id; this append-only file keeps them for `humanish reclaim`, and tells verify and
5
+ // the sweep which ids to look for.
4
6
  import { appendFile } from "node:fs/promises";
5
7
  import { prepareContainedOutputFile } from "./contained-output.js";
6
8
  export const SANDBOX_RECEIPTS_ARTIFACT = "sandbox-receipts.ndjson";
@@ -22,10 +24,11 @@ export function appendedSandboxIds(root) {
22
24
  return [...(appended.get(rootKey(root)) ?? [])];
23
25
  }
24
26
  /**
25
- * Append one receipt. Best-effort by design: the receipt exists to protect the run, so a failed
26
- * receipt write must never fail the participant. The only cost of a miss is that `reclaim` cannot see
27
- * this id and the TTL backstop covers it instead. Containment is the same prepare step every
28
- * artifact write uses; append (not atomic-replace) keeps racing participants' receipts intact.
27
+ * Append one receipt; false when the write failed. Best-effort by design: the receipt exists to
28
+ * protect the run, so a failed receipt write must never fail the participant. The only cost of a
29
+ * miss is that `reclaim` cannot see this id and the TTL backstop covers it instead. Containment is
30
+ * the same prepare step every artifact write uses; append (not atomic-replace) keeps racing
31
+ * participants' receipts intact.
29
32
  */
30
33
  export async function appendSandboxReceipt(root, receipt) {
31
34
  const key = rootKey(root);
@@ -40,11 +43,57 @@ export async function appendSandboxReceipt(root, receipt) {
40
43
  try {
41
44
  const filePath = await prepareContainedOutputFile(root, SANDBOX_RECEIPTS_ARTIFACT);
42
45
  await appendFile(filePath, `${JSON.stringify(receipt)}\n`, "utf8");
46
+ return true;
43
47
  }
44
48
  catch {
45
49
  // Swallowed on purpose; see the contract above.
50
+ return false;
46
51
  }
47
52
  }
53
+ /** Append one owner line; false when the write failed, which never fails the create. */
54
+ export async function appendSandboxOwner(root, owner) {
55
+ try {
56
+ const line = { at: new Date().toISOString(), provider: "e2b", owner };
57
+ const filePath = await prepareContainedOutputFile(root, SANDBOX_RECEIPTS_ARTIFACT);
58
+ // The leading newline ends a line an earlier append left torn, so this record parses.
59
+ await appendFile(filePath, `\n${JSON.stringify(line)}\n`, "utf8");
60
+ return true;
61
+ }
62
+ catch {
63
+ // Never fails the create. Without the line, reclaim reports the run's sandboxes as unknown,
64
+ // never clean, so the next create tries again.
65
+ return false;
66
+ }
67
+ }
68
+ const RUN_KEY = /^[0-9a-f]{16}$/;
69
+ /**
70
+ * The owner tag sets a journal's owner lines record for the run named `runId`. Only a complete
71
+ * tuple counts: provider e2b, `tool: humanish`, this run's id and a well-formed key, and nothing
72
+ * else. A partial or foreign line would widen reclaim's search past this run, so it is skipped.
73
+ */
74
+ export function parseSandboxOwners(text, runId) {
75
+ const owners = new Map();
76
+ for (const line of text.split("\n")) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed)
79
+ continue;
80
+ try {
81
+ const parsed = JSON.parse(trimmed);
82
+ const owner = parsed.owner;
83
+ if (parsed.provider !== "e2b" || owner === null || typeof owner !== "object")
84
+ continue;
85
+ if (Object.keys(owner).length !== 3 || owner.tool !== "humanish" || owner.runId !== runId)
86
+ continue;
87
+ if (typeof owner.runKey !== "string" || !RUN_KEY.test(owner.runKey))
88
+ continue;
89
+ owners.set(owner.runKey, { tool: "humanish", runId, runKey: owner.runKey });
90
+ }
91
+ catch {
92
+ // A torn line: skip it.
93
+ }
94
+ }
95
+ return [...owners.values()];
96
+ }
48
97
  /** Parse a receipts file leniently: a torn final line (crash mid-append) drops, valid lines keep. */
49
98
  export function parseSandboxReceipts(text) {
50
99
  const receipts = [];
@@ -1,3 +1,4 @@
1
+ import { type RunDisplayState } from "./display.js";
1
2
  import { type CostTotals, type CostRow } from "./costs.js";
2
3
  declare const STATS_SCHEMA = "humanish.stats.v2";
3
4
  interface StatsParticipants {
@@ -13,6 +14,7 @@ interface StatsStudyRow {
13
14
  dryRun: number;
14
15
  /** Runs whose bundle carries a verdict; the denominator for passRate. */
15
16
  judged: number;
17
+ /** Runs that show as passed (runDisplay in src/run/display.ts): a pass verdict and an ok run. */
16
18
  passed: number;
17
19
  /** passed / judged, absent when judged is 0. */
18
20
  passRate?: number;
@@ -52,7 +54,10 @@ export interface StatsResult {
52
54
  estimatedSpendUsd: number;
53
55
  unpricedRuns: number;
54
56
  participants: StatsParticipants;
57
+ /** The participants' verdicts. `outcomes` says how many runs passed. */
55
58
  verdicts: Record<string, number>;
59
+ /** Runs by the state every surface shows them in (runDisplay in src/run/display.ts). */
60
+ outcomes: Partial<Record<RunDisplayState, number>>;
56
61
  costs: CostTotals;
57
62
  };
58
63
  studies: StatsStudyRow[];
package/dist/run/stats.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // numbers under the per-run rules: an estimate is labelled an estimate, a run whose cost is unknown
3
3
  // counts as unknown and never as zero, and every rate has a denominator next to it.
4
4
  import path from "node:path";
5
+ import { runDisplay, runDisplayLabel } from "./display.js";
5
6
  import { readRunIndex } from "./run-index.js";
6
7
  import { addCostTotals, emptyCostTotals, readCostTotals, } from "./costs.js";
7
8
  import { round6 } from "./pricing.js";
@@ -68,7 +69,7 @@ function addToStudyRow(studies, entry, runUsd, costs) {
68
69
  row.dryRun += 1;
69
70
  if (entry.verdict !== undefined) {
70
71
  row.judged += 1;
71
- if (entry.verdict === "pass")
72
+ if (runDisplay(entry).state === "passed")
72
73
  row.passed += 1;
73
74
  }
74
75
  if (entry.mode === "live" && entry.durationMs !== undefined)
@@ -157,6 +158,7 @@ export async function computeStats(cwdInput, options = {}) {
157
158
  unpricedRuns: 0,
158
159
  participants: emptyParticipants(),
159
160
  verdicts: {},
161
+ outcomes: {},
160
162
  costs: emptyCostTotals(),
161
163
  };
162
164
  const studies = new Map();
@@ -181,6 +183,8 @@ export async function computeStats(cwdInput, options = {}) {
181
183
  addParticipants(totals.participants, entry);
182
184
  if (entry.verdict !== undefined)
183
185
  totals.verdicts[entry.verdict] = (totals.verdicts[entry.verdict] ?? 0) + 1;
186
+ const { state } = runDisplay(entry);
187
+ totals.outcomes[state] = (totals.outcomes[state] ?? 0) + 1;
184
188
  addToStudyRow(studies, entry, runUsd, accounting.costs);
185
189
  addToDayRow(days, entry, runUsd, accounting.costs);
186
190
  }
@@ -239,7 +243,7 @@ function runCountLines(t) {
239
243
  const running = t.running > 0 ? ` (${t.running} still running)` : "";
240
244
  return [`live runs: ${t.live}${running}`, ...(t.dryRun > 0 ? [`dry runs: ${t.dryRun}`] : [])];
241
245
  }
242
- /** Spend, analysis, participants and verdicts across the selected live runs. */
246
+ /** Spend, analysis, participants and outcomes across the selected live runs. */
243
247
  function totalsLines(t) {
244
248
  const c = t.costs;
245
249
  const incomplete = c.incompleteRunEstimates > 0
@@ -248,7 +252,7 @@ function totalsLines(t) {
248
252
  const unpriced = c.analysisUnpricedAttempts > 0 || c.analysisUnresolvedAttempts > 0
249
253
  ? `; ${c.analysisUnpricedAttempts} unpriced, ${c.analysisUnresolvedAttempts} unresolved`
250
254
  : "";
251
- const verdicts = Object.entries(t.verdicts).map(([verdict, count]) => `${verdict} ${count}`);
255
+ const outcomes = Object.entries(t.outcomes).map(([state, count]) => `${runDisplayLabel(state)} ${count}`);
252
256
  return [
253
257
  `known estimated spend: ${knownMoney(c.estimatedTotalUsd)}`,
254
258
  `participants and desktops: ${knownMoney(c.runEstimatedUsd)}${incomplete}`,
@@ -265,7 +269,7 @@ function totalsLines(t) {
265
269
  `participants: ${t.participants.reachedGoal} of ${t.participants.total} reached the goal, ${t.participants.reportedFriction} reported friction`,
266
270
  ]
267
271
  : []),
268
- ...(verdicts.length > 0 ? [`verdicts: ${verdicts.join(", ")}`] : []),
272
+ ...(outcomes.length > 0 ? [`outcomes: ${outcomes.join(", ")}`] : []),
269
273
  ];
270
274
  }
271
275
  function dayLine(row) {
@@ -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";
@@ -0,0 +1,21 @@
1
+ export type AgentsSectionReplacement =
2
+ /** The file has no humanish section. */
3
+ {
4
+ kind: "absent";
5
+ }
6
+ /** A start marker with no end marker, in text no release wrote: possibly hand-edited. */
7
+ | {
8
+ kind: "unknown";
9
+ }
10
+ /** The file with humanish's section replaced; equal to the input when it was already current. */
11
+ | {
12
+ kind: "replaced";
13
+ contents: string;
14
+ };
15
+ /**
16
+ * `existing` with humanish's section replaced by `section` (as `agentsSection()` returns it, with
17
+ * its leading blank line). The replaced range runs from the start of the heading line that holds
18
+ * the start marker to the end of the end marker's line, or, without an end marker, through the
19
+ * text of the release that wrote it.
20
+ */
21
+ export declare function replaceAgentsSection(existing: string, section: string): AgentsSectionReplacement;
@@ -0,0 +1,86 @@
1
+ // Finds humanish's own section in a project's `AGENTS.md` so init can replace it on a rerun and
2
+ // leave every other byte alone. Releases up to 0.110.1 wrote the section with a start marker and
3
+ // no end marker, so for those the section's extent is known only by matching a text init wrote.
4
+ import { createHash } from "node:crypto";
5
+ import { AGENTS_SECTION_END_MARKER, AGENTS_SECTION_MARKER } from "../cli/first-run-path.js";
6
+ /**
7
+ * Each section text a release wrote before the end marker existed, from its `## humanish` heading
8
+ * to its final newline: the UTF-16 length and the sha256 of its UTF-8 bytes. Comparing digests
9
+ * keeps the retired command and key names those versions taught out of the source.
10
+ */
11
+ const SECTIONS_WITHOUT_END_MARKER = [
12
+ {
13
+ releases: "0.59.0 to 0.80.0",
14
+ length: 1171,
15
+ sha256: "3739c98204cc5b2df603238398bffc5c4a491782a3ab72475b0ad9d967698a11",
16
+ },
17
+ {
18
+ releases: "0.81.0 to 0.93.1",
19
+ length: 1191,
20
+ sha256: "169d646f329f35af1b5e51eb02cd0454a717cf29a447a230f3aa2d5a7354729c",
21
+ },
22
+ {
23
+ releases: "0.94.0 to 0.99.1",
24
+ length: 1214,
25
+ sha256: "68f987769893f041ab78e0b1cb4cd8db14a438511f15b58f9eaa412ea002b51f",
26
+ },
27
+ {
28
+ releases: "0.100.0 to 0.105.0",
29
+ length: 1548,
30
+ sha256: "f60c37003d21b1d8280d002f203130c60bc3c23454b018b3207407faa01cb76f",
31
+ },
32
+ {
33
+ releases: "0.106.0 to 0.107.0",
34
+ length: 1686,
35
+ sha256: "c2cd9d831e17021ad4477f300dbea13df1613bec8f30ebbabfabcd749b73f7f7",
36
+ },
37
+ {
38
+ releases: "0.108.0",
39
+ length: 1679,
40
+ sha256: "6352ec71d7c5073b2bf4993293753944eaf7cfd21169a311f42d9f52bdacfead",
41
+ },
42
+ {
43
+ releases: "0.109.0 to 0.110.0",
44
+ length: 1681,
45
+ sha256: "2559a93463cf50225aadf6753e84dd6e32b25aa3afdf8066e90d19c5d88d0ef0",
46
+ },
47
+ {
48
+ releases: "0.110.1",
49
+ length: 2014,
50
+ sha256: "9fb4cb94400c7447ab0fece861339e50de8e9be99797cf0763d7502be0fe668d",
51
+ },
52
+ ];
53
+ /**
54
+ * `existing` with humanish's section replaced by `section` (as `agentsSection()` returns it, with
55
+ * its leading blank line). The replaced range runs from the start of the heading line that holds
56
+ * the start marker to the end of the end marker's line, or, without an end marker, through the
57
+ * text of the release that wrote it.
58
+ */
59
+ export function replaceAgentsSection(existing, section) {
60
+ const marker = existing.indexOf(AGENTS_SECTION_MARKER);
61
+ if (marker < 0)
62
+ return { kind: "absent" };
63
+ const start = existing.lastIndexOf("\n", marker) + 1;
64
+ const replacement = section.replace(/^\n/, "");
65
+ const endMarker = existing.indexOf(AGENTS_SECTION_END_MARKER, marker);
66
+ if (endMarker >= 0) {
67
+ const lineEnd = existing.indexOf("\n", endMarker);
68
+ const end = lineEnd < 0 ? existing.length : lineEnd + 1;
69
+ return {
70
+ kind: "replaced",
71
+ contents: existing.slice(0, start) + replacement + existing.slice(end),
72
+ };
73
+ }
74
+ for (const known of SECTIONS_WITHOUT_END_MARKER) {
75
+ const candidate = existing.slice(start, start + known.length);
76
+ if (candidate.length !== known.length)
77
+ continue;
78
+ if (createHash("sha256").update(candidate).digest("hex") !== known.sha256)
79
+ continue;
80
+ return {
81
+ kind: "replaced",
82
+ contents: existing.slice(0, start) + replacement + existing.slice(start + known.length),
83
+ };
84
+ }
85
+ return { kind: "unknown" };
86
+ }
@@ -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";