humanish 0.38.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 +31 -0
- package/dist/comms-catch-host.js +82 -4
- package/dist/comms-catch-host.js.map +1 -1
- package/dist/comms-sandbox-catch.d.ts +25 -1
- package/dist/comms-sandbox-catch.js +98 -18
- package/dist/comms-sandbox-catch.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +7 -2
- 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 +23 -4
- package/dist/cua-actor-lab.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 +23 -1
- 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 +1 -1
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ResolvedPersona } from "./persona.js";
|
|
2
|
+
import { type PreparedSelectedOutputDirectory } from "./selected-output-paths.js";
|
|
3
|
+
/** Persona ids are file-name segments, never paths: the same grammar the terminal lane enforces. */
|
|
4
|
+
export declare const PERSONA_ID_PATTERN: RegExp;
|
|
5
|
+
/** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
|
|
6
|
+
export declare function personaTitleFromId(personaId: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve ONE committed persona. Returns `null` (never a throw, never a guess) when the id is
|
|
9
|
+
* unsafe or no file exists, so a lane always runs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCommittedPersona(projectRoot: PreparedSelectedOutputDirectory, personaId: string): Promise<{
|
|
12
|
+
persona: ResolvedPersona | null;
|
|
13
|
+
warnings: string[];
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve every distinct persona id a run will use, once, before lane specs are built. Returning a
|
|
17
|
+
* map keeps the plan builder PURE (it is exported npm surface and asserted pure by tests): the
|
|
18
|
+
* async file reads happen here, and the composer only does a lookup.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCommittedPersonas(projectRoot: PreparedSelectedOutputDirectory, personaIds: readonly (string | undefined)[]): Promise<{
|
|
21
|
+
personas: Map<string, ResolvedPersona>;
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Every persona id a lab config could put on a browser lane: the per-lane roster when one is
|
|
26
|
+
* declared, otherwise the actor-level persona that every fan-out lane inherits.
|
|
27
|
+
*/
|
|
28
|
+
export declare function labPersonaIds(config: {
|
|
29
|
+
actors?: readonly {
|
|
30
|
+
persona?: string;
|
|
31
|
+
lanes?: readonly {
|
|
32
|
+
persona?: string;
|
|
33
|
+
}[];
|
|
34
|
+
}[];
|
|
35
|
+
}): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Same resolution from a plain cwd, for the labs that carry a directory string rather than an
|
|
38
|
+
* already-prepared root. Realpath-then-prepare mirrors the cua lab so a symlinked cwd still reads
|
|
39
|
+
* personas from the physical project.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveCommittedPersonasForCwd(cwd: string, personaIds: readonly (string | undefined)[]): Promise<{
|
|
42
|
+
personas: Map<string, ResolvedPersona>;
|
|
43
|
+
warnings: string[];
|
|
44
|
+
}>;
|
|
@@ -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
|
@@ -387,6 +387,8 @@ function registerCommsCommands(parent, io) {
|
|
|
387
387
|
.option("--port <port>", "Port for capture + inbox (default 8025).", String(DEFAULT_SANDBOX_CATCH_PORT))
|
|
388
388
|
.option("--dir <path>", "Directory for the deliveries log and rendered inbox.", ".humanish/comms-catch")
|
|
389
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])
|
|
390
392
|
.action(async (options) => {
|
|
391
393
|
const port = Number.parseInt(options.port, 10);
|
|
392
394
|
if (!Number.isInteger(port) || port <= 0 || port > 65_534) {
|
|
@@ -394,7 +396,27 @@ function registerCommsCommands(parent, io) {
|
|
|
394
396
|
io.setExitCode(2);
|
|
395
397
|
return;
|
|
396
398
|
}
|
|
397
|
-
|
|
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);
|
|
398
420
|
});
|
|
399
421
|
}
|
|
400
422
|
function registerReclaimCommand(parent, io) {
|