humanish 0.109.1 → 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 (164) hide show
  1. package/ARCHITECTURE.md +14 -11
  2. package/CHANGELOG.md +24 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +97 -48
  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/comms.js +25 -7
  16. package/dist/cli/commands/observe.js +4 -2
  17. package/dist/cli/commands/run-command.js +4 -3
  18. package/dist/cli/commands/run-signals.d.ts +2 -0
  19. package/dist/cli/commands/run-signals.js +96 -44
  20. package/dist/cli/commands/runs.d.ts +5 -0
  21. package/dist/cli/commands/runs.js +89 -67
  22. package/dist/cli/commands/setup.js +19 -5
  23. package/dist/cli/commands/study-format.js +29 -2
  24. package/dist/cli/commands/study-route-computer-use.js +2 -0
  25. package/dist/cli/commands/study-route-scripted.js +2 -0
  26. package/dist/cli/commands/study-route-shared-world.js +2 -0
  27. package/dist/cli/commands/study-route-terminal.js +2 -0
  28. package/dist/cli/commands/study.js +4 -3
  29. package/dist/cli/commands/tui.js +4 -3
  30. package/dist/cli/commands/watch.js +2 -2
  31. package/dist/cli/doctor.d.ts +7 -2
  32. package/dist/cli/doctor.js +47 -8
  33. package/dist/cli/findings.d.ts +103 -0
  34. package/dist/cli/findings.js +316 -0
  35. package/dist/cli/first-run-path.d.ts +5 -4
  36. package/dist/cli/first-run-path.js +14 -7
  37. package/dist/cli/io.d.ts +17 -3
  38. package/dist/cli/io.js +22 -4
  39. package/dist/cli/product-sentence.d.ts +3 -3
  40. package/dist/cli/product-sentence.js +3 -3
  41. package/dist/comms/receiving-runtime.js +1 -1
  42. package/dist/comms/setup.js +1 -1
  43. package/dist/evidence/redaction.d.ts +7 -3
  44. package/dist/evidence/redaction.js +68 -24
  45. package/dist/feedback/export-bundle.js +6 -1
  46. package/dist/feedback/export.js +3 -0
  47. package/dist/keys/env-file.d.ts +4 -0
  48. package/dist/keys/env-file.js +17 -0
  49. package/dist/keys/key-resolution.d.ts +7 -0
  50. package/dist/keys/key-resolution.js +10 -8
  51. package/dist/observer/data.d.ts +14 -1
  52. package/dist/observer/data.js +19 -9
  53. package/dist/observer/library.d.ts +4 -0
  54. package/dist/observer/library.js +18 -15
  55. package/dist/observer/render.js +8 -2
  56. package/dist/observer/run-routes.d.ts +2 -0
  57. package/dist/observer/run-routes.js +43 -34
  58. package/dist/observer-app.html +3 -3
  59. package/dist/routes/computer-use/bundle.js +3 -1
  60. package/dist/routes/computer-use/fanout-records.js +1 -1
  61. package/dist/routes/computer-use/live-phase.d.ts +1 -0
  62. package/dist/routes/computer-use/participant-execution.js +3 -1
  63. package/dist/routes/computer-use/participant-model.js +5 -2
  64. package/dist/routes/computer-use/plan.js +4 -4
  65. package/dist/routes/computer-use/preflight.js +2 -2
  66. package/dist/routes/computer-use/result.js +17 -28
  67. package/dist/routes/computer-use/single-bundle.js +7 -1
  68. package/dist/routes/computer-use/types.d.ts +6 -0
  69. package/dist/routes/scripted/bundle.js +4 -2
  70. package/dist/routes/scripted/plan.js +2 -2
  71. package/dist/routes/scripted/result.d.ts +3 -5
  72. package/dist/routes/scripted/result.js +16 -22
  73. package/dist/routes/scripted/scenario.js +4 -4
  74. package/dist/routes/scripted/types.d.ts +1 -0
  75. package/dist/routes/shared-world/bundle-records.js +6 -1
  76. package/dist/routes/shared-world/bundle.js +2 -0
  77. package/dist/routes/shared-world/plan.js +1 -1
  78. package/dist/routes/shared-world/result.js +22 -22
  79. package/dist/routes/shared-world/route.js +1 -1
  80. package/dist/routes/shared-world/types.d.ts +2 -0
  81. package/dist/routes/terminal/bundle.js +4 -2
  82. package/dist/routes/terminal/credentials.js +1 -1
  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 +23 -17
  87. package/dist/routes/terminal/plan.js +4 -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/session.js +1 -1
  91. package/dist/routes/terminal/types.d.ts +3 -0
  92. package/dist/run/bundle-shape.d.ts +3 -1
  93. package/dist/run/bundle-shape.js +26 -1
  94. package/dist/run/bundle.d.ts +30 -0
  95. package/dist/run/display.d.ts +59 -0
  96. package/dist/run/display.js +106 -0
  97. package/dist/run/dry-run.js +11 -7
  98. package/dist/run/judge.d.ts +3 -2
  99. package/dist/run/judge.js +10 -0
  100. package/dist/run/pricing.js +1 -1
  101. package/dist/run/projection.d.ts +0 -2
  102. package/dist/run/projection.js +0 -11
  103. package/dist/run/reclaim-outcomes.d.ts +89 -0
  104. package/dist/run/reclaim-outcomes.js +213 -0
  105. package/dist/run/reclaim.d.ts +18 -14
  106. package/dist/run/reclaim.js +179 -105
  107. package/dist/run/results.d.ts +11 -18
  108. package/dist/run/results.js +2 -2
  109. package/dist/run/run-index.d.ts +11 -1
  110. package/dist/run/run-index.js +11 -0
  111. package/dist/run/run.d.ts +34 -18
  112. package/dist/run/run.js +177 -72
  113. package/dist/run/sandbox-creates.d.ts +59 -0
  114. package/dist/run/sandbox-creates.js +138 -0
  115. package/dist/run/sandbox-ids.d.ts +53 -0
  116. package/dist/run/sandbox-ids.js +198 -0
  117. package/dist/run/sandbox-receipts.d.ts +16 -0
  118. package/dist/run/sandbox-receipts.js +70 -0
  119. package/dist/run/stats.d.ts +5 -0
  120. package/dist/run/stats.js +8 -4
  121. package/dist/run/status.d.ts +6 -9
  122. package/dist/run/status.js +7 -20
  123. package/dist/run/stored-runs.d.ts +5 -14
  124. package/dist/run/stored-runs.js +31 -137
  125. package/dist/run/streams.d.ts +3 -2
  126. package/dist/run-study.js +6 -1
  127. package/dist/study/doctor.d.ts +1 -1
  128. package/dist/study/doctor.js +7 -4
  129. package/dist/study/init-templates.js +2 -2
  130. package/dist/study/init.js +14 -3
  131. package/dist/study/keys.d.ts +12 -0
  132. package/dist/study/keys.js +32 -0
  133. package/dist/study/persona.d.ts +4 -0
  134. package/dist/study/persona.js +4 -2
  135. package/dist/study/types.d.ts +1 -1
  136. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  137. package/dist/substrates/e2b/peer-install.js +161 -0
  138. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  139. package/dist/substrates/e2b/sandbox.js +128 -35
  140. package/dist/substrates/e2b/sdk.d.ts +33 -0
  141. package/dist/substrates/e2b/sdk.js +21 -21
  142. package/dist/substrates/shell.d.ts +2 -0
  143. package/dist/substrates/shell.js +4 -0
  144. package/dist/tui-app.js +133 -133
  145. package/dist/verify/artifacts.d.ts +4 -0
  146. package/dist/verify/artifacts.js +8 -0
  147. package/dist/verify/liveness.d.ts +19 -3
  148. package/dist/verify/liveness.js +53 -28
  149. package/dist/verify/verify.d.ts +4 -1
  150. package/dist/verify/verify.js +27 -3
  151. package/docs/architecture/actor-contract.md +2 -1
  152. package/docs/architecture/observer.md +22 -7
  153. package/docs/architecture/restricted-codex-analysis.md +1 -1
  154. package/docs/architecture/task-protocol-support.md +13 -13
  155. package/docs/architecture/terminal-product-route.md +1 -1
  156. package/docs/contracts/run-bundle.md +90 -7
  157. package/docs/contracts/schemas.md +39 -22
  158. package/docs/contracts/study-analysis.md +45 -0
  159. package/docs/contracts/study-costs.md +8 -0
  160. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  161. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  162. package/docs/principles/invariants-and-defaults.md +11 -8
  163. package/package.json +5 -3
  164. package/skills/humanish/SKILL.md +18 -4
