@agent-relay/sdk 6.0.10 → 6.0.11

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.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=persona-spawn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-spawn.d.ts","sourceRoot":"","sources":["../../src/examples/persona-spawn.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * persona-spawn — spawn an agent from an AgentWorkforce persona.
3
+ *
4
+ * Personas are JSON files describing a pre-configured agent (harness, model,
5
+ * system prompt, MCP servers, permissions). They live in
6
+ * ./agentworkforce/personas
7
+ * or any directory you pass via `searchDirs` / `extraDirs`.
8
+ *
9
+ * Run:
10
+ * npm run build && node dist/examples/persona-spawn.js frontend "Build a settings page"
11
+ *
12
+ * Environment:
13
+ * RELAY_API_KEY — Relaycast workspace key (required)
14
+ */
15
+ import { AgentRelay } from "../relay.js";
16
+ import { listPersonas } from "../personas.js";
17
+ const [, , personaId, ...taskParts] = process.argv;
18
+ const task = taskParts.join(" ").trim();
19
+ if (!personaId) {
20
+ const found = listPersonas();
21
+ console.error("Usage: persona-spawn <personaId> [task...]\n");
22
+ if (found.length > 0) {
23
+ console.error("Personas discovered in the default cascade:");
24
+ for (const p of found) {
25
+ console.error(` - ${p.id} (${p.path})`);
26
+ }
27
+ }
28
+ else {
29
+ console.error("No personas found. Place JSON files under ./agentworkforce/personas " +
30
+ "or set AGENT_WORKFORCE_HOME.");
31
+ }
32
+ process.exit(1);
33
+ }
34
+ const relay = new AgentRelay();
35
+ relay.onAgentSpawned = (agent) => console.log(`spawned ${agent.name} (${agent.runtime})`);
36
+ relay.onAgentExited = (agent) => console.log(`exited ${agent.name} code=${agent.exitCode ?? "none"}`);
37
+ const agent = await relay.spawnPersona(personaId, {
38
+ ...(task ? { task } : {}),
39
+ channels: ["general"],
40
+ });
41
+ console.log(`agent ${agent.name} ready, waiting for exit...`);
42
+ await agent.waitForExit();
43
+ await relay.shutdown();
44
+ //# sourceMappingURL=persona-spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-spawn.js","sourceRoot":"","sources":["../../src/examples/persona-spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,CAAC,EAAE,AAAD,EAAG,SAAS,EAAE,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;AACnD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAExC,IAAI,CAAC,SAAS,EAAE,CAAC;IACf,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;IAC7B,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAC7D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CACX,sEAAsE;YACpE,8BAA8B,CACjC,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAC;AAE/B,KAAK,CAAC,cAAc,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;AAC1F,KAAK,CAAC,aAAa,GAAG,CAAC,KAAK,EAAE,EAAE,CAC9B,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC;AAEvE,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE;IAChD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACzB,QAAQ,EAAE,CAAC,SAAS,CAAC;CACtB,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,IAAI,6BAA6B,CAAC,CAAC;AAC9D,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC;AAC1B,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC"}
package/dist/index.d.ts CHANGED
@@ -15,4 +15,5 @@ export * from './workflows/index.js';
15
15
  export * from './spawn-from-env.js';
16
16
  export * from './cli-registry.js';
17
17
  export * from './cli-resolver.js';
18
+ export * from './personas.js';
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EACL,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACtE,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EACL,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACtE,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ export * from './workflows/index.js';
14
14
  export * from './spawn-from-env.js';
15
15
  export * from './cli-registry.js';
16
16
  export * from './cli-resolver.js';
