humanish 0.110.1 → 0.110.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -16
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +1 -1
- package/README.md +8 -7
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/local-agent/cli.d.ts +6 -0
- package/dist/actors/local-agent/cli.js +8 -0
- package/dist/actors/local-agent/readiness.js +2 -1
- package/dist/actors/registry.js +1 -1
- package/dist/analysis/automatic-completion.js +8 -0
- package/dist/cli/commands/observe.js +18 -4
- package/dist/cli/commands/run-signals.js +13 -4
- package/dist/cli/commands/runs.js +36 -13
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +7 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +4 -2
- package/dist/cli/commands/study-run.js +9 -19
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.js +9 -6
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +28 -6
- package/dist/cli/first-run-path.d.ts +27 -7
- package/dist/cli/first-run-path.js +112 -75
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +5 -2
- package/dist/cli/io.js +9 -16
- package/dist/cli/observer-follow.d.ts +24 -0
- package/dist/cli/observer-follow.js +48 -3
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +134 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.js +3 -2
- package/dist/observer/render.js +2 -1
- package/dist/observer/run-routes.js +8 -1
- package/dist/observer/serve.js +2 -1
- package/dist/routes/computer-use/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +6 -1
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +17 -78
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +10 -42
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/plan.js +5 -1
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +24 -63
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/bundle.js +2 -1
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- package/dist/routes/terminal/plan.js +8 -0
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +24 -63
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +2 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- package/dist/run/dry-run.js +2 -1
- package/dist/run/judge.js +2 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -0
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +34 -2
- package/dist/run/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/run.d.ts +6 -1
- package/dist/run/run.js +11 -2
- package/dist/run/sandbox-creates.js +5 -3
- package/dist/run/sandbox-receipts.d.ts +6 -5
- package/dist/run/sandbox-receipts.js +12 -7
- package/dist/run/status.d.ts +2 -0
- package/dist/run/status.js +5 -0
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -129
- package/dist/study/init.js +32 -35
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +13 -1
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- package/dist/substrates/e2b/sandbox.js +33 -15
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/tui/contract.js +5 -4
- package/dist/verify/liveness.js +2 -1
- package/dist/verify/verify.js +2 -1
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +26 -24
- package/docs/contracts/run-bundle.md +20 -4
- package/docs/contracts/schemas.md +6 -5
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +22 -18
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Every created sandbox id is journaled to the run dir
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
26
|
-
* receipt write must never fail the participant. The only cost of a
|
|
27
|
-
* this id and the TTL backstop covers it instead. Containment is
|
|
28
|
-
* artifact write uses; append (not atomic-replace) keeps racing
|
|
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,9 +43,11 @@ 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
|
}
|
|
48
53
|
/** Append one owner line; false when the write failed, which never fails the create. */
|
package/dist/run/status.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface RunStatusHandle {
|
|
|
80
80
|
* `running` or `finished` over it. When the run had finished it writes nothing, waits for the
|
|
81
81
|
* writes already queued and resolves false. */
|
|
82
82
|
interrupt(signal: RunInterruptSignal): Promise<boolean>;
|
|
83
|
+
/** True from the moment `interrupt` accepts a signal, before its write lands. */
|
|
84
|
+
readonly interrupted: boolean;
|
|
83
85
|
}
|
|
84
86
|
export interface BeginRunStatusOptions {
|
|
85
87
|
runId: string;
|
package/dist/run/status.js
CHANGED
|
@@ -69,6 +69,7 @@ export function beginRunStatus(runPaths, options) {
|
|
|
69
69
|
updatedAt: startedAt,
|
|
70
70
|
};
|
|
71
71
|
let finished = false;
|
|
72
|
+
let interrupted = false;
|
|
72
73
|
let finishedRecord;
|
|
73
74
|
let writing = Promise.resolve();
|
|
74
75
|
const write = (record) => {
|
|
@@ -119,11 +120,15 @@ export function beginRunStatus(runPaths, options) {
|
|
|
119
120
|
return false;
|
|
120
121
|
}
|
|
121
122
|
finished = true;
|
|
123
|
+
interrupted = true;
|
|
122
124
|
clearInterval(timer);
|
|
123
125
|
const completedAt = iso();
|
|
124
126
|
await write({ ...base, state: "interrupted", updatedAt: completedAt, completedAt, signal });
|
|
125
127
|
return true;
|
|
126
128
|
},
|
|
129
|
+
get interrupted() {
|
|
130
|
+
return interrupted;
|
|
131
|
+
},
|
|
127
132
|
};
|
|
128
133
|
return handle;
|
|
129
134
|
}
|
|
@@ -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
|
+
}
|
|
@@ -51,7 +51,7 @@ function thisRepoValidationReason(config) {
|
|
|
51
51
|
// desktop), and no public-target policy (it is always loopback; the loopback shape was already
|
|
52
52
|
// enforced in parseSubject). The actual "no inProcess executor supplied" case is inherently an
|
|
53
53
|
// engine-time decision (the parser cannot know whether a library caller will pass one), so
|
|
54
|
-
//
|
|
54
|
+
// planComputerUseStudy refuses it with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
|
|
55
55
|
function localAppValidationReason(config) {
|
|
56
56
|
if (config.subject.source === "local-app") {
|
|
57
57
|
const type = config.actors[0]?.type ?? "";
|
package/dist/study/doctor.js
CHANGED
|
@@ -2,12 +2,13 @@ import path from "node:path";
|
|
|
2
2
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
3
3
|
import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
4
4
|
import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
5
|
-
import { localAgentVersionRefusal } from "../actors/local-agent/cli.js";
|
|
5
|
+
import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
|
|
6
6
|
import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
|
|
7
7
|
import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
|
|
8
8
|
import { externalCatchHealthy } from "../comms/sandbox-catch.js";
|
|
9
9
|
import { receivingRequiredKey } from "../comms/setup.js";
|
|
10
10
|
import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
|
|
11
|
+
import { cli } from "../cli/invocation.js";
|
|
11
12
|
/** A ready row's message, with the schema values the launch recorded. */
|
|
12
13
|
function readyMessage(message, readiness) {
|
|
13
14
|
const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
|
|
@@ -212,7 +213,7 @@ async function localBrowserChecks(config, args) {
|
|
|
212
213
|
ok: healthy,
|
|
213
214
|
message: healthy
|
|
214
215
|
? "Recipient inbox routes are ready. Point your app's email sends at this catch; delivery remains untested. No mailbox-provider credentials are needed, and this does not receive arbitrary internet mail."
|
|
215
|
-
:
|
|
216
|
+
: `Captured inbox is unavailable or outdated. Start or upgrade and restart ${cli("comms catch")}, then check comms.email.external.catchBaseUrl (and inboxBaseUrl if set). No participant was allocated.`,
|
|
216
217
|
});
|
|
217
218
|
}
|
|
218
219
|
return checks;
|
|
@@ -229,7 +230,7 @@ async function realEmailCheck(connection, keys, args) {
|
|
|
229
230
|
? "The selected email connection is missing or invalid. Open Connections in the TUI."
|
|
230
231
|
: !args.keyPresent(name)
|
|
231
232
|
? `Missing ${name} for the selected email connection. Provide it through process env or --dotenv. Authentication has not been checked.`
|
|
232
|
-
:
|
|
233
|
+
: `Fresh hosted inbox per participant. Local presence only; run ${cli("comms check --online")} to authenticate. Provider permissions/capacity and delivery remain untested.`,
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
/** How the participant authenticates: the terminal model key, or a local agent's login. */
|
|
@@ -259,7 +260,7 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
|
|
|
259
260
|
return [
|
|
260
261
|
{
|
|
261
262
|
name: "local participant authentication",
|
|
262
|
-
ok: agent
|
|
263
|
+
ok: agent !== undefined && localAgentReady(agent),
|
|
263
264
|
message: agent?.authStatus === "authenticated"
|
|
264
265
|
? (outdated ??
|
|
265
266
|
`${agent.label}${agent.version === undefined ? "" : ` ${agent.version}`} reports authenticated on the host. E2B supplies the desktop; no OpenAI API key is required for this participant.`)
|
|
@@ -24,5 +24,5 @@ export declare const humanishScripts: Record<string, string>;
|
|
|
24
24
|
* a signed-in Codex and no provider key, the study runs with the operator's own agent and needs
|
|
25
25
|
* only E2B. Writing the other one there would hand someone homework instead of a first run.
|
|
26
26
|
*/
|
|
27
|
-
export declare function starterFilesFor(actor: StarterActor, localBrowser?: LocalBrowserStarter, localAgent?: LocalAgentId): StarterFile[];
|
|
27
|
+
export declare function starterFilesFor(actor: StarterActor, localBrowser?: LocalBrowserStarter, localAgent?: LocalAgentId, command?: string): StarterFile[];
|
|
28
28
|
export {};
|
|
@@ -34,6 +34,44 @@ defaults:
|
|
|
34
34
|
`,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The `humanish/README.md` init writes. Its commands start with `command`, the invocation init
|
|
39
|
+
* chose for this install (src/cli/invocation.ts), as in the section init writes to `AGENTS.md`.
|
|
40
|
+
*/
|
|
41
|
+
function readmeFile(command) {
|
|
42
|
+
return {
|
|
43
|
+
path: "humanish/README.md",
|
|
44
|
+
plane: "source",
|
|
45
|
+
contents: `# humanish
|
|
46
|
+
|
|
47
|
+
This directory holds the committed definitions of this app's humanish studies: study files,
|
|
48
|
+
personas and scenarios.
|
|
49
|
+
|
|
50
|
+
Keep this directory public-safe:
|
|
51
|
+
|
|
52
|
+
- synthetic personas only;
|
|
53
|
+
- synthetic fixtures only;
|
|
54
|
+
- env var names only, never values;
|
|
55
|
+
- no PII, PHI, secrets, raw private transcripts, private screenshots, customer data, or patient data.
|
|
56
|
+
|
|
57
|
+
Generated run bundles, screenshots, traces, logs, and local overrides belong in ignored \`.humanish/\`.
|
|
58
|
+
|
|
59
|
+
Studies:
|
|
60
|
+
|
|
61
|
+
- committed reusable studies live in humanish/studies/*.yaml;
|
|
62
|
+
- private or machine-local studies live in ignored .humanish/studies/*.yaml or
|
|
63
|
+
.humanish/local/studies/*.yaml;
|
|
64
|
+
- run a study with \`${command} run <study>\`, or \`${command} watch <study>\` to stay attached;
|
|
65
|
+
- files under humanish/labs/ move there with \`${command} migrate\`.
|
|
66
|
+
|
|
67
|
+
Format standard:
|
|
68
|
+
|
|
69
|
+
- human-authored humanish source uses .yaml;
|
|
70
|
+
- generated artifacts, synthetic fixtures, and event streams use .json or .ndjson;
|
|
71
|
+
- .yml is reserved for outside ecosystem files such as GitHub Actions, not humanish source.
|
|
72
|
+
`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
37
75
|
/**
|
|
38
76
|
* The flagship live study. A ChatGPT-account Codex participant has no API-dollar price, so preflight
|
|
39
77
|
* refuses a dollar cap on it (HUMANISH_COMPUTER_USE_UNPRICED_CAP); that variant is bounded by the
|
|
@@ -113,38 +151,7 @@ defaults:
|
|
|
113
151
|
};
|
|
114
152
|
}
|
|
115
153
|
export const starterFiles = [
|
|
116
|
-
|
|
117
|
-
path: "humanish/README.md",
|
|
118
|
-
plane: "source",
|
|
119
|
-
contents: `# humanish
|
|
120
|
-
|
|
121
|
-
This directory holds the committed definitions of this app's humanish studies: study files,
|
|
122
|
-
personas and scenarios.
|
|
123
|
-
|
|
124
|
-
Keep this directory public-safe:
|
|
125
|
-
|
|
126
|
-
- synthetic personas only;
|
|
127
|
-
- synthetic fixtures only;
|
|
128
|
-
- env var names only, never values;
|
|
129
|
-
- no PII, PHI, secrets, raw private transcripts, private screenshots, customer data, or patient data.
|
|
130
|
-
|
|
131
|
-
Generated run bundles, screenshots, traces, logs, and local overrides belong in ignored \`.humanish/\`.
|
|
132
|
-
|
|
133
|
-
Studies:
|
|
134
|
-
|
|
135
|
-
- committed reusable studies live in humanish/studies/*.yaml;
|
|
136
|
-
- private or machine-local studies live in ignored .humanish/studies/*.yaml or
|
|
137
|
-
.humanish/local/studies/*.yaml;
|
|
138
|
-
- run a study with \`humanish run <study>\`, or \`humanish watch <study>\` to stay attached;
|
|
139
|
-
- files under humanish/labs/ move there with \`humanish migrate\`.
|
|
140
|
-
|
|
141
|
-
Format standard:
|
|
142
|
-
|
|
143
|
-
- human-authored humanish source uses .yaml;
|
|
144
|
-
- generated artifacts, synthetic fixtures, and event streams use .json or .ndjson;
|
|
145
|
-
- .yml is reserved for outside ecosystem files such as GitHub Actions, not humanish source.
|
|
146
|
-
`,
|
|
147
|
-
},
|
|
154
|
+
readmeFile("humanish"),
|
|
148
155
|
{
|
|
149
156
|
path: "humanish/personas/synthetic-new-user.yaml",
|
|
150
157
|
plane: "source",
|
|
@@ -194,24 +201,6 @@ steps:
|
|
|
194
201
|
expectation: The user sees a clear next state.
|
|
195
202
|
- name: Capture review notes
|
|
196
203
|
expectation: Notes are public-safe and evidence-backed.
|
|
197
|
-
`,
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
path: "humanish/scenarios/onboarding-regression.yaml",
|
|
201
|
-
plane: "source",
|
|
202
|
-
contents: `schema: humanish.scenario.v1
|
|
203
|
-
id: onboarding-regression
|
|
204
|
-
title: Onboarding regression
|
|
205
|
-
persona: skeptical-power-user
|
|
206
|
-
goal: Exercise onboarding friction using synthetic inputs and explicit recovery checks.
|
|
207
|
-
mode: dry-run
|
|
208
|
-
steps:
|
|
209
|
-
- name: Start onboarding
|
|
210
|
-
expectation: Required information is clear.
|
|
211
|
-
- name: Use synthetic fixture data
|
|
212
|
-
expectation: No real user data is entered.
|
|
213
|
-
- name: Check recovery path
|
|
214
|
-
expectation: The user can back out or retry safely.
|
|
215
204
|
`,
|
|
216
205
|
},
|
|
217
206
|
{
|
|
@@ -308,83 +297,6 @@ execution:
|
|
|
308
297
|
# allowPublicTargets: true # required to drive a non-loopback app-url (a deployment you own)
|
|
309
298
|
defaults:
|
|
310
299
|
open: true
|
|
311
|
-
`,
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
path: "humanish/studies/lobby-trivia-3player.yaml",
|
|
315
|
-
plane: "source",
|
|
316
|
-
contents: `schema: humanish.study.v3
|
|
317
|
-
id: lobby-trivia-3player
|
|
318
|
-
title: "Shared world on a public app: three mobile participants in one lobby"
|
|
319
|
-
description: >-
|
|
320
|
-
A worked example of the external-public shared-world route: three participants on mobile layouts
|
|
321
|
-
play the same multiplayer lobby on a public deployment at the same time. The public site is the
|
|
322
|
-
shared plane, so there is no clone, subject sandbox or seed. The host participant creates the
|
|
323
|
-
lobby, humanish reads the session code from the host's page URL, and the other participants get
|
|
324
|
-
it in their missions and join through the app's own join flow. Starts as a dry run, which writes
|
|
325
|
-
one evidence bundle at no cost. A live run needs OPENAI_API_KEY, E2B_API_KEY and mode: live. Point appUrl and publicTarget at a public deployment you own or operate. Mobile layout:
|
|
326
|
-
Chrome's physical window is at least 500px wide and has no touch emulation, desktop pixel ratio
|
|
327
|
-
and a desktop user agent. Set execution.desktop.fidelity.mobileEmulation to true to request the
|
|
328
|
-
preset's CSS viewport, touch, pixel ratio and mobile user agent on Chrome or Chromium; the bundle
|
|
329
|
-
records the measured geometry and emulation fidelity. A run on a public site proves less than a
|
|
330
|
-
provisioned one: its provenance is external-public, it carries no synthetic-data attestation and
|
|
331
|
-
no authoritative shared-state proof, and concurrency rests on overlapping sessions and every
|
|
332
|
-
participant reaching the same lobby.
|
|
333
|
-
route: shared-world
|
|
334
|
-
mode: dry-run # a live run opens three mobile-layout desktops against the public app
|
|
335
|
-
subject:
|
|
336
|
-
source: app-url # the public deployment is the shared plane: no clone and no getHost
|
|
337
|
-
appUrl: https://your-public-app.example/ # a deployment you own or operate
|
|
338
|
-
publicTarget: # required: your attestation that you own or operate this deployment
|
|
339
|
-
owner: your-org/your-app
|
|
340
|
-
authorized: true
|
|
341
|
-
policies:
|
|
342
|
-
allowPublicTargets: true # required: the shared plane is a public, non-loopback deployment
|
|
343
|
-
actor:
|
|
344
|
-
type: openai-computer-use
|
|
345
|
-
mission: You are one of several users on the same shared session at once. Play your role, then stop.
|
|
346
|
-
participants: # at least two participants, exactly one with host: true
|
|
347
|
-
- id: host
|
|
348
|
-
host: true # this participant creates the shared session
|
|
349
|
-
device: mobile # 414x896 preset; physical window width floors to 500 (see emulation above)
|
|
350
|
-
instruction: Create a shared lobby, wait for the others to join, then start and play.
|
|
351
|
-
- id: player-2
|
|
352
|
-
device: mobile
|
|
353
|
-
instruction: Join the lobby you're told the code for, then play.
|
|
354
|
-
- id: player-3
|
|
355
|
-
device: small-mobile
|
|
356
|
-
instruction: Join the lobby you're told the code for, then play.
|
|
357
|
-
execution:
|
|
358
|
-
target: e2b-desktop
|
|
359
|
-
# Each participant's session budget, which also bounds the host's handoff deadline. It has to
|
|
360
|
-
# cover provisioning, joining the shared session and play; 420000 ms (7 minutes) does.
|
|
361
|
-
timeoutMs: 420000
|
|
362
|
-
concurrency: 3 # all three participants at once, the default; lower it only to limit paid desktops
|
|
363
|
-
# desktop:
|
|
364
|
-
# fidelity: { mobileEmulation: true } # opt into mobile CSS viewport, touch, DPR and user agent
|
|
365
|
-
defaults:
|
|
366
|
-
open: true
|
|
367
|
-
`,
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
path: "humanish/coverage-map.md",
|
|
371
|
-
plane: "source",
|
|
372
|
-
contents: `# Coverage Map
|
|
373
|
-
|
|
374
|
-
This file should enumerate screens, roles, states, and paths before scenarios are treated as complete.
|
|
375
|
-
|
|
376
|
-
Current starter coverage is intentionally minimal and synthetic.
|
|
377
|
-
`,
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
path: "humanish/coverage-matrix.md",
|
|
381
|
-
plane: "source",
|
|
382
|
-
contents: `# Coverage Matrix
|
|
383
|
-
|
|
384
|
-
| Area | Persona | Happy Path | Sad Path | Status |
|
|
385
|
-
| --- | --- | --- | --- | --- |
|
|
386
|
-
| First run | synthetic-new-user | planned | planned | starter |
|
|
387
|
-
| Onboarding | skeptical-power-user | planned | planned | starter |
|
|
388
300
|
`,
|
|
389
301
|
},
|
|
390
302
|
];
|
|
@@ -412,10 +324,12 @@ export const humanishScripts = {
|
|
|
412
324
|
* a signed-in Codex and no provider key, the study runs with the operator's own agent and needs
|
|
413
325
|
* only E2B. Writing the other one there would hand someone homework instead of a first run.
|
|
414
326
|
*/
|
|
415
|
-
export function starterFilesFor(actor, localBrowser = DEFAULT_LOCAL_BROWSER_STARTER, localAgent) {
|
|
327
|
+
export function starterFilesFor(actor, localBrowser = DEFAULT_LOCAL_BROWSER_STARTER, localAgent, command = "humanish") {
|
|
416
328
|
return starterFiles.map((file) => file.path === "humanish/studies/local-browser.yaml"
|
|
417
329
|
? localBrowserStudy(localBrowser)
|
|
418
330
|
: file.path === "humanish/studies/try-live.yaml"
|
|
419
331
|
? tryLiveStudy(actor, localAgent)
|
|
420
|
-
: file
|
|
332
|
+
: file.path === "humanish/README.md"
|
|
333
|
+
? readmeFile(command)
|
|
334
|
+
: file);
|
|
421
335
|
}
|
package/dist/study/init.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { agentsSection, firstRunGuidance, starterActorFor, starterLocalAgentFor, } from "../cli/first-run-path.js";
|
|
4
4
|
import { detectLocalAgents } from "../actors/local-agent/cli.js";
|
|
5
|
+
import { humanishCommand } from "../cli/invocation.js";
|
|
6
|
+
import { replaceAgentsSection } from "./agents-section.js";
|
|
7
|
+
import { probeLocalBrowserHost } from "./local-browser-host.js";
|
|
5
8
|
import { probeKeySources } from "../keys/key-resolution.js";
|
|
6
9
|
import { DEFAULT_LOCAL_BROWSER_STARTER, runtimeDirectories, starterFilesFor, } from "./init-templates.js";
|
|
7
10
|
import { assertPreparedSelectedOutputDirectory, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
@@ -38,13 +41,13 @@ export async function runInit(options) {
|
|
|
38
41
|
if (initialPathCheck) {
|
|
39
42
|
return failed(initialPathCheck);
|
|
40
43
|
}
|
|
41
|
-
await planAgentsFile(preparedProjectRoot, cwd, plan);
|
|
42
44
|
// The starter live study is written for the brain this machine can actually use. Shipping it as
|
|
43
45
|
// openai-computer-use on a machine with no provider key but a signed-in Codex would hand someone
|
|
44
46
|
// a file that asks for a credential they were just told they do not need.
|
|
45
47
|
const machine = await firstRunEnvironment(options.env ?? process.env, requestedCwd);
|
|
48
|
+
await planAgentsFile(preparedProjectRoot, cwd, plan, humanishCommand());
|
|
46
49
|
const starterActor = starterActorFor(machine);
|
|
47
|
-
await planStarterFiles(preparedProjectRoot, cwd, starterFilesFor(starterActor, localBrowser.value, starterLocalAgentFor(machine)), options.localBrowser !== undefined, plan);
|
|
50
|
+
await planStarterFiles(preparedProjectRoot, cwd, starterFilesFor(starterActor, localBrowser.value, starterLocalAgentFor(machine), machine.humanish), options.localBrowser !== undefined, plan);
|
|
48
51
|
await planRuntimeDirectories(preparedProjectRoot, cwd, plan);
|
|
49
52
|
const gitignorePlan = await planGitignore(preparedProjectRoot, cwd);
|
|
50
53
|
plan.changes.push(gitignorePlan.change);
|
|
@@ -88,47 +91,40 @@ export async function runInit(options) {
|
|
|
88
91
|
// `humanish init` is a coding agent doing setup on someone's behalf. Without this, the agent
|
|
89
92
|
// that arrives tomorrow finds a humanish/ directory and no idea what to do with it.
|
|
90
93
|
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
+
// An existing `AGENTS.md` is a file someone wrote. init appends its own section once, and on a
|
|
95
|
+
// rerun replaces only that section, between its two markers, so the commands match the installed
|
|
96
|
+
// humanish. Every byte outside the markers stays as it was.
|
|
97
|
+
async function planAgentsFile(preparedProjectRoot, cwd, plan, humanish) {
|
|
94
98
|
const agentsPath = "AGENTS.md";
|
|
95
99
|
const existingAgents = await readTextIfExists(preparedProjectRoot, agentsPath);
|
|
96
|
-
const section = agentsSection();
|
|
97
|
-
|
|
98
|
-
plan.changes.push({
|
|
99
|
-
path: agentsPath,
|
|
100
|
-
action: "create",
|
|
101
|
-
target: "source",
|
|
102
|
-
reason: "how a coding agent runs humanish here",
|
|
103
|
-
});
|
|
100
|
+
const section = agentsSection(humanish);
|
|
101
|
+
const write = (contents, action, reason) => {
|
|
102
|
+
plan.changes.push({ path: agentsPath, action, target: "source", reason });
|
|
104
103
|
plan.writes.push({
|
|
105
104
|
absolutePath: path.join(cwd, agentsPath),
|
|
106
105
|
relativePath: agentsPath,
|
|
107
|
-
contents
|
|
106
|
+
contents,
|
|
108
107
|
target: "source",
|
|
109
108
|
});
|
|
109
|
+
};
|
|
110
|
+
const skip = (reason) => plan.changes.push({ path: agentsPath, action: "skip", target: "source", reason });
|
|
111
|
+
if (existingAgents === null) {
|
|
112
|
+
write(`# AGENTS.md\n${section}`, "create", "how a coding agent runs humanish here");
|
|
113
|
+
return;
|
|
110
114
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
115
|
+
const replaced = replaceAgentsSection(existingAgents, section);
|
|
116
|
+
if (replaced.kind === "absent") {
|
|
117
|
+
write(`${existingAgents.replace(/\s*$/, "")}\n${section}`, "update", "append how a coding agent runs humanish");
|
|
118
|
+
}
|
|
119
|
+
else if (replaced.kind === "unknown") {
|
|
120
|
+
skip("humanish section edited by hand");
|
|
121
|
+
plan.warnings.push(`Left the humanish section of ${agentsPath} unchanged: it has no end marker and does not match a version init wrote, so it may hold your edits. To get the current section, delete it from the "## humanish" heading to the end of the section and run init again.`);
|
|
122
|
+
}
|
|
123
|
+
else if (replaced.contents === existingAgents) {
|
|
124
|
+
skip("humanish section is current");
|
|
118
125
|
}
|
|
119
126
|
else {
|
|
120
|
-
|
|
121
|
-
path: agentsPath,
|
|
122
|
-
action: "update",
|
|
123
|
-
target: "source",
|
|
124
|
-
reason: "append how a coding agent runs humanish",
|
|
125
|
-
});
|
|
126
|
-
plan.writes.push({
|
|
127
|
-
absolutePath: path.join(cwd, agentsPath),
|
|
128
|
-
relativePath: agentsPath,
|
|
129
|
-
contents: `${existingAgents.replace(/\s*$/, "")}\n${section}`,
|
|
130
|
-
target: "source",
|
|
131
|
-
});
|
|
127
|
+
write(replaced.contents, "update", "refresh the humanish section");
|
|
132
128
|
}
|
|
133
129
|
}
|
|
134
130
|
/** Create each starter file that is missing; never overwrite one that exists. */
|
|
@@ -318,8 +314,9 @@ async function firstRunEnvironment(env, cwd) {
|
|
|
318
314
|
localAgents: agents
|
|
319
315
|
.filter((agent) => agent.authStatus === "authenticated")
|
|
320
316
|
.map((agent) => ({ id: agent.id, label: agent.label })),
|
|
317
|
+
localBrowserHost: await probeLocalBrowserHost(),
|
|
321
318
|
platform: process.platform,
|
|
322
|
-
|
|
319
|
+
humanish: humanishCommand(),
|
|
323
320
|
};
|
|
324
321
|
}
|
|
325
322
|
function getMode(options) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LocalBrowserHost } from "../cli/first-run-path.js";
|
|
2
|
+
export interface LocalBrowserHostProbe {
|
|
3
|
+
platform: NodeJS.Platform;
|
|
4
|
+
arch: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
/** Resolves when `file` can be opened with `mode`; rejects otherwise. */
|
|
7
|
+
access: (file: string, mode: number) => Promise<void>;
|
|
8
|
+
/** The Mac's chip name, as `sysctl -n machdep.cpu.brand_string` prints it. */
|
|
9
|
+
macChip: () => Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function probeLocalBrowserHost(probe?: LocalBrowserHostProbe): Promise<LocalBrowserHost>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// init's quick read of whether this host can run a local browser study, so its next steps never
|
|
2
|
+
// send someone to a route the host cannot run. It reads devices, `PATH` and the Mac chip name only;
|
|
3
|
+
// it starts no container and no VM. `doctor --study local-browser` runs the full check, including
|
|
4
|
+
// whether Docker is rootful and local.
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
const defaultProbe = () => ({
|
|
12
|
+
platform: process.platform,
|
|
13
|
+
arch: process.arch,
|
|
14
|
+
env: process.env,
|
|
15
|
+
access,
|
|
16
|
+
macChip: async () => (await exec("sysctl", ["-n", "machdep.cpu.brand_string"], { timeout: 5_000 })).stdout,
|
|
17
|
+
});
|
|
18
|
+
export async function probeLocalBrowserHost(probe = defaultProbe()) {
|
|
19
|
+
const can = async (file, mode) => probe.access(file, mode).then(() => true, () => false);
|
|
20
|
+
if (probe.platform === "linux" && probe.arch === "x64") {
|
|
21
|
+
// The same device checks doctor makes. The rootful Docker daemon opens these devices, so this
|
|
22
|
+
// user needs no access to them.
|
|
23
|
+
if (!(await can("/dev/kvm", constants.F_OK)))
|
|
24
|
+
return { ok: false, reason: "/dev/kvm is missing" };
|
|
25
|
+
if (!(await can("/dev/net/tun", constants.F_OK)))
|
|
26
|
+
return { ok: false, reason: "/dev/net/tun is missing" };
|
|
27
|
+
const dirs = (probe.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
28
|
+
for (const dir of dirs)
|
|
29
|
+
if (await can(path.join(dir, "docker"), constants.X_OK))
|
|
30
|
+
return { ok: true };
|
|
31
|
+
return { ok: false, reason: "the docker command is not on the search path" };
|
|
32
|
+
}
|
|
33
|
+
if (probe.platform === "darwin" && probe.arch === "arm64") {
|
|
34
|
+
// The local route needs nested virtualization, which Apple added with the M3.
|
|
35
|
+
const chip = await probe.macChip().catch(() => "");
|
|
36
|
+
const generation = /^Apple M(\d+)/.exec(chip.trim());
|
|
37
|
+
if (generation !== null && Number(generation[1]) >= 3)
|
|
38
|
+
return { ok: true };
|
|
39
|
+
return { ok: false, reason: "they need an M3 or newer Mac" };
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, reason: "they need Linux x64 or an M3-or-newer Mac" };
|
|
42
|
+
}
|
|
@@ -2,6 +2,7 @@ import { DEVICE_PRESET_NAMES, isDevicePresetName } from "../device-presets.js";
|
|
|
2
2
|
import { isReasoningEffort, reasoningEffortNames } from "../../actors/reasoning-effort.js";
|
|
3
3
|
import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
4
4
|
import { isHttpUrl } from "./subject.js";
|
|
5
|
+
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
5
6
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
6
7
|
import { invalid, posInt, str } from "./values.js";
|
|
7
8
|
import { isRecord } from "../../run/type-guards.js";
|
|
@@ -265,13 +266,20 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
265
266
|
if (!isHttpUrl(target)) {
|
|
266
267
|
return invalid(`actors[${actorIndex}].lanes[${entryIndex}].target must be an absolute http(s) URL.`);
|
|
267
268
|
}
|
|
269
|
+
const credential = urlCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].target`, target);
|
|
270
|
+
if (credential)
|
|
271
|
+
return invalid(credential);
|
|
268
272
|
parsedEntry.target = target;
|
|
269
273
|
}
|
|
270
274
|
// `entry` is shape-captured here; the same-origin-with-serve.url check needs serve context, so
|
|
271
275
|
// it runs in sharedWorldValidationReason (where the route + serve.url are known).
|
|
272
276
|
const entryPath = str(entry.entry);
|
|
273
|
-
if (entryPath !== undefined)
|
|
277
|
+
if (entryPath !== undefined) {
|
|
278
|
+
const credential = entryCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].entry`, entryPath);
|
|
279
|
+
if (credential)
|
|
280
|
+
return invalid(credential);
|
|
274
281
|
parsedEntry.entry = entryPath;
|
|
282
|
+
}
|
|
275
283
|
// `host` marks the designated host on the external-public shared-world route; the
|
|
276
284
|
// exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
|
|
277
285
|
if (entry.host !== undefined) {
|