humanish 0.37.0 → 0.39.0
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/dist/comms-catch-host.d.ts +50 -0
- package/dist/comms-catch-host.js +147 -0
- package/dist/comms-catch-host.js.map +1 -0
- package/dist/comms-sandbox-catch.d.ts +71 -1
- package/dist/comms-sandbox-catch.js +209 -18
- package/dist/comms-sandbox-catch.js.map +1 -1
- package/dist/concurrent-shared-world-lab.d.ts +4 -1
- package/dist/concurrent-shared-world-lab.js +65 -8
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +8 -0
- package/dist/cua-actor-lab.js +26 -5
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/lab-config.d.ts +21 -1
- package/dist/lab-config.js +55 -6
- package/dist/lab-config.js.map +1 -1
- package/dist/persona-resolve.d.ts +44 -0
- package/dist/persona-resolve.js +109 -0
- package/dist/persona-resolve.js.map +1 -0
- package/dist/program.js +47 -0
- package/dist/program.js.map +1 -1
- package/dist/shared-world-lab.js +7 -2
- package/dist/shared-world-lab.js.map +1 -1
- package/docs/contracts/schemas.md +19 -3
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Resolve committed persona files into compiled personas, for EVERY lane (#381).
|
|
2
|
+
//
|
|
3
|
+
// #308 gave the terminal lane this: read `humanish/personas/<id>.yaml`, compile the traits into
|
|
4
|
+
// behavioral directives, and record truthfully which traits were applied. The computer-use lanes
|
|
5
|
+
// never got it — they composed a bare `Persona: <id>.` line and hardcoded `traitsApplied: []`, so
|
|
6
|
+
// on browser routes the persona axis was a label rather than a behavior. A live two-lane contrast
|
|
7
|
+
// (an impatient expert vs a patient newcomer, same app, same mission) came back with near-identical
|
|
8
|
+
// action profiles, which read like evidence that personas do not matter and was actually evidence
|
|
9
|
+
// that personas were never applied.
|
|
10
|
+
//
|
|
11
|
+
// This module is the shared implementation so the two routes cannot drift again: one containment
|
|
12
|
+
// rule, one compiler, one fallback polarity.
|
|
13
|
+
//
|
|
14
|
+
// FAIL-SAFE, not fail-closed — deliberately the opposite polarity to the scorer loader. A persona
|
|
15
|
+
// that declared nothing must never be given fabricated traits, so an unsafe id, a missing file, or
|
|
16
|
+
// unparseable YAML falls back to the bare id line with a truthful EMPTY traitsApplied (warning only
|
|
17
|
+
// when the file existed but could not be read). Silence here is honest; invention would not be.
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { parseResolvedPersona } from "./persona.js";
|
|
21
|
+
import { prepareSelectedOutputDirectory, readContainedRegularFile } from "./selected-output-paths.js";
|
|
22
|
+
import { realpath } from "node:fs/promises";
|
|
23
|
+
/** Persona ids are file-name segments, never paths: the same grammar the terminal lane enforces. */
|
|
24
|
+
export const PERSONA_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
25
|
+
/** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
|
|
26
|
+
export function personaTitleFromId(personaId) {
|
|
27
|
+
return personaId
|
|
28
|
+
.split(/[-_]/)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
31
|
+
.join(" ");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve ONE committed persona. Returns `null` (never a throw, never a guess) when the id is
|
|
35
|
+
* unsafe or no file exists, so a lane always runs.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveCommittedPersona(projectRoot, personaId) {
|
|
38
|
+
if (!PERSONA_ID_PATTERN.test(personaId)) {
|
|
39
|
+
return { persona: null, warnings: [] };
|
|
40
|
+
}
|
|
41
|
+
for (const candidate of [
|
|
42
|
+
path.posix.join("humanish", "personas", `${personaId}.yaml`),
|
|
43
|
+
path.posix.join("humanish", "personas", `${personaId}.yml`)
|
|
44
|
+
]) {
|
|
45
|
+
const bytes = await readContainedRegularFile(projectRoot, candidate);
|
|
46
|
+
if (!bytes)
|
|
47
|
+
continue;
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = parseYaml(bytes.toString("utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {
|
|
54
|
+
persona: null,
|
|
55
|
+
warnings: [`${candidate} could not be parsed as YAML; the lane ran with the persona id only (no traits applied).`]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { persona: parseResolvedPersona(raw, { id: personaId, name: personaTitleFromId(personaId) }), warnings: [] };
|
|
59
|
+
}
|
|
60
|
+
return { persona: null, warnings: [] };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve every distinct persona id a run will use, once, before lane specs are built. Returning a
|
|
64
|
+
* map keeps the plan builder PURE (it is exported npm surface and asserted pure by tests): the
|
|
65
|
+
* async file reads happen here, and the composer only does a lookup.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveCommittedPersonas(projectRoot, personaIds) {
|
|
68
|
+
const personas = new Map();
|
|
69
|
+
const warnings = [];
|
|
70
|
+
for (const personaId of new Set(personaIds.filter((id) => typeof id === "string" && id.length > 0))) {
|
|
71
|
+
const resolved = await resolveCommittedPersona(projectRoot, personaId);
|
|
72
|
+
if (resolved.persona)
|
|
73
|
+
personas.set(personaId, resolved.persona);
|
|
74
|
+
warnings.push(...resolved.warnings);
|
|
75
|
+
}
|
|
76
|
+
return { personas, warnings };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every persona id a lab config could put on a browser lane: the per-lane roster when one is
|
|
80
|
+
* declared, otherwise the actor-level persona that every fan-out lane inherits.
|
|
81
|
+
*/
|
|
82
|
+
export function labPersonaIds(config) {
|
|
83
|
+
const ids = [];
|
|
84
|
+
for (const actor of config.actors ?? []) {
|
|
85
|
+
if (actor.persona)
|
|
86
|
+
ids.push(actor.persona);
|
|
87
|
+
for (const lane of actor.lanes ?? []) {
|
|
88
|
+
if (lane.persona)
|
|
89
|
+
ids.push(lane.persona);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...new Set(ids)];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Same resolution from a plain cwd, for the labs that carry a directory string rather than an
|
|
96
|
+
* already-prepared root. Realpath-then-prepare mirrors the cua lab so a symlinked cwd still reads
|
|
97
|
+
* personas from the physical project.
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveCommittedPersonasForCwd(cwd, personaIds) {
|
|
100
|
+
try {
|
|
101
|
+
const physical = await realpath(path.resolve(cwd));
|
|
102
|
+
const projectRoot = await prepareSelectedOutputDirectory(path.dirname(physical), physical);
|
|
103
|
+
return await resolveCommittedPersonas(projectRoot, personaIds);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { personas: new Map(), warnings: [] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=persona-resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persona-resolve.js","sourceRoot":"","sources":["../src/persona-resolve.ts"],"names":[],"mappings":"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"}
|
package/dist/program.js
CHANGED
|
@@ -22,6 +22,8 @@ import { DEFAULT_OSS_REPOS, runOssLab } from "./oss-lab.js";
|
|
|
22
22
|
import { cleanupOssMetaLabSandboxes, cleanupStaleOssMetaLabSandboxes, runOssMetaLab, startOssMetaLabLiveRefresh } from "./oss-meta-lab.js";
|
|
23
23
|
import { cleanupRun, doctor, listRuns, readReview, runDryRun, verifyRun } from "./run.js";
|
|
24
24
|
import { reclaimRunSandboxes } from "./reclaim.js";
|
|
25
|
+
import { runCommsCatchHost } from "./comms-catch-host.js";
|
|
26
|
+
import { DEFAULT_SANDBOX_CATCH_PORT } from "./comms-sandbox-catch.js";
|
|
25
27
|
export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
|
|
26
28
|
function readCliVersion() {
|
|
27
29
|
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
@@ -167,6 +169,7 @@ export function createProgram(io = {}) {
|
|
|
167
169
|
registerCleanupCommand(program, cliIo);
|
|
168
170
|
registerReviewCommand(program, cliIo);
|
|
169
171
|
registerRunsCommand(program, cliIo);
|
|
172
|
+
registerCommsCommands(program, cliIo);
|
|
170
173
|
registerReclaimCommand(program, cliIo);
|
|
171
174
|
registerWatchCommand(program, cliIo);
|
|
172
175
|
registerObserveCommand(program, cliIo);
|
|
@@ -372,6 +375,50 @@ function registerRunsCommand(parent, io) {
|
|
|
372
375
|
io.setExitCode(result.ok ? 0 : 2);
|
|
373
376
|
});
|
|
374
377
|
}
|
|
378
|
+
function registerCommsCommands(parent, io) {
|
|
379
|
+
const comms = parent
|
|
380
|
+
.command("comms")
|
|
381
|
+
.description("Off-app comms surfaces (email/SMS the app under test sends).")
|
|
382
|
+
.summary("Off-app comms surfaces.");
|
|
383
|
+
comms
|
|
384
|
+
.command("catch")
|
|
385
|
+
.description("Run the email catch on THIS host so humanish can study an app it does not provision (#328). Your app posts its email sends here; the persona opens /inbox; humanish drains GET /deliveries and writes digest-only evidence. Point your lab's comms.email.external.catchBaseUrl at this server.")
|
|
386
|
+
.summary("Run the adopter-hosted email catch.")
|
|
387
|
+
.option("--port <port>", "Port for capture + inbox (default 8025).", String(DEFAULT_SANDBOX_CATCH_PORT))
|
|
388
|
+
.option("--dir <path>", "Directory for the deliveries log and rendered inbox.", ".humanish/comms-catch")
|
|
389
|
+
.option("--token <value>", "Require this bearer token on GET /deliveries (recommended when reachable off-host).")
|
|
390
|
+
.option("--inbox-port <port>", "Also serve a READ-ONLY inbox listener on 0.0.0.0:<port>, so a persona on another machine can open /inbox. Without it the catch stays loopback-only.")
|
|
391
|
+
.option("--recipient <address>", "Only render mail sent to this address (repeatable). Default: render whatever the app actually mailed.", (value, previous = []) => [...previous, value])
|
|
392
|
+
.action(async (options) => {
|
|
393
|
+
const port = Number.parseInt(options.port, 10);
|
|
394
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_534) {
|
|
395
|
+
io.writeErr("--port must be an integer between 1 and 65534.\n");
|
|
396
|
+
io.setExitCode(2);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
let inboxPort;
|
|
400
|
+
if (options.inboxPort !== undefined) {
|
|
401
|
+
inboxPort = Number.parseInt(options.inboxPort, 10);
|
|
402
|
+
if (!Number.isInteger(inboxPort) || inboxPort <= 0 || inboxPort > 65_534) {
|
|
403
|
+
io.writeErr("--inbox-port must be an integer between 1 and 65534.\n");
|
|
404
|
+
io.setExitCode(2);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (inboxPort === port) {
|
|
408
|
+
io.writeErr("--inbox-port must differ from --port (the capture listener is loopback-only; the inbox listener is not).\n");
|
|
409
|
+
io.setExitCode(2);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
await runCommsCatchHost({
|
|
414
|
+
port,
|
|
415
|
+
dir: options.dir,
|
|
416
|
+
...(options.token ? { token: options.token } : {}),
|
|
417
|
+
...(inboxPort === undefined ? {} : { inboxPort }),
|
|
418
|
+
...(options.recipient && options.recipient.length > 0 ? { recipients: options.recipient } : {})
|
|
419
|
+
}, io);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
375
422
|
function registerReclaimCommand(parent, io) {
|
|
376
423
|
parent
|
|
377
424
|
.command("reclaim")
|