@@ -0,0 +1,198 @@
1
+ // A run's raw sandbox ids live in one place: sandbox-receipts.ndjson, which reclaim reads to kill
2
+ // them and export omits. Every other record a run writes (run.json, its review and projections, the
3
+ // cleanup and reclaim receipts) and every result or CLI line names a sandbox by marker and digest,
4
+ // so output an agent pastes into an issue never carries an id.
5
+ import { opendir, realpath } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { readPlainText } from "../evidence/plain-text.js";
8
+ import { collectSandboxIds, redactSandboxIds, sandboxIdDigest, REDACTED_SANDBOX_ID, } from "../evidence/redaction.js";
9
+ import { readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
10
+ import { bindExistingRunArtifactPaths, isSafeRunIdSegment, validatePreparedRunRootIdentity, } from "./paths.js";
11
+ import { appendedSandboxIds, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
12
+ /** How text names a sandbox: the marker with the id's digest. */
13
+ function sandboxIdLabel(id) {
14
+ return `${REDACTED_SANDBOX_ID.slice(0, -1)} ${sandboxIdDigest(id)}]`;
15
+ }
16
+ /**
17
+ * How YAML names a sandbox: the label without its brackets, since a plain scalar that starts with
18
+ * `[` reads as a list. Every other byte of the file stays as written.
19
+ */
20
+ function yamlSandboxIdLabel(id) {
21
+ return `redacted-sandbox-id-${sandboxIdDigest(id)}`;
22
+ }
23
+ /** `text` with every occurrence of each raw id replaced by its label, longest id first. */
24
+ export function scrubSandboxIds(text, ids, label = sandboxIdLabel) {
25
+ const unique = [...new Set(ids)].filter((id) => id.length > 0);
26
+ unique.sort((left, right) => right.length - left.length);
27
+ return unique.reduce((current, id) => current.split(id).join(label(id)), text);
28
+ }
29
+ /**
30
+ * `value` as it may be written or returned: ids at the sandbox-id keys become the marker and a
31
+ * digest, and any raw id left in free text or a URL becomes its label.
32
+ */
33
+ export function publicSandboxView(value, ids) {
34
+ const redacted = redactSandboxIds(value);
35
+ const known = [...ids];
36
+ if (known.length === 0)
37
+ return redacted;
38
+ const text = JSON.stringify(redacted);
39
+ if (text === undefined)
40
+ return redacted;
41
+ const scrubbed = scrubSandboxIds(text, known);
42
+ return (scrubbed === text ? redacted : JSON.parse(scrubbed));
43
+ }
44
+ /** The raw sandbox ids a receipts journal names. */
45
+ function receiptSandboxIds(journal) {
46
+ return parseSandboxReceipts(journal.toString()).map((receipt) => receipt.sandboxId);
47
+ }
48
+ /**
49
+ * The raw ids at sandbox-id keys in `value`, such as a run.json recorded before 0.110. A value
50
+ * shorter than an E2B id is left to the key rule, so the text scrub never rewrites a common word.
51
+ */
52
+ export function keyedSandboxIds(value) {
53
+ return [...collectSandboxIds(value)].filter((id) => id.length >= 8);
54
+ }
55
+ /** The JSON records in one file's text: one document, or one per NDJSON line. */
56
+ function jsonRecords(file, text) {
57
+ // Most run files name no sandbox key, and parsing each one would slow verify on large runs. A
58
+ // key spelled with an escape only shows once parsed.
59
+ if (!/"(?:sandboxId|subjectSandboxId|providerResources|resources)"|\\u/.test(text))
60
+ return [];
61
+ const parse = (part) => {
62
+ try {
63
+ return [JSON.parse(part)];
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ };
69
+ if (/\.json$/i.test(file))
70
+ return parse(text);
71
+ if (/\.(?:ndjson|jsonl)$/i.test(file))
72
+ return text.split("\n").flatMap(parse);
73
+ return [];
74
+ }
75
+ /**
76
+ * Every raw sandbox id a run's files name: the receipts', and any at a sandbox-id key in its
77
+ * JSON, which is where a run from before 0.110 that has no receipts still records them.
78
+ */
79
+ export function inventorySandboxIds(files) {
80
+ const ids = new Set();
81
+ for (const file of files) {
82
+ if (file.path === SANDBOX_RECEIPTS_ARTIFACT) {
83
+ for (const id of receiptSandboxIds(file.bytes))
84
+ ids.add(id);
85
+ continue;
86
+ }
87
+ for (const record of jsonRecords(file.path, file.bytes.toString("utf8")))
88
+ for (const id of keyedSandboxIds(record))
89
+ ids.add(id);
90
+ }
91
+ return [...ids];
92
+ }
93
+ /** Whether a run file's JSON holds a raw id at a sandbox-id key, which no receipt may name. */
94
+ export function holdsKeyedSandboxId(file, text) {
95
+ return jsonRecords(file, text).some((record) => collectSandboxIds(record).size > 0);
96
+ }
97
+ /**
98
+ * The raw sandbox ids a run's receipts journal, with any this process receipted whose append
99
+ * failed; empty when there are none or the journal cannot be read.
100
+ */
101
+ export async function readRunSandboxIds(root) {
102
+ let journaled = [];
103
+ try {
104
+ const bytes = await readContainedRegularFile(root, SANDBOX_RECEIPTS_ARTIFACT);
105
+ if (bytes)
106
+ journaled = receiptSandboxIds(bytes);
107
+ }
108
+ catch {
109
+ // The ids this process receipted still apply.
110
+ }
111
+ return [...new Set([...journaled, ...appendedSandboxIds(root)])];
112
+ }
113
+ /**
114
+ * `value` as a run writes it: raw sandbox ids stay in the run's sandbox-receipts.ndjson, and the
115
+ * file names each by marker and digest.
116
+ */
117
+ export async function withPublicSandboxIds(root, value) {
118
+ return publicSandboxView(value, await readRunSandboxIds(root));
119
+ }
120
+ /**
121
+ * The bytes of `file` with each raw id replaced by its label: the same buffer when none occurs, or
122
+ * when the bytes are not the plain text verify and export read as text.
123
+ */
124
+ export function scrubSandboxIdBytes(file, bytes, ids) {
125
+ if (!ids.some((id) => id.length > 0 && bytes.includes(id)))
126
+ return bytes;
127
+ const decoded = readPlainText(bytes);
128
+ if (!decoded.ok)
129
+ return bytes;
130
+ const label = /\.ya?ml$/i.test(file) ? yamlSandboxIdLabel : sandboxIdLabel;
131
+ return Buffer.from(scrubSandboxIds(decoded.text, ids, label));
132
+ }
133
+ /** The run files the sweep rewrites: the text formats export copies, and the Observer page. */
134
+ const TEXT_FILE = /\.(?:json|ndjson|jsonl|md|txt|log|yaml|yml|csv|html)$/i;
135
+ /** Enough for any run directory; a sweep stops there rather than hold up the run's finish. */
136
+ const MAX_SWEPT_ENTRIES = 10_000;
137
+ /**
138
+ * Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
139
+ * receipts, with the label in place of the id. Writers such as a participant's actor.json can
140
+ * quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
141
+ * Best effort: verify grades a file this misses.
142
+ */
143
+ export async function scrubRunSandboxIds(paths) {
144
+ const ids = await readRunSandboxIds(paths);
145
+ if (ids.length === 0)
146
+ return;
147
+ const valid = await validatePreparedRunRootIdentity(paths).then(() => true, () => false);
148
+ if (!valid)
149
+ return;
150
+ const scrub = async (file) => {
151
+ const bytes = await readContainedRegularFile(paths, file);
152
+ if (bytes === null)
153
+ return;
154
+ const scrubbed = scrubSandboxIdBytes(file, bytes, ids);
155
+ if (scrubbed !== bytes)
156
+ await writeContainedOutputFile(paths, file, scrubbed);
157
+ };
158
+ let seen = 0;
159
+ const walk = async (relative) => {
160
+ const directory = path.join(paths.physicalRunRoot, relative);
161
+ // A directory replaced by a link after it was listed resolves elsewhere and is not walked.
162
+ if ((await realpath(directory).catch(() => null)) !== directory)
163
+ return;
164
+ const listing = await opendir(directory).catch(() => null);
165
+ if (listing === null)
166
+ return;
167
+ for await (const entry of listing) {
168
+ // Returning from the loop closes the listing.
169
+ if (++seen > MAX_SWEPT_ENTRIES)
170
+ return;
171
+ const file = relative ? `${relative}/${entry.name}` : entry.name;
172
+ if (entry.isDirectory())
173
+ await walk(file);
174
+ else if (entry.isFile() && file !== SANDBOX_RECEIPTS_ARTIFACT && TEXT_FILE.test(file))
175
+ // A file that cannot be scrubbed stays as written; verify names it.
176
+ await scrub(file).catch(() => { });
177
+ }
178
+ };
179
+ await walk("").catch(() => {
180
+ // A directory that changed while it was listed ends the sweep; verify names what it missed.
181
+ });
182
+ }
183
+ /**
184
+ * A run's result as it may be returned or printed: the same object when it names no sandbox,
185
+ * otherwise its public view, scrubbed of the ids the run's receipts journal.
186
+ */
187
+ export async function publicRunResult(result, cwd) {
188
+ let ids = [];
189
+ if (typeof result.runId === "string" && isSafeRunIdSegment(result.runId)) {
190
+ try {
191
+ ids = await readRunSandboxIds(await bindExistingRunArtifactPaths(cwd, result.runId));
192
+ }
193
+ catch {
194
+ // A run that never created its directory has no receipts; the keys are still redacted.
195
+ }
196
+ }
197
+ return publicSandboxView(result, ids);
198
+ }
@@ -20,6 +20,8 @@ export interface SandboxReceipt {
20
20
  export interface ParsedSandboxReceipt extends Omit<SandboxReceipt, "provider"> {
21
21
  provider: string;
22
22
  }
23
+ /** The ids this process has receipted for `root`, whether or not the journal write landed. */
24
+ export declare function appendedSandboxIds(root: PreparedOutputRoot): string[];
23
25
  /**
24
26
  * Append one receipt. Best-effort by design: the receipt exists to protect the run, so a failed
25
27
  * receipt write must never fail the participant. The only cost of a miss is that `reclaim` cannot see
@@ -27,5 +29,19 @@ export interface ParsedSandboxReceipt extends Omit<SandboxReceipt, "provider"> {
27
29
  * artifact write uses; append (not atomic-replace) keeps racing participants' receipts intact.
28
30
  */
29
31
  export declare function appendSandboxReceipt(root: PreparedOutputRoot, receipt: SandboxReceipt): Promise<void>;
32
+ /** Append one owner line; false when the write failed, which never fails the create. */
33
+ export declare function appendSandboxOwner(root: PreparedOutputRoot, owner: SandboxOwnerTags): Promise<boolean>;
34
+ /** The owner tags every sandbox carries; sandboxOwnerTags in src/run/sandbox-creates.ts. */
35
+ export type SandboxOwnerTags = {
36
+ tool: "humanish";
37
+ runId: string;
38
+ runKey: string;
39
+ };
40
+ /**
41
+ * The owner tag sets a journal's owner lines record for the run named `runId`. Only a complete
42
+ * tuple counts: provider e2b, `tool: humanish`, this run's id and a well-formed key, and nothing
43
+ * else. A partial or foreign line would widen reclaim's search past this run, so it is skipped.
44
+ */
45
+ export declare function parseSandboxOwners(text: string, runId: string): SandboxOwnerTags[];
30
46
  /** Parse a receipts file leniently: a torn final line (crash mid-append) drops, valid lines keep. */
31
47
  export declare function parseSandboxReceipts(text: string): ParsedSandboxReceipt[];
@@ -4,6 +4,23 @@
4
4
  import { appendFile } from "node:fs/promises";
5
5
  import { prepareContainedOutputFile } from "./contained-output.js";
6
6
  export const SANDBOX_RECEIPTS_ARTIFACT = "sandbox-receipts.ndjson";
7
+ /**
8
+ * The ids this process has receipted for each run directory, kept even when the append fails, so
9
+ * the run's other records can still be scrubbed of them. Keyed by the directory's identity, so a
10
+ * run created later at a deleted run's path starts empty, and only the latest runs are kept.
11
+ */
12
+ const appended = new Map();
13
+ const MAX_REMEMBERED_RUNS = 64;
14
+ function rootKey(root) {
15
+ const [where, { dev, ino, birthtimeNs }] = "physicalRunRoot" in root
16
+ ? [root.physicalRunRoot, root.runRootIdentity]
17
+ : [root.physicalPath, root.identity];
18
+ return `${where}\0${dev}:${ino}:${birthtimeNs}`;
19
+ }
20
+ /** The ids this process has receipted for `root`, whether or not the journal write landed. */
21
+ export function appendedSandboxIds(root) {
22
+ return [...(appended.get(rootKey(root)) ?? [])];
23
+ }
7
24
  /**
8
25
  * Append one receipt. Best-effort by design: the receipt exists to protect the run, so a failed
9
26
  * receipt write must never fail the participant. The only cost of a miss is that `reclaim` cannot see
@@ -11,6 +28,15 @@ export const SANDBOX_RECEIPTS_ARTIFACT = "sandbox-receipts.ndjson";
11
28
  * artifact write uses; append (not atomic-replace) keeps racing participants' receipts intact.
12
29
  */
13
30
  export async function appendSandboxReceipt(root, receipt) {
31
+ const key = rootKey(root);
32
+ const ids = appended.get(key) ?? new Set();
33
+ appended.delete(key);
34
+ appended.set(key, ids.add(receipt.sandboxId));
35
+ for (const oldest of appended.keys()) {
36
+ if (appended.size <= MAX_REMEMBERED_RUNS)
37
+ break;
38
+ appended.delete(oldest);
39
+ }
14
40
  try {
15
41
  const filePath = await prepareContainedOutputFile(root, SANDBOX_RECEIPTS_ARTIFACT);
16
42
  await appendFile(filePath, `${JSON.stringify(receipt)}\n`, "utf8");
@@ -19,6 +45,50 @@ export async function appendSandboxReceipt(root, receipt) {
19
45
  // Swallowed on purpose; see the contract above.
20
46
  }
21
47
  }
48
+ /** Append one owner line; false when the write failed, which never fails the create. */
49
+ export async function appendSandboxOwner(root, owner) {
50
+ try {
51
+ const line = { at: new Date().toISOString(), provider: "e2b", owner };
52
+ const filePath = await prepareContainedOutputFile(root, SANDBOX_RECEIPTS_ARTIFACT);
53
+ // The leading newline ends a line an earlier append left torn, so this record parses.
54
+ await appendFile(filePath, `\n${JSON.stringify(line)}\n`, "utf8");
55
+ return true;
56
+ }
57
+ catch {
58
+ // Never fails the create. Without the line, reclaim reports the run's sandboxes as unknown,
59
+ // never clean, so the next create tries again.
60
+ return false;
61
+ }
62
+ }
63
+ const RUN_KEY = /^[0-9a-f]{16}$/;
64
+ /**
65
+ * The owner tag sets a journal's owner lines record for the run named `runId`. Only a complete
66
+ * tuple counts: provider e2b, `tool: humanish`, this run's id and a well-formed key, and nothing
67
+ * else. A partial or foreign line would widen reclaim's search past this run, so it is skipped.
68
+ */
69
+ export function parseSandboxOwners(text, runId) {
70
+ const owners = new Map();
71
+ for (const line of text.split("\n")) {
72
+ const trimmed = line.trim();
73
+ if (!trimmed)
74
+ continue;
75
+ try {
76
+ const parsed = JSON.parse(trimmed);
77
+ const owner = parsed.owner;
78
+ if (parsed.provider !== "e2b" || owner === null || typeof owner !== "object")
79
+ continue;
80
+ if (Object.keys(owner).length !== 3 || owner.tool !== "humanish" || owner.runId !== runId)
81
+ continue;
82
+ if (typeof owner.runKey !== "string" || !RUN_KEY.test(owner.runKey))
83
+ continue;
84
+ owners.set(owner.runKey, { tool: "humanish", runId, runKey: owner.runKey });
85
+ }
86
+ catch {
87
+ // A torn line: skip it.
88
+ }
89
+ }
90
+ return [...owners.values()];
91
+ }
22
92
  /** Parse a receipts file leniently: a torn final line (crash mid-append) drops, valid lines keep. */
23
93
  export function parseSandboxReceipts(text) {
24
94
  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,14 +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
15
  export const RUN_STATUS_SCHEMA = "humanish.run-status.v1";
16
16
  /** The file, relative to the run directory. */
17
17
  export const RUN_STATUS_FILE = "status.json";
@@ -45,6 +45,9 @@ export function runStatusOutcome(bundle) {
45
45
  ...(typeof bundle.cost?.fullyEstimated === "boolean"
46
46
  ? { estimatedCostComplete: bundle.cost.fullyEstimated }
47
47
  : {}),
48
+ ...(bundle.outcome?.state === "finished"
49
+ ? { ok: bundle.outcome.ok, execution: bundle.outcome.execution }
50
+ : {}),
48
51
  };
49
52
  }
50
53
  /**
@@ -102,26 +105,10 @@ export function beginRunStatus(runPaths, options) {
102
105
  };
103
106
  await write(finishedRecord);
104
107
  },
105
- async settle(result) {
108
+ async restate(outcome) {
106
109
  if (finishedRecord === undefined)
107
110
  return;
108
- // The record is public-safe by construction, so each message passes the shape redaction
109
- // again even though the routes scrubbed it.
110
- const redacted = (failure) => ({
111
- kind: failure.kind,
112
- message: redactText(failure.message),
113
- });
114
- const execution = {
115
- succeeded: result.execution.succeeded,
116
- failures: result.execution.failures.map(redacted),
117
- ...(result.execution.warnings === undefined
118
- ? {}
119
- : { warnings: result.execution.warnings.map(redacted) }),
120
- };
121
- finishedRecord = {
122
- ...finishedRecord,
123
- outcome: { ...finishedRecord.outcome, ok: result.ok, execution },
124
- };
111
+ finishedRecord = { ...finishedRecord, outcome };
125
112
  await write(finishedRecord);
126
113
  },
127
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 {};