humanish 0.109.0 → 0.109.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.
- package/CHANGELOG.md +9 -0
- package/dist/analysis/automatic-config.d.ts +1 -1
- package/dist/cli/commands/migrate.js +15 -4
- package/dist/cli/commands/runs.js +6 -2
- package/dist/evidence/redaction.d.ts +11 -0
- package/dist/evidence/redaction.js +43 -0
- package/dist/feedback/export-bundle.js +5 -4
- package/dist/routes/computer-use/in-process-desktop.js +1 -1
- package/dist/routes/computer-use/plan.js +3 -3
- package/dist/routes/computer-use/setup.js +1 -1
- package/dist/study/migrate.d.ts +7 -2
- package/dist/study/migrate.js +60 -4
- package/dist/study/run-study-events.js +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/docs/contracts/run-bundle.md +5 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,15 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.109.1: Current option names in messages, reclaim --env-file (2026-10-03)
|
|
12
|
+
|
|
13
|
+
humanish 0.109.1 is a patch. Library refusals and warnings name `RunStudyOptions`, which 0.109.0
|
|
14
|
+
exports, where they named the removed `RunLabOptions`. `humanish export --format bundle` no longer
|
|
15
|
+
copies sandbox ids into its redacted workspace. `humanish reclaim` takes `--env-file`, and
|
|
16
|
+
`humanish migrate` removes a `labs/` directory that its moves emptied.
|
|
17
|
+
|
|
18
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.109.1)
|
|
19
|
+
|
|
11
20
|
## 0.109.0: Lab names removed, v2 study files refused (2026-10-03)
|
|
12
21
|
|
|
13
22
|
humanish 0.109.0 removes what 0.108.0 deprecated. A `humanish.lab.v2` file, or any study file in a
|
|
@@ -7,7 +7,7 @@ interface StudyAnalysisSettings {
|
|
|
7
7
|
question?: string;
|
|
8
8
|
timeoutMs?: number;
|
|
9
9
|
}
|
|
10
|
-
/** The study's review.analysis: a provider selection for a separate review after a live
|
|
10
|
+
/** The study's review.analysis: a provider selection for a separate review after a live run. */
|
|
11
11
|
export type StudyAnalysis = StudyAnalysisSettings & ({
|
|
12
12
|
provider?: "openai";
|
|
13
13
|
maxCostUsd: number;
|
|
@@ -4,8 +4,11 @@ function formatValue(value) {
|
|
|
4
4
|
const text = JSON.stringify(value);
|
|
5
5
|
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Each file's source, destination, moved keys and dropped keys, and each labs/ directory the moves
|
|
9
|
+
* empty, printed before any write.
|
|
10
|
+
*/
|
|
11
|
+
function formatMigratePlan(files, emptied) {
|
|
9
12
|
const lines = [];
|
|
10
13
|
for (const file of files) {
|
|
11
14
|
if (file.action === "skip") {
|
|
@@ -22,6 +25,9 @@ function formatMigratePlan(files) {
|
|
|
22
25
|
lines.push(` its comment: ${key.comments.replace(/\n/g, " ")}`);
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
for (const directory of emptied) {
|
|
29
|
+
lines.push(`${directory}/: removed after the move, since nothing else is in it`);
|
|
30
|
+
}
|
|
25
31
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "No study files to convert.\n";
|
|
26
32
|
}
|
|
27
33
|
function formatMigrateOutcome(result) {
|
|
@@ -33,7 +39,12 @@ function formatMigrateOutcome(result) {
|
|
|
33
39
|
const files = written === 1 ? "1 study file" : `${written} study files`;
|
|
34
40
|
if (result.dryRun)
|
|
35
41
|
return `Dry run: nothing was written. ${files} would change.\n`;
|
|
36
|
-
|
|
42
|
+
if (written === 0)
|
|
43
|
+
return "No v2 study files to convert.\n";
|
|
44
|
+
const removed = result.removedDirectories ?? [];
|
|
45
|
+
return removed.length === 0
|
|
46
|
+
? `Converted ${files}.\n`
|
|
47
|
+
: `Converted ${files}. Removed ${removed.map((directory) => `${directory}/`).join(", ")}, which the move emptied.\n`;
|
|
37
48
|
}
|
|
38
49
|
/** `humanish migrate`. */
|
|
39
50
|
export function registerMigrateCommand(parent, io) {
|
|
@@ -57,7 +68,7 @@ export function registerMigrateCommand(parent, io) {
|
|
|
57
68
|
dryRun: options.dryRun === true,
|
|
58
69
|
// The plan is printed before anything is written; with --json it goes to stderr, so stdout
|
|
59
70
|
// stays one JSON document.
|
|
60
|
-
onPlan: (files) => (json ? io.writeErr : io.writeOut)(formatMigratePlan(files)),
|
|
71
|
+
onPlan: (files, emptied) => (json ? io.writeErr : io.writeOut)(formatMigratePlan(files, emptied)),
|
|
61
72
|
});
|
|
62
73
|
writeResult(command, io, result, formatMigrateOutcome);
|
|
63
74
|
io.setExitCode(result.ok ? 0 : 2);
|
|
@@ -7,7 +7,7 @@ import { cleanupRun, listRuns, readReview } from "../../run/stored-runs.js";
|
|
|
7
7
|
import { verifyRun } from "../../verify/verify.js";
|
|
8
8
|
import { reclaimPreflightSandboxes, reclaimRunSandboxes, } from "../../run/reclaim.js";
|
|
9
9
|
import { addRunOptions, handleRun } from "./run-command.js";
|
|
10
|
-
import { CWD_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
|
|
10
|
+
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
|
|
11
11
|
import { plural } from "../../run/text.js";
|
|
12
12
|
export function registerRunCommand(parent, io) {
|
|
13
13
|
addRunOptions(parent
|
|
@@ -165,13 +165,17 @@ export function registerRuntimeCommands(parent, io) {
|
|
|
165
165
|
export function registerReclaimCommand(parent, io) {
|
|
166
166
|
parent
|
|
167
167
|
.command("reclaim")
|
|
168
|
-
.description("Kill an interrupted run's sandboxes by the exact ids journaled in its sandbox-receipts.ndjson; never enumerates the E2B account. Needs E2B_API_KEY in the environment.")
|
|
168
|
+
.description("Kill an interrupted run's sandboxes by the exact ids journaled in its sandbox-receipts.ndjson; never enumerates the E2B account. Needs E2B_API_KEY in the environment or in --env-file.")
|
|
169
169
|
.summary("Stop an interrupted run's leftover sandboxes.")
|
|
170
170
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
171
171
|
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
172
172
|
.addOption(new Option("--preflight", "Reclaim sandboxes left by interrupted `humanish study check` probes (journaled in .humanish/preflight) instead of a run's.").conflicts("run"))
|
|
173
|
+
.option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
|
|
173
174
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
174
175
|
.action(async (options, command) => {
|
|
176
|
+
// The kill calls read E2B_API_KEY from the environment: the env file, then discovered keys.
|
|
177
|
+
if (!(await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io })))
|
|
178
|
+
return;
|
|
175
179
|
const result = options.preflight
|
|
176
180
|
? await reclaimPreflightSandboxes(options.cwd)
|
|
177
181
|
: await reclaimRunSandboxes(options.cwd, options.run);
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
export declare function sensitivePatterns(): readonly RegExp[];
|
|
3
3
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
4
4
|
export declare function containsSensitive(text: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
7
|
+
* passes only this value at a sandbox-id key.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
10
|
+
/**
|
|
11
|
+
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID: strings at `sandboxId` and
|
|
12
|
+
* `subjectSandboxId`, and the `id` of each `providerResources` entry. A run keeps raw ids as local
|
|
13
|
+
* evidence for cleanup; a copy made to be shared names no sandbox.
|
|
14
|
+
*/
|
|
15
|
+
export declare function redactSandboxIds(value: unknown): unknown;
|
|
5
16
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
6
17
|
export declare function redactText(text: string): string;
|
|
7
18
|
/** Redact every sensitive match (secrets and paths) to a single [REDACTED_SECRET] label. */
|
|
@@ -55,6 +55,49 @@ export function containsSensitive(text) {
|
|
|
55
55
|
function redactLocalPaths(text, label) {
|
|
56
56
|
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + (match.match(/\\+$/)?.[0] ?? "")), text);
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
60
|
+
* passes only this value at a sandbox-id key.
|
|
61
|
+
*/
|
|
62
|
+
export const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
63
|
+
/** The keys run writers put a sandbox id under; `providerResources` entries hold one as `id`. */
|
|
64
|
+
const SANDBOX_ID_KEYS = new Set(["sandboxId", "subjectSandboxId"]);
|
|
65
|
+
/**
|
|
66
|
+
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID: strings at `sandboxId` and
|
|
67
|
+
* `subjectSandboxId`, and the `id` of each `providerResources` entry. A run keeps raw ids as local
|
|
68
|
+
* evidence for cleanup; a copy made to be shared names no sandbox.
|
|
69
|
+
*/
|
|
70
|
+
export function redactSandboxIds(value) {
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
const items = value.map(redactSandboxIds);
|
|
73
|
+
return items.every((item, index) => item === value[index]) ? value : items;
|
|
74
|
+
}
|
|
75
|
+
if (value === null || typeof value !== "object")
|
|
76
|
+
return value;
|
|
77
|
+
let changed = false;
|
|
78
|
+
// fromEntries keeps a key such as __proto__ an own property, as JSON.parse made it.
|
|
79
|
+
const entries = Object.entries(value).map(([key, child]) => {
|
|
80
|
+
let next;
|
|
81
|
+
if (SANDBOX_ID_KEYS.has(key) && typeof child === "string")
|
|
82
|
+
next = REDACTED_SANDBOX_ID;
|
|
83
|
+
else if (key === "providerResources" && Array.isArray(child))
|
|
84
|
+
next = child.map((resource) => resource !== null &&
|
|
85
|
+
typeof resource === "object" &&
|
|
86
|
+
!Array.isArray(resource) &&
|
|
87
|
+
"id" in resource
|
|
88
|
+
? Object.fromEntries(Object.entries(redactSandboxIds(resource)).map(([field, entry]) => [
|
|
89
|
+
field,
|
|
90
|
+
field === "id" ? REDACTED_SANDBOX_ID : entry,
|
|
91
|
+
]))
|
|
92
|
+
: redactSandboxIds(resource));
|
|
93
|
+
else
|
|
94
|
+
next = redactSandboxIds(child);
|
|
95
|
+
if (next !== child && JSON.stringify(next) !== JSON.stringify(child))
|
|
96
|
+
changed = true;
|
|
97
|
+
return [key, next];
|
|
98
|
+
});
|
|
99
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
100
|
+
}
|
|
58
101
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
59
102
|
export function redactText(text) {
|
|
60
103
|
const withoutSecrets = SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, "[REDACTED_SECRET]"), text);
|
|
@@ -8,7 +8,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
8
8
|
import { ACTOR_TRACE_SCHEMA } from "../actors/contract.js";
|
|
9
9
|
import { renderObserver } from "../observer/render.js";
|
|
10
10
|
import { buildObserverData } from "../observer/data.js";
|
|
11
|
-
import { containsSensitive, redactScreenshot, redactText } from "../evidence/redaction.js";
|
|
11
|
+
import { containsSensitive, redactSandboxIds, redactScreenshot, redactText, } from "../evidence/redaction.js";
|
|
12
12
|
import { decodeEscapes } from "../evidence/encoded-text.js";
|
|
13
13
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
14
14
|
import { streamScreenshotPaths } from "../verify/artifacts.js";
|
|
@@ -224,7 +224,8 @@ function transformText(file, imagePaths) {
|
|
|
224
224
|
const parsed = JSON.parse(text);
|
|
225
225
|
// Parsing also resolves escape sequences in string values before inspection.
|
|
226
226
|
assertNoInlineRaster(JSON.stringify(parsed));
|
|
227
|
-
|
|
227
|
+
// The copy names no sandbox: an id is local operational evidence, and the copy is shared.
|
|
228
|
+
const transformed = redactSandboxIds(updateScreenshotDeclarations(parsed, imagePaths));
|
|
228
229
|
if (JSON.stringify(transformed) === JSON.stringify(parsed))
|
|
229
230
|
return file.bytes;
|
|
230
231
|
assertRewritableNumbers(text);
|
|
@@ -237,7 +238,7 @@ function transformText(file, imagePaths) {
|
|
|
237
238
|
return line;
|
|
238
239
|
const parsed = JSON.parse(line);
|
|
239
240
|
assertNoInlineRaster(JSON.stringify(parsed));
|
|
240
|
-
const updated = JSON.stringify(updateScreenshotDeclarations(parsed, imagePaths));
|
|
241
|
+
const updated = JSON.stringify(redactSandboxIds(updateScreenshotDeclarations(parsed, imagePaths)));
|
|
241
242
|
if (updated === JSON.stringify(parsed))
|
|
242
243
|
return line;
|
|
243
244
|
assertRewritableNumbers(line);
|
|
@@ -329,7 +330,7 @@ async function writeDerivative(source, stagePaths, bundle) {
|
|
|
329
330
|
bytes = transformText(file, imagePaths);
|
|
330
331
|
if (file.path === "run.json") {
|
|
331
332
|
assertRewritableNumbers(file.bytes.toString("utf8"));
|
|
332
|
-
const transformed = updateScreenshotDeclarations(bundle, imagePaths);
|
|
333
|
+
const transformed = redactSandboxIds(updateScreenshotDeclarations(bundle, imagePaths));
|
|
333
334
|
transformed.redaction = {
|
|
334
335
|
status: "passed",
|
|
335
336
|
notes: `${bundle.redaction.notes} Exported copy: screenshots were blurred during export; the original capture is retained separately. See derivation.json.`,
|
|
@@ -20,7 +20,7 @@ export function createInProcessDesktop(deps) {
|
|
|
20
20
|
throw new Error("The participant desktop must be prepared and may only be opened once, before finalization.");
|
|
21
21
|
opened = true;
|
|
22
22
|
if (deps.inProcessExecutor === undefined)
|
|
23
|
-
throw new Error("The in-process route needs
|
|
23
|
+
throw new Error("The in-process route needs RunStudyOptions.inProcess.");
|
|
24
24
|
return { executor: await deps.inProcessExecutor(deps.appUrl) };
|
|
25
25
|
},
|
|
26
26
|
finalize() {
|
|
@@ -159,7 +159,7 @@ function driverReason(driving, { localAppSubject, inProcessRoute }) {
|
|
|
159
159
|
if (driving.inProcess && !driving.createProvider)
|
|
160
160
|
return {
|
|
161
161
|
code: "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER",
|
|
162
|
-
message: "
|
|
162
|
+
message: "RunStudyOptions.inProcess needs RunStudyOptions.createProvider: an in-process executor returns no screenshot, so it needs a provider that does not read images. The default OpenAI computer-use provider reads screenshots and would stop the session.",
|
|
163
163
|
};
|
|
164
164
|
// There is no built-in in-process driver for a local app.
|
|
165
165
|
if (localAppSubject && !inProcessRoute)
|
|
@@ -233,7 +233,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
233
233
|
const source = config.subject.source;
|
|
234
234
|
if (driving.inProcess &&
|
|
235
235
|
(source === "clone" || source === "local-tree" || source === "desktop-cli"))
|
|
236
|
-
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", `
|
|
236
|
+
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", `RunStudyOptions.inProcess drives subject.appUrl in this process, and a ${source} subject needs the hosted desktop the in-process route never creates. Use an app-url or local-app subject with inProcess, or remove inProcess to run on a hosted desktop.`, actor);
|
|
237
237
|
// The parser's rule; without a product the desktop study fails later.
|
|
238
238
|
const productReason = desktopCliProductReason(config);
|
|
239
239
|
if (productReason)
|
|
@@ -243,7 +243,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
243
243
|
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", `Computer-use fan-out is capped at ${MAX_COMPUTER_USE_PARTICIPANTS} participants (resolved ${participants.length}); N concurrent paid desktops is real spend.`, actor);
|
|
244
244
|
const [first, ...rest] = participants;
|
|
245
245
|
if (driving.inProcess && rest.length > 0)
|
|
246
|
-
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", "Fan-out to more than one participant is not supported on the in-process route (
|
|
246
|
+
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", "Fan-out to more than one participant is not supported on the in-process route (RunStudyOptions.inProcess): fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Run a single in-process participant, or fan out on the E2B route.", actor);
|
|
247
247
|
if (first === undefined)
|
|
248
248
|
throw new Error("computerUseParticipants returned no participant");
|
|
249
249
|
const appUrl = config.subject.appUrl ?? "";
|
|
@@ -89,7 +89,7 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
89
89
|
ok: false,
|
|
90
90
|
result: fail(...args),
|
|
91
91
|
});
|
|
92
|
-
// runStudyWith's
|
|
92
|
+
// runStudyWith's `localVm` supplies the desktop of a local-target app-url study; a direct route call or
|
|
93
93
|
// an in-process executor on the same study has none. It reads the declared source, as the planner
|
|
94
94
|
// did: a library config the parser never saw can plan to an app-url subject from another source.
|
|
95
95
|
if (config.subject.source === "app-url" &&
|
package/dist/study/migrate.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface MigrateResult {
|
|
|
21
21
|
readonly files: readonly MigrateFile[];
|
|
22
22
|
/** Files left behind after a clean-up step found them changed: sources, backups or temp files. */
|
|
23
23
|
readonly unresolved?: readonly string[];
|
|
24
|
+
/** The labs/ directories the moves emptied and migrate removed, or with `--dry-run` would remove. */
|
|
25
|
+
readonly removedDirectories?: readonly string[];
|
|
24
26
|
readonly error?: {
|
|
25
27
|
readonly code: "HUMANISH_MIGRATE_REFUSED" | "HUMANISH_MIGRATE_FAILED";
|
|
26
28
|
readonly phase: "plan" | "stage" | "commit" | "clean-up";
|
|
@@ -34,8 +36,11 @@ export interface MigrateOptions {
|
|
|
34
36
|
/** Files to convert. Without any, the six study directories are scanned. */
|
|
35
37
|
readonly paths?: readonly string[];
|
|
36
38
|
readonly dryRun?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Called with the plan before anything is written, so a caller can print every destination and
|
|
41
|
+
* each labs/ directory the moves empty, which migrate then removes.
|
|
42
|
+
*/
|
|
43
|
+
readonly onPlan?: (files: readonly MigrateFile[], emptied: readonly string[]) => void;
|
|
39
44
|
/** Replaces `fs.link`. Tests use it to stand in for a filesystem without hard links. */
|
|
40
45
|
readonly link?: (from: string, to: string) => Promise<void>;
|
|
41
46
|
/** Called between stage and commit, and between commit and clean-up. Tests change files there. */
|
package/dist/study/migrate.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// name, so no file this run did not write is ever overwritten. A failure undoes the commits.
|
|
9
9
|
// 4. Clean up: delete each moved source and each rewrite's original, once checked (owned-files.ts).
|
|
10
10
|
// A file that changed since the plan is kept and listed; a rewrite's original as <name>.v2.bak.
|
|
11
|
+
// Then a labs/ directory whose every entry moved is removed with a plain rmdir, which fails, and
|
|
12
|
+
// keeps the directory, when anything else is in it by then.
|
|
11
13
|
import { randomUUID } from "node:crypto";
|
|
12
14
|
import { link, lstat, readdir, readFile, realpath, rename, rmdir } from "node:fs/promises";
|
|
13
15
|
import path from "node:path";
|
|
@@ -317,6 +319,46 @@ async function undoCommits(root, writes, linkFile) {
|
|
|
317
319
|
}
|
|
318
320
|
return unrestored;
|
|
319
321
|
}
|
|
322
|
+
// Each labs/ directory whose every entry, a dotfile or a subdirectory included, is a file this run
|
|
323
|
+
// moves out. Only these are removed after the move.
|
|
324
|
+
async function emptiedRetiredDirectories(root, writes) {
|
|
325
|
+
const moving = new Map();
|
|
326
|
+
for (const write of writes) {
|
|
327
|
+
if (write.file.action !== "move")
|
|
328
|
+
continue;
|
|
329
|
+
const directory = path.dirname(write.sourcePath);
|
|
330
|
+
const relative = path.relative(root, directory);
|
|
331
|
+
const retired = STUDY_DIRECTORIES.some((entry) => entry.family === "labs" && entry.relativeDir === relative);
|
|
332
|
+
if (!retired)
|
|
333
|
+
continue;
|
|
334
|
+
const entry = moving.get(directory) ?? { write, names: new Set() };
|
|
335
|
+
entry.names.add(path.basename(write.sourcePath));
|
|
336
|
+
moving.set(directory, entry);
|
|
337
|
+
}
|
|
338
|
+
const emptied = [];
|
|
339
|
+
for (const [directory, { write, names }] of moving) {
|
|
340
|
+
const entries = await readdir(directory).catch(() => undefined);
|
|
341
|
+
if (entries !== undefined && entries.every((name) => names.has(name)))
|
|
342
|
+
emptied.push(write);
|
|
343
|
+
}
|
|
344
|
+
return emptied;
|
|
345
|
+
}
|
|
346
|
+
// Remove each emptied labs/ directory, if it is still the one the plan read. rmdir is not
|
|
347
|
+
// recursive, so a file that appeared in it since the plan keeps it. Returns the removed paths.
|
|
348
|
+
async function removeEmptiedDirectories(root, emptied) {
|
|
349
|
+
const removed = [];
|
|
350
|
+
for (const write of emptied) {
|
|
351
|
+
try {
|
|
352
|
+
await assertDirectory(write.sourceDirectory, root);
|
|
353
|
+
await rmdir(path.dirname(write.sourcePath));
|
|
354
|
+
removed.push(path.relative(root, path.dirname(write.sourcePath)));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Not empty, or not the directory the plan read: it stays.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return removed;
|
|
361
|
+
}
|
|
320
362
|
// Delete each moved source and each rewrite's original, but only once the v3 file is in place as
|
|
321
363
|
// written, and only if the old file is still the one the plan read (removeChecked). Anything kept
|
|
322
364
|
// is listed; a rewrite's original is kept as <name>.v2.bak.
|
|
@@ -356,9 +398,17 @@ export async function migrateStudies(options) {
|
|
|
356
398
|
const linkFile = options.link ?? link;
|
|
357
399
|
try {
|
|
358
400
|
({ files, writes } = await planWrites(root, options));
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
401
|
+
const emptied = await emptiedRetiredDirectories(root, writes);
|
|
402
|
+
const emptiedPaths = emptied.map((write) => path.relative(root, path.dirname(write.sourcePath)));
|
|
403
|
+
options.onPlan?.(files, emptiedPaths);
|
|
404
|
+
if (options.dryRun === true || writes.length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
...base,
|
|
407
|
+
ok: true,
|
|
408
|
+
files,
|
|
409
|
+
...(emptiedPaths.length === 0 ? {} : { removedDirectories: emptiedPaths }),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
362
412
|
try {
|
|
363
413
|
await stage(root, writes, created);
|
|
364
414
|
options.onPhase?.("stage");
|
|
@@ -386,7 +436,13 @@ export async function migrateStudies(options) {
|
|
|
386
436
|
},
|
|
387
437
|
};
|
|
388
438
|
}
|
|
389
|
-
|
|
439
|
+
const removed = await removeEmptiedDirectories(root, emptied);
|
|
440
|
+
return {
|
|
441
|
+
...base,
|
|
442
|
+
ok: true,
|
|
443
|
+
files,
|
|
444
|
+
...(removed.length === 0 ? {} : { removedDirectories: removed }),
|
|
445
|
+
};
|
|
390
446
|
}
|
|
391
447
|
catch (error) {
|
|
392
448
|
const failure = error instanceof MigrateError
|
|
@@ -43,7 +43,7 @@ export function studyEventEmitter(onEvent, warnings, secretValues) {
|
|
|
43
43
|
catch {
|
|
44
44
|
detail = "the thrown value has no message";
|
|
45
45
|
}
|
|
46
|
-
warnings.push(`
|
|
46
|
+
warnings.push(`RunStudyOptions.onEvent failed on ${type}: ${detail}`);
|
|
47
47
|
};
|
|
48
48
|
try {
|
|
49
49
|
const returned = onEvent(event);
|
|
@@ -11,7 +11,7 @@ import { studyResultIdentity } from "../run/study-result.js";
|
|
|
11
11
|
const unsupported = (option, route, reason) => ({
|
|
12
12
|
ok: false,
|
|
13
13
|
code: "HUMANISH_STUDY_OPTION_UNSUPPORTED",
|
|
14
|
-
message: `
|
|
14
|
+
message: `RunStudyOptions.${option} is not supported on the ${route} route: ${reason}`,
|
|
15
15
|
});
|
|
16
16
|
// Fields RunStudyOptions no longer has, and where each one's job went.
|
|
17
17
|
const REMOVED_OPTIONS = {
|
|
@@ -32,7 +32,7 @@ export function removedOptionRefusal(options) {
|
|
|
32
32
|
return {
|
|
33
33
|
ok: false,
|
|
34
34
|
code: "HUMANISH_STUDY_OPTION_UNSUPPORTED",
|
|
35
|
-
message: '
|
|
35
|
+
message: '`rerun.laneIds` was removed in 0.107.0. Use `rerun.participantIds`. See docs/contracts/schemas.md, "Library options".',
|
|
36
36
|
};
|
|
37
37
|
const field = Object.keys(REMOVED_OPTIONS).find((key) => Reflect.get(options, key) !== undefined);
|
|
38
38
|
if (field === undefined)
|
|
@@ -40,7 +40,7 @@ export function removedOptionRefusal(options) {
|
|
|
40
40
|
return {
|
|
41
41
|
ok: false,
|
|
42
42
|
code: "HUMANISH_STUDY_OPTION_UNSUPPORTED",
|
|
43
|
-
message:
|
|
43
|
+
message: `\`${field}\` was removed. ${REMOVED_OPTIONS[field]} See docs/contracts/schemas.md, "Library options".`,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
/** Why the route cannot honor an option it was given, or undefined when it can. */
|
|
@@ -633,6 +633,11 @@ The receipt describes the transformation; it does not attest that a participant'
|
|
|
633
633
|
finding is true. Its own bytes are not included in its hash inventory. Feedback
|
|
634
634
|
commands can subsequently generate new derivative-local artifacts.
|
|
635
635
|
|
|
636
|
+
The derivative names no sandbox. In every JSON and NDJSON file it copies, the
|
|
637
|
+
values at `sandboxId`, `subjectSandboxId` and each `providerResources[].id` read
|
|
638
|
+
`[redacted-sandbox-id]`; `sandbox-receipts.ndjson` is omitted. The source keeps
|
|
639
|
+
its raw ids, which cleanup and reclaim read.
|
|
640
|
+
|
|
636
641
|
PNG files that actor traces reference as stream screenshots are re-encoded as
|
|
637
642
|
blurred thumbnails. A PNG that nothing in `run.json` cites is omitted and
|
|
638
643
|
inventoried. Export refuses a run whose feedback candidates, adapter artifacts or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "humanish",
|
|
3
|
-
"version": "0.109.
|
|
3
|
+
"version": "0.109.1",
|
|
4
4
|
"description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"prose:check": "node scripts/check-code-prose.mjs",
|
|
92
92
|
"vocabulary:check": "tsx scripts/check-retired-words.ts",
|
|
93
93
|
"site-css:check": "node scripts/check-site-css.mjs",
|
|
94
|
+
"site:tour": "tsx scripts/site-tour.ts",
|
|
94
95
|
"site:hero:proof": "node scripts/site-hero-wrap-proof.mjs"
|
|
95
96
|
},
|
|
96
97
|
"dependencies": {
|