17
+ export * from './personas.js';
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,eAAe,EAA+B,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EACL,gBAAgB,GAKjB,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEpE,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,eAAe,EAA+B,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EACL,gBAAgB,GAKjB,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEpE,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Persona loading and translation.
3
+ *
4
+ * A persona is a JSON file that describes a pre-configured agent: which
5
+ * harness (CLI) to use, which model, what system prompt to inject, plus
6
+ * optional MCP servers and permission flags. Personas live in
7
+ * `<cwd>/agentworkforce/personas`, the AgentWorkforce home directory, or
8
+ * any directory the caller passes explicitly.
9
+ *
10
+ * Translation from a resolved persona to `{bin, args}` delegates to
11
+ * `@agentworkforce/harness-kit#buildInteractiveSpec`, so relay always
12
+ * produces the same launch args the AgentWorkforce CLI does.
13
+ *
14
+ * The schema mirrors the AgentWorkforce persona format
15
+ * (see https://github.com/AgentWorkforce/workforce). Skills installation,
16
+ * mount policy, sidecar markdown, input rendering, and routing profiles
17
+ * are deliberately not handled here — callers needing those should use
18
+ * the `agentworkforce` CLI directly.
19
+ */
20
+ import { type InteractiveConfigFile } from '@agentworkforce/harness-kit';
21
+ import { HARNESS_VALUES, PERSONA_TIERS, type Harness, type McpServerSpec, type PersonaPermissions, type PersonaTier } from '@agentworkforce/workload-router';
22
+ export type { Harness, McpServerSpec, PersonaPermissions, PersonaTier };
23
+ export { HARNESS_VALUES, PERSONA_TIERS };
24
+ export interface PersonaTierSpec {
25
+ harness?: Harness;
26
+ model?: string;
27
+ systemPrompt?: string;
28
+ /** Free-form harness settings (reasoning level, timeout) — not consumed by spawnPty today. */
29
+ harnessSettings?: Record<string, unknown>;
30
+ }
31
+ /** Raw persona file shape. */
32
+ export interface PersonaFile {
33
+ id: string;
34
+ intent?: string;
35
+ description?: string;
36
+ tags?: string[];
37
+ /** Used for every tier when set without `tiers`. Ignored when `tiers` is set. */
38
+ systemPrompt?: string;
39
+ /** Top-level harness/model — used when there are no tiers. */
40
+ harness?: Harness;
41
+ model?: string;
42
+ permissions?: PersonaPermissions;
43
+ mcpServers?: Record<string, McpServerSpec>;
44
+ /** Per-tier overrides. A tier set here takes precedence over the top-level fields. */
45
+ tiers?: Partial<Record<PersonaTier, PersonaTierSpec>>;
46
+ /** Inherits from another persona id (looked up in the same search dirs). One level deep. */
47
+ extends?: string;
48
+ }
49
+ /** A persona file located on disk. */
50
+ export interface DiscoveredPersona {
51
+ id: string;
52
+ path: string;
53
+ spec: PersonaFile;
54
+ }
55
+ /** A persona resolved against a tier — ready for {@link buildPersonaSpawnSpec}. */
56
+ export interface ResolvedPersona {
57
+ id: string;
58
+ /** Absolute path to the JSON file the spec came from. */
59
+ source: string;
60
+ tier: PersonaTier;
61
+ harness: Harness;
62
+ model: string;
63
+ systemPrompt: string;
64
+ description?: string;
65
+ permissions?: PersonaPermissions;
66
+ mcpServers?: Record<string, McpServerSpec>;
67
+ }
68
+ export interface PersonaLoadOptions {
69
+ cwd?: string;
70
+ /** Override the default search-dir cascade. */
71
+ searchDirs?: string[];
72
+ /** Extra dirs appended after the default cascade. */
73
+ extraDirs?: string[];
74
+ /** Tier to resolve. Defaults to 'best'. */
75
+ tier?: PersonaTier;
76
+ }
77
+ /**
78
+ * The shape `AgentRelay.spawnPersona` needs to drive `spawnPty`. Built by
79
+ * {@link buildPersonaSpawnSpec} from a {@link ResolvedPersona}.
80
+ */
81
+ export interface PersonaSpawnSpec {
82
+ /** CLI to launch (matches relay's AgentCli union: 'claude' | 'codex' | 'opencode'). */
83
+ cli: string;
84
+ model: string;
85
+ args: string[];
86
+ /**
87
+ * If non-null, append this as the final positional arg to the CLI invocation.
88
+ * Codex uses this to carry the system prompt; claude / opencode return null.
89
+ */
90
+ initialPrompt: string | null;
91
+ /**
92
+ * Files the caller must materialize (relative to spawn cwd) before launching
93
+ * the agent. Used by opencode to drop an `opencode.json` carrying the
94
+ * persona's agent definition. Empty for claude / codex.
95
+ */
96
+ configFiles: InteractiveConfigFile[];
97
+ /** Non-fatal warnings from the harness-kit translation step. */
98
+ warnings: string[];
99
+ }
100
+ /**
101
+ * The default cascade. First match wins for a given persona id.
102
+ *
103
+ * 1. `<cwd>/agentworkforce/personas` (the path most projects pick)
104
+ * 2. `<cwd>/.agentworkforce/workforce/personas` (workforce CLI's project default)
105
+ * 3. `<cwd>/agentworkforce/workforce/personas` (alt workforce layout)
106
+ * 4. `$AGENT_WORKFORCE_HOME/personas` if set, else `~/.agentworkforce/workforce/personas`
107
+ */
108
+ export declare function defaultPersonaSearchDirs(cwd?: string): string[];
109
+ /**
110
+ * List every persona discoverable across the search-dir cascade. When the
111
+ * same id appears in multiple dirs, only the first match (by cascade order)
112
+ * is returned.
113
+ */
114
+ export declare function listPersonas(options?: PersonaLoadOptions): DiscoveredPersona[];
115
+ /**
116
+ * Find a persona file by id across the search-dir cascade.
117
+ * Returns undefined if not found.
118
+ */
119
+ export declare function findPersona(id: string, options?: PersonaLoadOptions): DiscoveredPersona | undefined;
120
+ /**
121
+ * Load and resolve a persona by id. Searches the cascade, applies the
122
+ * chosen tier, and resolves a single level of `extends` against the same
123
+ * cascade. Throws if the persona is missing required fields for the tier.
124
+ */
125
+ export declare function loadPersona(id: string, options?: PersonaLoadOptions): ResolvedPersona;
126
+ /**
127
+ * Translate a resolved persona into the bin/args spawnPty needs. Delegates
128
+ * to {@link buildInteractiveSpec} from `@agentworkforce/harness-kit` so
129
+ * relay produces the same launch shape the AgentWorkforce CLI does.
130
+ */
131
+ export declare function buildPersonaSpawnSpec(persona: ResolvedPersona): PersonaSpawnSpec;
132
+ /**
133
+ * Codex has no system-prompt flag, so the persona's instructions must ride
134
+ * on the task. Combines them in the same shape the agentworkforce
135
+ * harness-kit uses for non-interactive codex runs.
136
+ */
137
+ export declare function composePersonaTask(spec: Pick<PersonaSpawnSpec, 'initialPrompt'>, userTask: string | undefined): string | undefined;
138
+ /** Tracks a file we wrote, so the caller can restore the prior contents. */
139
+ export interface MaterializedConfigFile {
140
+ /** Absolute path that was written. */
141
+ path: string;
142
+ /** Whether a file existed at this path before we wrote. */
143
+ existed: boolean;
144
+ /** Prior contents (only set when existed is true). */
145
+ previous?: string;
146
+ }
147
+ /**
148
+ * Write each persona config file into `cwd`. Refuses absolute paths or
149
+ * paths that escape `cwd`. Returns handles the caller can pass to
150
+ * {@link restorePersonaConfigFiles}.
151
+ */
152
+ export declare function materializePersonaConfigFiles(cwd: string, files: readonly InteractiveConfigFile[]): MaterializedConfigFile[];
153
+ /**
154
+ * Restore the original state of files written by
155
+ * {@link materializePersonaConfigFiles}. Files that did not exist before
156
+ * are removed; files that did exist are written back to their prior
157
+ * contents. Errors are swallowed — restore is best-effort cleanup.
158
+ */
159
+ export declare function restorePersonaConfigFiles(writes: readonly MaterializedConfigFile[]): void;
160
+ //# sourceMappingURL=personas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"personas.d.ts","sourceRoot":"","sources":["../src/personas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Persona loading and translation.
3
+ *
4
+ * A persona is a JSON file that describes a pre-configured agent: which
5
+ * harness (CLI) to use, which model, what system prompt to inject, plus
6
+ * optional MCP servers and permission flags. Personas live in
7
+ * `<cwd>/agentworkforce/personas`, the AgentWorkforce home directory, or
8
+ * any directory the caller passes explicitly.
9
+ *
10
+ * Translation from a resolved persona to `{bin, args}` delegates to
11
+ * `@agentworkforce/harness-kit#buildInteractiveSpec`, so relay always
12
+ * produces the same launch args the AgentWorkforce CLI does.
13
+ *
14
+ * The schema mirrors the AgentWorkforce persona format
15
+ * (see https://github.com/AgentWorkforce/workforce). Skills installation,
16
+ * mount policy, sidecar markdown, input rendering, and routing profiles
17
+ * are deliberately not handled here — callers needing those should use
18
+ * the `agentworkforce` CLI directly.
19
+ */
20
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from 'node:path';
23
+ import { buildInteractiveSpec, } from '@agentworkforce/harness-kit';
24
+ import { HARNESS_VALUES, PERSONA_TIERS, } from '@agentworkforce/workload-router';
25
+ export { HARNESS_VALUES, PERSONA_TIERS };
26
+ // ── Default search dirs ────────────────────────────────────────────────────
27
+ function expandHome(p) {
28
+ if (p === '~')
29
+ return homedir();
30
+ if (p.startsWith('~/') || p.startsWith('~\\'))
31
+ return join(homedir(), p.slice(2));
32
+ return p;
33
+ }
34
+ /**
35
+ * The default cascade. First match wins for a given persona id.
36
+ *
37
+ * 1. `<cwd>/agentworkforce/personas` (the path most projects pick)
38
+ * 2. `<cwd>/.agentworkforce/workforce/personas` (workforce CLI's project default)
39
+ * 3. `<cwd>/agentworkforce/workforce/personas` (alt workforce layout)
40
+ * 4. `$AGENT_WORKFORCE_HOME/personas` if set, else `~/.agentworkforce/workforce/personas`
41
+ */
42
+ export function defaultPersonaSearchDirs(cwd = process.cwd()) {
43
+ const dirs = [
44
+ join(cwd, 'agentworkforce', 'personas'),
45
+ join(cwd, '.agentworkforce', 'workforce', 'personas'),
46
+ join(cwd, 'agentworkforce', 'workforce', 'personas'),
47
+ ];
48
+ const home = process.env.AGENT_WORKFORCE_HOME?.trim();
49
+ if (home) {
50
+ dirs.push(join(expandHome(home), 'personas'));
51
+ }
52
+ else {
53
+ dirs.push(join(homedir(), '.agentworkforce', 'workforce', 'personas'));
54
+ }
55
+ return dirs;
56
+ }
57
+ function effectiveSearchDirs(options) {
58
+ const cwd = options.cwd ?? process.cwd();
59
+ const base = options.searchDirs
60
+ ? options.searchDirs.map((d) => normalizeDir(d, cwd))
61
+ : defaultPersonaSearchDirs(cwd);
62
+ const extras = (options.extraDirs ?? []).map((d) => normalizeDir(d, cwd));
63
+ return dedupe([...base, ...extras]);
64
+ }
65
+ function normalizeDir(input, cwd) {
66
+ const expanded = expandHome(input.trim());
67
+ return isAbsolute(expanded) ? resolvePath(expanded) : resolvePath(cwd, expanded);
68
+ }
69
+ function dedupe(items) {
70
+ const seen = new Set();
71
+ const out = [];
72
+ for (const item of items) {
73
+ if (seen.has(item))
74
+ continue;
75
+ seen.add(item);
76
+ out.push(item);
77
+ }
78
+ return out;
79
+ }
80
+ // ── Discovery ──────────────────────────────────────────────────────────────
81
+ /**
82
+ * List every persona discoverable across the search-dir cascade. When the
83
+ * same id appears in multiple dirs, only the first match (by cascade order)
84
+ * is returned.
85
+ */
86
+ export function listPersonas(options = {}) {
87
+ const dirs = effectiveSearchDirs(options);
88
+ const byId = new Map();
89
+ for (const dir of dirs) {
90
+ if (!existsSync(dir))
91
+ continue;
92
+ let entries;
93
+ try {
94
+ entries = readdirSync(dir);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ for (const file of entries) {
100
+ if (!file.endsWith('.json'))
101
+ continue;
102
+ const path = join(dir, file);
103
+ let spec;
104
+ try {
105
+ // Single read avoids a TOCTOU between stat and readFileSync — if the
106
+ // entry is a directory, vanished, or unreadable we skip it.
107
+ spec = parsePersonaFile(JSON.parse(readFileSync(path, 'utf8')), path);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (!byId.has(spec.id)) {
113
+ byId.set(spec.id, { id: spec.id, path, spec });
114
+ }
115
+ }
116
+ }
117
+ return [...byId.values()];
118
+ }
119
+ /**
120
+ * Find a persona file by id across the search-dir cascade.
121
+ * Returns undefined if not found.
122
+ */
123
+ export function findPersona(id, options = {}) {
124
+ const dirs = effectiveSearchDirs(options);
125
+ for (const dir of dirs) {
126
+ if (!existsSync(dir))
127
+ continue;
128
+ const candidate = join(dir, `${id}.json`);
129
+ let candidateBytes;
130
+ try {
131
+ // Single read avoids a stat/read TOCTOU. ENOENT (file missing) falls
132
+ // through to a directory scan for personas with mismatched filenames;
133
+ // any other read failure or parse failure on a convention-named file
134
+ // surfaces directly so a typo in the JSON isn't silently treated as
135
+ // "persona not found".
136
+ candidateBytes = readFileSync(candidate, 'utf8');
137
+ }
138
+ catch (err) {
139
+ if (err.code !== 'ENOENT')
140
+ throw err;
141
+ }
142
+ if (candidateBytes !== undefined) {
143
+ const spec = parsePersonaFile(JSON.parse(candidateBytes), candidate);
144
+ if (spec.id === id)
145
+ return { id, path: candidate, spec };
146
+ }
147
+ let entries;
148
+ try {
149
+ entries = readdirSync(dir);
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ for (const file of entries) {
155
+ if (!file.endsWith('.json'))
156
+ continue;
157
+ const path = join(dir, file);
158
+ try {
159
+ const spec = parsePersonaFile(JSON.parse(readFileSync(path, 'utf8')), path);
160
+ if (spec.id === id)
161
+ return { id, path, spec };
162
+ }
163
+ catch {
164
+ continue;
165
+ }
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+ // ── Resolution ─────────────────────────────────────────────────────────────
171
+ /**
172
+ * Load and resolve a persona by id. Searches the cascade, applies the
173
+ * chosen tier, and resolves a single level of `extends` against the same
174
+ * cascade. Throws if the persona is missing required fields for the tier.
175
+ */
176
+ export function loadPersona(id, options = {}) {
177
+ const discovered = findPersona(id, options);
178
+ if (!discovered) {
179
+ const dirs = effectiveSearchDirs(options);
180
+ throw new Error(`Persona "${id}" not found. Searched:\n ${dirs.join('\n ')}\n` +
181
+ 'Set searchDirs / extraDirs to include the directory containing the persona file.');
182
+ }
183
+ const tier = options.tier ?? 'best';
184
+ let spec = discovered.spec;
185
+ if (spec.extends) {
186
+ const base = findPersona(spec.extends, options);
187
+ if (!base) {
188
+ throw new Error(`Persona "${id}" extends "${spec.extends}" but the base could not be found in the search cascade.`);
189
+ }
190
+ spec = mergeSpecs(base.spec, spec);
191
+ }
192
+ const tierSpec = spec.tiers?.[tier];
193
+ const harness = (tierSpec?.harness ?? spec.harness);
194
+ const model = tierSpec?.model ?? spec.model;
195
+ const systemPrompt = tierSpec?.systemPrompt ?? spec.systemPrompt;
196
+ if (!harness) {
197
+ throw new Error(`Persona "${id}" tier "${tier}" has no harness; set tiers.${tier}.harness or top-level harness.`);
198
+ }
199
+ if (!HARNESS_VALUES.includes(harness)) {
200
+ throw new Error(`Persona "${id}" tier "${tier}" uses unsupported harness "${String(harness)}". ` +
201
+ `Supported: ${HARNESS_VALUES.join(', ')}.`);
202
+ }
203
+ if (!model) {
204
+ throw new Error(`Persona "${id}" tier "${tier}" has no model; set tiers.${tier}.model or top-level model.`);
205
+ }
206
+ if (!systemPrompt) {
207
+ throw new Error(`Persona "${id}" tier "${tier}" has no systemPrompt; set tiers.${tier}.systemPrompt or top-level systemPrompt.`);
208
+ }
209
+ return {
210
+ id: spec.id,
211
+ source: discovered.path,
212
+ tier,
213
+ harness,
214
+ model,
215
+ systemPrompt,
216
+ description: spec.description,
217
+ permissions: spec.permissions,
218
+ mcpServers: spec.mcpServers,
219
+ };
220
+ }
221
+ // ── Merge (extends) ────────────────────────────────────────────────────────
222
+ function mergeSpecs(base, override) {
223
+ const tiers = {};
224
+ for (const tier of PERSONA_TIERS) {
225
+ const baseTier = base.tiers?.[tier];
226
+ const overrideTier = override.tiers?.[tier];
227
+ if (overrideTier || baseTier) {
228
+ tiers[tier] = { ...(baseTier ?? {}), ...(overrideTier ?? {}) };
229
+ }
230
+ }
231
+ return {
232
+ id: override.id,
233
+ intent: override.intent ?? base.intent,
234
+ description: override.description ?? base.description,
235
+ tags: override.tags ?? base.tags,
236
+ systemPrompt: override.systemPrompt ?? base.systemPrompt,
237
+ harness: override.harness ?? base.harness,
238
+ model: override.model ?? base.model,
239
+ permissions: mergePermissions(base.permissions, override.permissions),
240
+ mcpServers: { ...(base.mcpServers ?? {}), ...(override.mcpServers ?? {}) },
241
+ tiers: Object.keys(tiers).length > 0 ? tiers : undefined,
242
+ };
243
+ }
244
+ function mergePermissions(base, override) {
245
+ if (!base && !override)
246
+ return undefined;
247
+ const allow = dedupe([...(base?.allow ?? []), ...(override?.allow ?? [])]);
248
+ const deny = dedupe([...(base?.deny ?? []), ...(override?.deny ?? [])]);
249
+ return {
250
+ ...(allow.length > 0 ? { allow } : {}),
251
+ ...(deny.length > 0 ? { deny } : {}),
252
+ ...(override?.mode ?? base?.mode ? { mode: override?.mode ?? base?.mode } : {}),
253
+ };
254
+ }
255
+ // ── Validation ─────────────────────────────────────────────────────────────
256
+ function isPlainObject(value) {
257
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
258
+ }
259
+ function parsePersonaFile(value, source) {
260
+ if (!isPlainObject(value)) {
261
+ throw new Error(`${source}: persona must be a JSON object`);
262
+ }
263
+ if (typeof value.id !== 'string' || !value.id.trim()) {
264
+ throw new Error(`${source}: persona.id must be a non-empty string`);
265
+ }
266
+ // Validate harness values up front so a typo in the file fails at load time
267
+ // rather than at spawn — the runtime check in loadPersona stays as a
268
+ // defense-in-depth guard for callers that bypass parsing.
269
+ const topHarness = value.harness;
270
+ if (topHarness !== undefined && !isValidHarness(topHarness)) {
271
+ throw new Error(`${source}: persona.harness must be one of: ${HARNESS_VALUES.join(', ')} (got ${JSON.stringify(topHarness)})`);
272
+ }
273
+ const tiers = value.tiers;
274
+ if (tiers !== undefined) {
275
+ if (!isPlainObject(tiers)) {
276
+ throw new Error(`${source}: persona.tiers must be an object if provided`);
277
+ }
278
+ for (const [tierName, tierSpec] of Object.entries(tiers)) {
279
+ if (!PERSONA_TIERS.includes(tierName))
280
+ continue; // unknown tier names are ignored
281
+ if (!isPlainObject(tierSpec))
282
+ continue;
283
+ const harness = tierSpec.harness;
284
+ if (harness !== undefined && !isValidHarness(harness)) {
285
+ throw new Error(`${source}: persona.tiers.${tierName}.harness must be one of: ${HARNESS_VALUES.join(', ')} (got ${JSON.stringify(harness)})`);
286
+ }
287
+ }
288
+ }
289
+ return value;
290
+ }
291
+ function isValidHarness(value) {
292
+ return typeof value === 'string' && HARNESS_VALUES.includes(value);
293
+ }
294
+ // ── Translation: persona → spawn args ──────────────────────────────────────
295
+ /**
296
+ * Translate a resolved persona into the bin/args spawnPty needs. Delegates
297
+ * to {@link buildInteractiveSpec} from `@agentworkforce/harness-kit` so
298
+ * relay produces the same launch shape the AgentWorkforce CLI does.
299
+ */
300
+ export function buildPersonaSpawnSpec(persona) {
301
+ const input = {
302
+ harness: persona.harness,
303
+ personaId: persona.id,
304
+ model: persona.model,
305
+ systemPrompt: persona.systemPrompt,
306
+ ...(persona.mcpServers ? { mcpServers: persona.mcpServers } : {}),
307
+ ...(persona.permissions ? { permissions: persona.permissions } : {}),
308
+ };
309
+ const spec = buildInteractiveSpec(input);
310
+ return {
311
+ cli: spec.bin,
312
+ model: persona.model,
313
+ args: [...spec.args],
314
+ initialPrompt: spec.initialPrompt,
315
+ configFiles: [...spec.configFiles],
316
+ warnings: [...spec.warnings],
317
+ };
318
+ }
319
+ /**
320
+ * Codex has no system-prompt flag, so the persona's instructions must ride
321
+ * on the task. Combines them in the same shape the agentworkforce
322
+ * harness-kit uses for non-interactive codex runs.
323
+ */
324
+ export function composePersonaTask(spec, userTask) {
325
+ if (!spec.initialPrompt)
326
+ return userTask;
327
+ if (!userTask)
328
+ return spec.initialPrompt;
329
+ return `${spec.initialPrompt}\n\nUser task:\n${userTask}`;
330
+ }
331
+ /**
332
+ * Write each persona config file into `cwd`. Refuses absolute paths or
333
+ * paths that escape `cwd`. Returns handles the caller can pass to
334
+ * {@link restorePersonaConfigFiles}.
335
+ */
336
+ export function materializePersonaConfigFiles(cwd, files) {
337
+ const out = [];
338
+ const cwdAbs = resolvePath(cwd);
339
+ for (const file of files) {
340
+ if (!file.path)
341
+ throw new Error('persona config file path must be non-empty');
342
+ if (isAbsolute(file.path)) {
343
+ throw new Error(`persona config file path must be relative: ${file.path}`);
344
+ }
345
+ const target = resolvePath(cwd, file.path);
346
+ // Use path.relative for separator-agnostic containment so Windows paths
347
+ // (`C:\proj\opencode.json`) aren't falsely rejected by a hardcoded '/' check.
348
+ const rel = relative(cwdAbs, target);
349
+ if (rel.startsWith('..') || (isAbsolute(rel) && rel !== '')) {
350
+ throw new Error(`persona config file path escapes cwd: ${file.path}`);
351
+ }
352
+ if (rel.split(sep).some((segment) => segment === '..')) {
353
+ throw new Error(`persona config file path escapes cwd: ${file.path}`);
354
+ }
355
+ // Single read with ENOENT detection avoids a TOCTOU between `existsSync`
356
+ // and `readFileSync`. Any other read error (permissions, EISDIR) bubbles up
357
+ // — the caller can decide whether to retry or surface to the user.
358
+ let existed = true;
359
+ let previous;
360
+ try {
361
+ previous = readFileSync(target, 'utf8');
362
+ }
363
+ catch (err) {
364
+ if (err.code === 'ENOENT') {
365
+ existed = false;
366
+ }
367
+ else {
368
+ throw err;
369
+ }
370
+ }
371
+ mkdirSync(dirname(target), { recursive: true });
372
+ writeFileSync(target, file.contents, 'utf8');
373
+ out.push({ path: target, existed, ...(previous !== undefined ? { previous } : {}) });
374
+ }
375
+ return out;
376
+ }
377
+ /**
378
+ * Restore the original state of files written by
379
+ * {@link materializePersonaConfigFiles}. Files that did not exist before
380
+ * are removed; files that did exist are written back to their prior
381
+ * contents. Errors are swallowed — restore is best-effort cleanup.
382
+ */
383
+ export function restorePersonaConfigFiles(writes) {
384
+ for (const write of [...writes].reverse()) {
385
+ try {
386
+ if (write.existed) {
387
+ writeFileSync(write.path, write.previous ?? '', 'utf8');
388
+ }
389
+ else {
390
+ rmSync(write.path, { force: true });
391
+ }
392
+ }
393
+ catch (err) {
394
+ // Best-effort: a failed restore shouldn't break the spawn lifecycle, but
395
+ // it can leave a stale opencode.json behind, so surface the failure.
396
+ const msg = err?.message ?? String(err);
397
+ console.warn(`[personas] failed to restore ${write.path}: ${msg}`);
398
+ }
399
+ }
400
+ }
401
+ //# sourceMappingURL=personas.js.map