@akagilnc/pi-workflow-roles 0.1.3722 → 0.1.3733

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.
@@ -1,21 +1,35 @@
1
1
  /**
2
2
  * One ACP host description. Every host-specific value the generic ACP adapter
3
- * needs — binary location, argv shape, resume verb, binding filename, child env
4
- * — is data here; the lifecycle in role-turn-host.ts stays one copy (#732).
3
+ * needs — binary location, argv shape, resume verb, binding filename, child env,
4
+ * optional seat-profile soul — is data here; the lifecycle in role-turn-host.ts
5
+ * stays one copy (#732).
5
6
  */
6
7
  import { join } from "node:path";
7
8
  /** Absolute agent binary for one operator home. */
8
9
  export function resolveAcpBinary(description, operatorHome) {
9
10
  return join(operatorHome, ...description.binaryFromHome);
10
11
  }
11
- /** Stdio argv: prefix, optional model/thinking flag pairs, suffix. */
12
- export function acpStdioArgs(description, model) {
12
+ /** Stdio argv: optional profile flag, thinking flag (before the subcommand),
13
+ * prefix, optional model flag pair, suffix. */
14
+ export function acpStdioArgs(description, model, seat) {
13
15
  const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
14
16
  const pair = (flag, value) => flag === undefined || value === undefined ? [] : [flag, value];
15
17
  return [
18
+ ...pair(description.seatProfileSoul?.flag, seat?.profileName),
19
+ ...pair(thinkingFlag, model?.thinking),
16
20
  ...prefix,
17
21
  ...pair(modelFlag, model?.model),
18
- ...pair(thinkingFlag, model?.thinking),
19
22
  ...suffix,
20
23
  ];
21
24
  }
25
+ /**
26
+ * The modelId the host addresses the seat model by.
27
+ * "argv" hosts address by bare model name; "set_model" hosts address by the
28
+ * `provider:model` modelId the ACP catalog exposes (seat provider after host
29
+ * alias projection, concatenated — never a package provider map).
30
+ */
31
+ export function acpModelId(modelPassing, model) {
32
+ if (model?.model === undefined)
33
+ return undefined;
34
+ return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
35
+ }
@@ -164,7 +164,7 @@ function normalizeReviewComment(raw) {
164
164
  function createGhApiRunner(options = {}) {
165
165
  const spawnImpl = options.spawnImpl ?? spawn;
166
166
  return async (args, runOptions = {}) => {
167
- return await new Promise((resolve19, reject) => {
167
+ return await new Promise((resolve20, reject) => {
168
168
  const signal = runOptions.signal;
169
169
  if (signal?.aborted) {
170
170
  reject(signal.reason ?? new Error("aborted"));
@@ -237,11 +237,11 @@ function createGhApiRunner(options = {}) {
237
237
  const value = line2.slice(idx + 1).trim();
238
238
  headers[name] = value;
239
239
  }
240
- resolve19({ status, headers, bodyText });
240
+ resolve20({ status, headers, bodyText });
241
241
  return;
242
242
  }
243
243
  if (code === 0) {
244
- resolve19({ status: 200, headers: {}, bodyText: stdout });
244
+ resolve20({ status: 200, headers: {}, bodyText: stdout });
245
245
  return;
246
246
  }
247
247
  const failure2 = new Error(
@@ -1227,7 +1227,7 @@ async function spawnEngineDetourOnce(input) {
1227
1227
  }
1228
1228
  const command = input.argv[0];
1229
1229
  const args = input.argv.slice(1);
1230
- return await new Promise((resolve19, reject) => {
1230
+ return await new Promise((resolve20, reject) => {
1231
1231
  let settled = false;
1232
1232
  const signal = input.signal;
1233
1233
  const child = spawn2(command, args, {
@@ -1257,7 +1257,7 @@ async function spawnEngineDetourOnce(input) {
1257
1257
  if (signal !== void 0) {
1258
1258
  signal.removeEventListener("abort", onAbort);
1259
1259
  }
1260
- resolve19(result);
1260
+ resolve20(result);
1261
1261
  };
1262
1262
  const onAbort = () => {
1263
1263
  fail5(signal !== void 0 ? abortReasonError(signal) : new Error("aborted"));
@@ -8398,6 +8398,7 @@ var init_host_descriptions = __esm({
8398
8398
  suffix: Object.freeze(["stdio"]),
8399
8399
  modelFlag: "--model"
8400
8400
  }),
8401
+ modelPassing: "argv",
8401
8402
  boundResume: "session/load",
8402
8403
  sessionBindingFile: "grok-acp-session.json",
8403
8404
  childEnv: Object.freeze({
@@ -8405,6 +8406,31 @@ var init_host_descriptions = __esm({
8405
8406
  GROK_MEMORY: "0",
8406
8407
  GROK_SUBAGENTS: "0"
8407
8408
  })
8409
+ }),
8410
+ /**
8411
+ * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
8412
+ * Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
8413
+ * (seat table provider + model concatenated). Reasoning is the global
8414
+ * `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
8415
+ * (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
8416
+ */
8417
+ "hermes": Object.freeze({
8418
+ binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
8419
+ argv: Object.freeze({
8420
+ prefix: Object.freeze(["acp"]),
8421
+ suffix: Object.freeze([]),
8422
+ thinkingFlag: "--reasoning"
8423
+ }),
8424
+ modelPassing: "set_model",
8425
+ boundResume: "session/load",
8426
+ sessionBindingFile: "hermes-acp-session.json",
8427
+ childEnv: Object.freeze({}),
8428
+ seatProfileSoul: Object.freeze({
8429
+ flag: "-p",
8430
+ namePrefix: "ak-",
8431
+ profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
8432
+ soulFileName: "SOUL.md"
8433
+ })
8408
8434
  })
8409
8435
  });
8410
8436
  }
@@ -11451,12 +11477,12 @@ function createSystemCollectorClock() {
11451
11477
  return {
11452
11478
  wallNow: () => /* @__PURE__ */ new Date(),
11453
11479
  monoNow: () => Number(process.hrtime.bigint() - start) / 1e6,
11454
- sleep: (ms, signal) => new Promise((resolve19, reject) => {
11480
+ sleep: (ms, signal) => new Promise((resolve20, reject) => {
11455
11481
  if (signal?.aborted) {
11456
11482
  reject(signal.reason ?? new Error("aborted"));
11457
11483
  return;
11458
11484
  }
11459
- const timer = setTimeout(resolve19, ms);
11485
+ const timer = setTimeout(resolve20, ms);
11460
11486
  const onAbort = () => {
11461
11487
  clearTimeout(timer);
11462
11488
  reject(signal?.reason ?? new Error("aborted"));
@@ -14877,10 +14903,10 @@ function presentFailureTerminal(terminal, io) {
14877
14903
  }
14878
14904
  function defaultNavigatorGraceSleep() {
14879
14905
  let timer;
14880
- const sleep = ((ms) => new Promise((resolve19) => {
14906
+ const sleep = ((ms) => new Promise((resolve20) => {
14881
14907
  timer = setTimeout(() => {
14882
14908
  timer = void 0;
14883
- resolve19();
14909
+ resolve20();
14884
14910
  }, ms);
14885
14911
  }));
14886
14912
  sleep.cancel = () => {
@@ -14892,7 +14918,7 @@ function defaultNavigatorGraceSleep() {
14892
14918
  return sleep;
14893
14919
  }
14894
14920
  function raceNavigatorGrace(work, graceMs = NAVIGATOR_POST_ROLE_GRACE_MS, sleep = defaultNavigatorGraceSleep()) {
14895
- return new Promise((resolve19, reject) => {
14921
+ return new Promise((resolve20, reject) => {
14896
14922
  let settled = false;
14897
14923
  const finish = (action) => {
14898
14924
  if (settled) return;
@@ -14901,11 +14927,11 @@ function raceNavigatorGrace(work, graceMs = NAVIGATOR_POST_ROLE_GRACE_MS, sleep
14901
14927
  action();
14902
14928
  };
14903
14929
  void work.then(
14904
- (value) => finish(() => resolve19({ status: "done", value })),
14930
+ (value) => finish(() => resolve20({ status: "done", value })),
14905
14931
  (error) => finish(() => reject(error))
14906
14932
  );
14907
14933
  void sleep(graceMs).then(() => {
14908
- finish(() => resolve19({ status: "timeout" }));
14934
+ finish(() => resolve20({ status: "timeout" }));
14909
14935
  });
14910
14936
  });
14911
14937
  }
@@ -24155,7 +24181,7 @@ import { mkdtemp as mkdtemp3, rm as rm3 } from "node:fs/promises";
24155
24181
  import { tmpdir as tmpdir3 } from "node:os";
24156
24182
  import { join as join32 } from "node:path";
24157
24183
  async function runCommand(command, args, options = {}) {
24158
- return await new Promise((resolve19, reject) => {
24184
+ return await new Promise((resolve20, reject) => {
24159
24185
  const child = spawn4(command, args, { ...options.cwd === void 0 ? {} : { cwd: options.cwd }, stdio: ["ignore", "pipe", "pipe"], signal: options.signal });
24160
24186
  let stdout = "", stderr = "";
24161
24187
  child.stdout.setEncoding("utf8").on("data", (chunk) => {
@@ -24175,7 +24201,7 @@ async function runCommand(command, args, options = {}) {
24175
24201
  const actual = code ?? 1;
24176
24202
  if ((options.allowedCodes ?? [0]).includes(actual)) {
24177
24203
  settled = true;
24178
- resolve19({ stdout, stderr, code: actual });
24204
+ resolve20({ stdout, stderr, code: actual });
24179
24205
  } else fail5(void 0, code, signal);
24180
24206
  });
24181
24207
  });
@@ -24639,16 +24665,21 @@ import { join as join33 } from "node:path";
24639
24665
  function resolveAcpBinary(description, operatorHome) {
24640
24666
  return join33(operatorHome, ...description.binaryFromHome);
24641
24667
  }
24642
- function acpStdioArgs(description, model) {
24668
+ function acpStdioArgs(description, model, seat) {
24643
24669
  const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
24644
24670
  const pair = (flag, value) => flag === void 0 || value === void 0 ? [] : [flag, value];
24645
24671
  return [
24672
+ ...pair(description.seatProfileSoul?.flag, seat?.profileName),
24673
+ ...pair(thinkingFlag, model?.thinking),
24646
24674
  ...prefix,
24647
24675
  ...pair(modelFlag, model?.model),
24648
- ...pair(thinkingFlag, model?.thinking),
24649
24676
  ...suffix
24650
24677
  ];
24651
24678
  }
24679
+ function acpModelId(modelPassing, model) {
24680
+ if (model?.model === void 0) return void 0;
24681
+ return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
24682
+ }
24652
24683
 
24653
24684
  // src/acp-host/role-envelope.ts
24654
24685
  init_gatekeeper_pass_envelope();
@@ -24750,8 +24781,8 @@ function connectAcpStdio(options) {
24750
24781
  request(method, params) {
24751
24782
  if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "ACP connection is closed"));
24752
24783
  const id = ++nextId;
24753
- return new Promise((resolve19, reject) => {
24754
- pending.set(id, { resolve: resolve19, reject });
24784
+ return new Promise((resolve20, reject) => {
24785
+ pending.set(id, { resolve: resolve20, reject });
24755
24786
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
24756
24787
  `, (error) => {
24757
24788
  if (error === null || error === void 0) return;
@@ -24775,7 +24806,7 @@ function connectAcpStdio(options) {
24775
24806
  settleClosed(acpError("acp-connection-closed", "ACP connection is closed"));
24776
24807
  child.stdin.end();
24777
24808
  child.kill("SIGTERM");
24778
- await new Promise((resolve19) => child.once("close", () => resolve19()));
24809
+ await new Promise((resolve20) => child.once("close", () => resolve20()));
24779
24810
  }
24780
24811
  });
24781
24812
  }
@@ -24786,6 +24817,7 @@ function createAcpRoleTurnHost(config) {
24786
24817
  const execution = serial.then(async () => {
24787
24818
  const continuation = request.continuation;
24788
24819
  const prepared = await config.prepare(request);
24820
+ const systemPromptOverride = renderAcpSystemPromptOverride(prepared.systemPrompt);
24789
24821
  let connection;
24790
24822
  let sessionId;
24791
24823
  let accepted = false;
@@ -24801,40 +24833,47 @@ function createAcpRoleTurnHost(config) {
24801
24833
  const initializeMeta = initialized._meta;
24802
24834
  const modelState = initializeMeta?.modelState;
24803
24835
  const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : void 0;
24804
- if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === request.model?.model)) {
24836
+ if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === acpModelId(config.modelPassing, request.model))) {
24805
24837
  return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
24806
24838
  provider: request.model.provider,
24807
24839
  model: request.model.model
24808
24840
  });
24809
24841
  }
24810
24842
  const priorNativePaths = continuation.kind === "resume" ? request.hostTransition?.priorNativePaths : void 0;
24843
+ const loadConnection = connection;
24844
+ const sessionBindParams = {
24845
+ cwd: request.cwd,
24846
+ mcpServers: prepared.mcpServers,
24847
+ _meta: { systemPromptOverride, yoloMode: false }
24848
+ };
24849
+ const loadSession = async (bindSessionId) => {
24850
+ const loaded = await loadConnection.request("session/load", {
24851
+ sessionId: bindSessionId,
24852
+ ...sessionBindParams
24853
+ });
24854
+ return typeof loaded.sessionId === "string" && loaded.sessionId !== "" ? loaded.sessionId : bindSessionId;
24855
+ };
24811
24856
  if (continuation.kind === "resume" && config.boundResume === "session/load") {
24812
24857
  const boundSessionId = await config.sessionIdentity.load(request.principal);
24813
24858
  if (boundSessionId !== void 0 && boundSessionId !== "") {
24814
- const loaded = await connection.request("session/load", {
24815
- sessionId: boundSessionId,
24816
- cwd: request.cwd,
24817
- mcpServers: prepared.mcpServers,
24818
- _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false }
24819
- });
24820
- sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== "" ? loaded.sessionId : boundSessionId;
24859
+ sessionId = await loadSession(boundSessionId);
24821
24860
  }
24822
24861
  }
24823
24862
  if (sessionId === void 0) {
24824
- const session = await connection.request(
24825
- "session/new",
24826
- {
24827
- cwd: request.cwd,
24828
- mcpServers: prepared.mcpServers,
24829
- _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false }
24830
- }
24831
- );
24863
+ const session = await connection.request("session/new", sessionBindParams);
24832
24864
  sessionId = typeof session.sessionId === "string" ? session.sessionId : void 0;
24833
24865
  if (sessionId === void 0 || sessionId === "") {
24834
24866
  return failure("session", "AcpSessionFailure", "session-id-missing");
24835
24867
  }
24836
24868
  await config.sessionIdentity.bind(request.principal, sessionId);
24837
24869
  }
24870
+ if (config.modelPassing === "set_model" && request.model !== void 0 && sessionId !== void 0) {
24871
+ await connection.request("session/set_model", {
24872
+ sessionId,
24873
+ modelId: acpModelId(config.modelPassing, request.model)
24874
+ });
24875
+ sessionId = await loadSession(sessionId);
24876
+ }
24838
24877
  let prompt = priorNativePaths !== void 0 && priorNativePaths.length > 0 ? `${prepared.prompt}
24839
24878
  ${priorNativePaths.join("\n")}` : prepared.prompt;
24840
24879
  const abortSignal = request.signal === void 0 ? prepared.abortSignal : prepared.abortSignal === void 0 ? request.signal : AbortSignal.any([prepared.abortSignal, request.signal]);
@@ -24845,7 +24884,7 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
24845
24884
  }
24846
24885
  const promptRequest = activeConnection.request("session/prompt", params);
24847
24886
  if (abortSignal === void 0) return promptRequest;
24848
- return new Promise((resolve19, reject) => {
24887
+ return new Promise((resolve20, reject) => {
24849
24888
  let settled = false;
24850
24889
  const onAbort = () => {
24851
24890
  if (settled) return;
@@ -24860,7 +24899,7 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
24860
24899
  if (settled) return;
24861
24900
  settled = true;
24862
24901
  abortSignal.removeEventListener("abort", onAbort);
24863
- resolve19(value);
24902
+ resolve20(value);
24864
24903
  },
24865
24904
  (error) => {
24866
24905
  if (settled) return;
@@ -24874,12 +24913,9 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
24874
24913
  for (let attempt = 0; attempt < 8; attempt += 1) {
24875
24914
  let result;
24876
24915
  try {
24877
- const promptParts = [
24878
- { type: "text", text: prompt }
24879
- ];
24880
24916
  result = await promptOrAbort({
24881
24917
  sessionId,
24882
- prompt: promptParts
24918
+ prompt: [{ type: "text", text: prompt }]
24883
24919
  });
24884
24920
  } catch (error) {
24885
24921
  if (typeof error === "object" && error !== null && error.code === "host-aborted") {
@@ -24984,11 +25020,11 @@ function projectAcpActivationFlags(request) {
24984
25020
  return flags;
24985
25021
  }
24986
25022
  async function listen(server, path) {
24987
- await new Promise((resolve19, reject) => {
25023
+ await new Promise((resolve20, reject) => {
24988
25024
  server.once("error", reject);
24989
25025
  server.listen(path, () => {
24990
25026
  server.off("error", reject);
24991
- resolve19();
25027
+ resolve20();
24992
25028
  });
24993
25029
  });
24994
25030
  }
@@ -25379,8 +25415,8 @@ async function prepareAcpRoleEnvelope(options) {
25379
25415
  try {
25380
25416
  const closeAll = server.closeAllConnections;
25381
25417
  if (typeof closeAll === "function") closeAll.call(server);
25382
- await new Promise((resolve19, reject) => {
25383
- server.close((error) => error ? reject(error) : resolve19());
25418
+ await new Promise((resolve20, reject) => {
25419
+ server.close((error) => error ? reject(error) : resolve20());
25384
25420
  });
25385
25421
  } catch (error) {
25386
25422
  cleanupFailures.push(error);
@@ -25488,11 +25524,70 @@ async function prepareAcpRoleEnvelope(options) {
25488
25524
  }
25489
25525
  }
25490
25526
 
25527
+ // src/acp-host/seat-profile-soul.ts
25528
+ import { constants as constants3 } from "node:fs";
25529
+ import { access as access4, copyFile, lstat as lstat6, mkdir as mkdir6, readlink, symlink, unlink as unlink4 } from "node:fs/promises";
25530
+ import { dirname as dirname18, join as join35, relative as relative3, resolve as resolve19 } from "node:path";
25531
+ function seatProfileName(spec, role) {
25532
+ return `${spec.namePrefix}${role}`;
25533
+ }
25534
+ function packageRoleSoulPath(packageRoot, role) {
25535
+ return join35(packageRoot, "souls", `${role}.md`);
25536
+ }
25537
+ async function pathExists(path) {
25538
+ try {
25539
+ await access4(path, constants3.F_OK);
25540
+ return true;
25541
+ } catch {
25542
+ return false;
25543
+ }
25544
+ }
25545
+ async function ensureSeatProfileSoul(options) {
25546
+ const { spec, operatorHome, packageRoot, role } = options;
25547
+ const profileName = seatProfileName(spec, role);
25548
+ const soulTarget = resolve19(packageRoleSoulPath(packageRoot, role));
25549
+ if (!await pathExists(soulTarget)) {
25550
+ throw new Error(`packaged role soul missing: ${soulTarget}`);
25551
+ }
25552
+ const profilesRoot = join35(operatorHome, ...spec.profilesRootFromHome);
25553
+ const profileDir = join35(profilesRoot, profileName);
25554
+ const hostRoot = dirname18(profilesRoot);
25555
+ const soulPath = join35(profileDir, spec.soulFileName);
25556
+ if (!await pathExists(profileDir)) {
25557
+ await mkdir6(profileDir, { recursive: true });
25558
+ for (const name of ["auth.json", ".env", "config.yaml"]) {
25559
+ const source = join35(hostRoot, name);
25560
+ if (!await pathExists(source)) continue;
25561
+ await copyFile(source, join35(profileDir, name));
25562
+ }
25563
+ } else {
25564
+ await mkdir6(profileDir, { recursive: true });
25565
+ }
25566
+ const desiredLink = relative3(profileDir, soulTarget);
25567
+ let current;
25568
+ try {
25569
+ const st = await lstat6(soulPath);
25570
+ if (st.isSymbolicLink()) {
25571
+ current = await readlink(soulPath);
25572
+ }
25573
+ } catch {
25574
+ current = void 0;
25575
+ }
25576
+ if (current === desiredLink || current === soulTarget) {
25577
+ return profileName;
25578
+ }
25579
+ if (await pathExists(soulPath) || current !== void 0) {
25580
+ await unlink4(soulPath);
25581
+ }
25582
+ await symlink(desiredLink, soulPath);
25583
+ return profileName;
25584
+ }
25585
+
25491
25586
  // src/acp-host/session-identity.ts
25492
- import { mkdir as mkdir6, readFile as readFile19, rename, writeFile as writeFile9 } from "node:fs/promises";
25493
- import { dirname as dirname18, join as join35 } from "node:path";
25587
+ import { mkdir as mkdir7, readFile as readFile19, rename, writeFile as writeFile9 } from "node:fs/promises";
25588
+ import { dirname as dirname19, join as join36 } from "node:path";
25494
25589
  function createAcpSessionIdentityAuthority(authority, sessionBindingFile) {
25495
- const bindingPath = (principal) => join35(authority.decode(principal).sessionDirectory, sessionBindingFile);
25590
+ const bindingPath = (principal) => join36(authority.decode(principal).sessionDirectory, sessionBindingFile);
25496
25591
  return {
25497
25592
  resolveSessionFile(principal) {
25498
25593
  return authority.decode(principal).sessionFile;
@@ -25511,7 +25606,7 @@ function createAcpSessionIdentityAuthority(authority, sessionBindingFile) {
25511
25606
  },
25512
25607
  async bind(principal, sessionId) {
25513
25608
  const target = bindingPath(principal);
25514
- await mkdir6(dirname18(target), { recursive: true });
25609
+ await mkdir7(dirname19(target), { recursive: true });
25515
25610
  const temporary = `${target}.${process.pid}.tmp`;
25516
25611
  await writeFile9(temporary, `${JSON.stringify({ sessionId })}
25517
25612
  `, { encoding: "utf8", mode: 384 });
@@ -25598,11 +25693,23 @@ function createProductionAcpRoleTurnHost(options) {
25598
25693
  return createComposedAcpRoleTurnHost({
25599
25694
  sessionIdentity: createAcpSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
25600
25695
  boundResume: description.boundResume,
25696
+ modelPassing: description.modelPassing,
25601
25697
  roleRuntimeDependencies: createAcpRoleRuntimeDependencies(packageRoot),
25602
25698
  async connect(request) {
25699
+ const seatProfile = description.seatProfileSoul;
25700
+ const profileName = seatProfile === void 0 ? void 0 : await ensureSeatProfileSoul({
25701
+ spec: seatProfile,
25702
+ operatorHome: request.home,
25703
+ packageRoot,
25704
+ role: request.activation.role
25705
+ });
25603
25706
  return connectAcpStdio({
25604
25707
  binary: resolveAcpBinary(description, request.home),
25605
- args: acpStdioArgs(description, request.model),
25708
+ args: acpStdioArgs(
25709
+ description,
25710
+ request.model,
25711
+ profileName === void 0 ? void 0 : { profileName }
25712
+ ),
25606
25713
  cwd: request.cwd,
25607
25714
  env
25608
25715
  });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Seat-scoped host profile soul delivery (#644 / 拍 2).
3
+ *
4
+ * Hermes has no per-session systemPrompt channel; identity is the profile's
5
+ * SOUL.md. Each seat owns `profiles/<namePrefix><role>/`, and SOUL.md is a
6
+ * symlink to the packaged `souls/<role>.md` (package is the sole soul source).
7
+ */
8
+ import { constants } from "node:fs";
9
+ import { access, copyFile, lstat, mkdir, readlink, symlink, unlink } from "node:fs/promises";
10
+ import { dirname, join, relative, resolve } from "node:path";
11
+ /** Profile id for one seat role. */
12
+ export function seatProfileName(spec, role) {
13
+ return `${spec.namePrefix}${role}`;
14
+ }
15
+ /** Packaged soul path for one role (`souls/<role>.md`). */
16
+ export function packageRoleSoulPath(packageRoot, role) {
17
+ return join(packageRoot, "souls", `${role}.md`);
18
+ }
19
+ async function pathExists(path) {
20
+ try {
21
+ await access(path, constants.F_OK);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Ensure the seat profile directory exists and SOUL.md is a symlink to the
30
+ * packaged role soul. On first create, copy credential surfaces from the host
31
+ * root (parent of the profiles root) so the profile can authenticate without
32
+ * touching the default profile in place. Returns the profile id for argv.
33
+ */
34
+ export async function ensureSeatProfileSoul(options) {
35
+ const { spec, operatorHome, packageRoot, role } = options;
36
+ const profileName = seatProfileName(spec, role);
37
+ const soulTarget = resolve(packageRoleSoulPath(packageRoot, role));
38
+ if (!(await pathExists(soulTarget))) {
39
+ throw new Error(`packaged role soul missing: ${soulTarget}`);
40
+ }
41
+ const profilesRoot = join(operatorHome, ...spec.profilesRootFromHome);
42
+ const profileDir = join(profilesRoot, profileName);
43
+ const hostRoot = dirname(profilesRoot);
44
+ const soulPath = join(profileDir, spec.soulFileName);
45
+ if (!(await pathExists(profileDir))) {
46
+ await mkdir(profileDir, { recursive: true });
47
+ // First-create credential bootstrap only. Never rewrite an existing profile's
48
+ // auth/config; never write into the host root / default profile.
49
+ for (const name of ["auth.json", ".env", "config.yaml"]) {
50
+ const source = join(hostRoot, name);
51
+ if (!(await pathExists(source)))
52
+ continue;
53
+ await copyFile(source, join(profileDir, name));
54
+ }
55
+ }
56
+ else {
57
+ await mkdir(profileDir, { recursive: true });
58
+ }
59
+ const desiredLink = relative(profileDir, soulTarget);
60
+ let current;
61
+ try {
62
+ const st = await lstat(soulPath);
63
+ if (st.isSymbolicLink()) {
64
+ current = await readlink(soulPath);
65
+ }
66
+ }
67
+ catch {
68
+ current = undefined;
69
+ }
70
+ if (current === desiredLink || current === soulTarget) {
71
+ return profileName;
72
+ }
73
+ if (await pathExists(soulPath) || current !== undefined) {
74
+ await unlink(soulPath);
75
+ }
76
+ await symlink(desiredLink, soulPath);
77
+ return profileName;
78
+ }
@@ -10,6 +10,7 @@ export const HOST_DESCRIPTIONS = Object.freeze({
10
10
  suffix: Object.freeze(["stdio"]),
11
11
  modelFlag: "--model",
12
12
  }),
13
+ modelPassing: "argv",
13
14
  boundResume: "session/load",
14
15
  sessionBindingFile: "grok-acp-session.json",
15
16
  childEnv: Object.freeze({
@@ -18,6 +19,31 @@ export const HOST_DESCRIPTIONS = Object.freeze({
18
19
  GROK_SUBAGENTS: "0",
19
20
  }),
20
21
  }),
22
+ /**
23
+ * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
24
+ * Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
25
+ * (seat table provider + model concatenated). Reasoning is the global
26
+ * `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
27
+ * (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
28
+ */
29
+ "hermes": Object.freeze({
30
+ binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
31
+ argv: Object.freeze({
32
+ prefix: Object.freeze(["acp"]),
33
+ suffix: Object.freeze([]),
34
+ thinkingFlag: "--reasoning",
35
+ }),
36
+ modelPassing: "set_model",
37
+ boundResume: "session/load",
38
+ sessionBindingFile: "hermes-acp-session.json",
39
+ childEnv: Object.freeze({}),
40
+ seatProfileSoul: Object.freeze({
41
+ flag: "-p",
42
+ namePrefix: "ak-",
43
+ profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
44
+ soulFileName: "SOUL.md",
45
+ }),
46
+ }),
21
47
  });
22
48
  export function lookupHostDescription(host) {
23
49
  return Object.hasOwn(HOST_DESCRIPTIONS, host) ? HOST_DESCRIPTIONS[host] : undefined;
@@ -15446,6 +15446,7 @@ var init_host_descriptions = __esm({
15446
15446
  suffix: Object.freeze(["stdio"]),
15447
15447
  modelFlag: "--model"
15448
15448
  }),
15449
+ modelPassing: "argv",
15449
15450
  boundResume: "session/load",
15450
15451
  sessionBindingFile: "grok-acp-session.json",
15451
15452
  childEnv: Object.freeze({
@@ -15453,6 +15454,31 @@ var init_host_descriptions = __esm({
15453
15454
  GROK_MEMORY: "0",
15454
15455
  GROK_SUBAGENTS: "0"
15455
15456
  })
15457
+ }),
15458
+ /**
15459
+ * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
15460
+ * Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
15461
+ * (seat table provider + model concatenated). Reasoning is the global
15462
+ * `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
15463
+ * (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
15464
+ */
15465
+ "hermes": Object.freeze({
15466
+ binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
15467
+ argv: Object.freeze({
15468
+ prefix: Object.freeze(["acp"]),
15469
+ suffix: Object.freeze([]),
15470
+ thinkingFlag: "--reasoning"
15471
+ }),
15472
+ modelPassing: "set_model",
15473
+ boundResume: "session/load",
15474
+ sessionBindingFile: "hermes-acp-session.json",
15475
+ childEnv: Object.freeze({}),
15476
+ seatProfileSoul: Object.freeze({
15477
+ flag: "-p",
15478
+ namePrefix: "ak-",
15479
+ profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
15480
+ soulFileName: "SOUL.md"
15481
+ })
15456
15482
  })
15457
15483
  });
15458
15484
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3722",
3
+ "version": "0.1.3733",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * One ACP host description. Every host-specific value the generic ACP adapter
3
- * needs — binary location, argv shape, resume verb, binding filename, child env
4
- * — is data here; the lifecycle in role-turn-host.ts stays one copy (#732).
3
+ * needs — binary location, argv shape, resume verb, binding filename, child env,
4
+ * optional seat-profile soul — is data here; the lifecycle in role-turn-host.ts
5
+ * stays one copy (#732).
5
6
  */
6
7
  import { join } from "node:path";
7
8
 
9
+ import type { SeatProfileSoul } from "./seat-profile-soul.ts";
10
+
8
11
  export type AcpHostDescription = Readonly<{
9
12
  /** Binary path segments relative to the operator home. */
10
13
  binaryFromHome: readonly string[];
@@ -12,13 +15,28 @@ export type AcpHostDescription = Readonly<{
12
15
  prefix: readonly string[];
13
16
  suffix: readonly string[];
14
17
  modelFlag?: string;
18
+ /** CLI flag whose value is the seat thinking level; placed before `prefix`
19
+ * so it lands ahead of the subcommand (hermes global `--reasoning`). */
15
20
  thinkingFlag?: string;
16
21
  }>;
22
+ /**
23
+ * How the seat model reaches the agent:
24
+ * - "argv": passed as the CLI `--model` flag (grok);
25
+ * - "set_model": sent as an ACP `session/set_model` RPC with modelId
26
+ * `provider:model` (hermes).
27
+ */
28
+ modelPassing: "argv" | "set_model";
17
29
  /** Which verb a bound resume uses; "session/new" hosts always mint + bind. */
18
30
  boundResume: "session/load" | "session/new";
19
31
  /** Durable ACP binding filename written beside the session principal. */
20
32
  sessionBindingFile: string;
21
33
  childEnv: Readonly<Record<string, string>>;
34
+ /**
35
+ * When set, the production factory ensures a seat profile whose SOUL.md is a
36
+ * symlink to the packaged role soul, and prefixes argv with `flag <name>`.
37
+ * Used by hosts that have no per-session systemPrompt channel (hermes).
38
+ */
39
+ seatProfileSoul?: SeatProfileSoul;
22
40
  }>;
23
41
 
24
42
  /** Absolute agent binary for one operator home. */
@@ -26,18 +44,35 @@ export function resolveAcpBinary(description: AcpHostDescription, operatorHome:
26
44
  return join(operatorHome, ...description.binaryFromHome);
27
45
  }
28
46
 
29
- /** Stdio argv: prefix, optional model/thinking flag pairs, suffix. */
47
+ /** Stdio argv: optional profile flag, thinking flag (before the subcommand),
48
+ * prefix, optional model flag pair, suffix. */
30
49
  export function acpStdioArgs(
31
50
  description: AcpHostDescription,
32
51
  model?: { readonly model?: string; readonly thinking?: string },
52
+ seat?: { readonly profileName?: string },
33
53
  ): string[] {
34
54
  const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
35
55
  const pair = (flag: string | undefined, value: string | undefined): string[] =>
36
56
  flag === undefined || value === undefined ? [] : [flag, value];
37
57
  return [
58
+ ...pair(description.seatProfileSoul?.flag, seat?.profileName),
59
+ ...pair(thinkingFlag, model?.thinking),
38
60
  ...prefix,
39
61
  ...pair(modelFlag, model?.model),
40
- ...pair(thinkingFlag, model?.thinking),
41
62
  ...suffix,
42
63
  ];
43
64
  }
65
+
66
+ /**
67
+ * The modelId the host addresses the seat model by.
68
+ * "argv" hosts address by bare model name; "set_model" hosts address by the
69
+ * `provider:model` modelId the ACP catalog exposes (seat provider after host
70
+ * alias projection, concatenated — never a package provider map).
71
+ */
72
+ export function acpModelId(
73
+ modelPassing: AcpHostDescription["modelPassing"],
74
+ model?: { readonly model?: string; readonly provider?: string },
75
+ ): string | undefined {
76
+ if (model?.model === undefined) return undefined;
77
+ return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
78
+ }
@@ -27,6 +27,7 @@ import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "..
27
27
  import { acpStdioArgs, resolveAcpBinary, type AcpHostDescription } from "./description.ts";
28
28
  import { createComposedAcpRoleTurnHost } from "./role-envelope.ts";
29
29
  import { connectAcpStdio } from "./role-turn-host.ts";
30
+ import { ensureSeatProfileSoul } from "./seat-profile-soul.ts";
30
31
  import { createAcpSessionIdentityAuthority } from "./session-identity.ts";
31
32
 
32
33
  export type ProductionAcpHostOptions = Readonly<{
@@ -121,11 +122,25 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
121
122
  return createComposedAcpRoleTurnHost({
122
123
  sessionIdentity: createAcpSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
123
124
  boundResume: description.boundResume,
125
+ modelPassing: description.modelPassing,
124
126
  roleRuntimeDependencies: createAcpRoleRuntimeDependencies(packageRoot),
125
127
  async connect(request) {
128
+ const seatProfile = description.seatProfileSoul;
129
+ const profileName = seatProfile === undefined
130
+ ? undefined
131
+ : await ensureSeatProfileSoul({
132
+ spec: seatProfile,
133
+ operatorHome: request.home,
134
+ packageRoot,
135
+ role: request.activation.role,
136
+ });
126
137
  return connectAcpStdio({
127
138
  binary: resolveAcpBinary(description, request.home),
128
- args: acpStdioArgs(description, request.model),
139
+ args: acpStdioArgs(
140
+ description,
141
+ request.model,
142
+ profileName === undefined ? undefined : { profileName },
143
+ ),
129
144
  cwd: request.cwd,
130
145
  env,
131
146
  });
@@ -622,7 +622,8 @@ export async function prepareAcpRoleEnvelope(options: {
622
622
  };
623
623
 
624
624
  // Shared envelope activation. systemPrompt must be ready before session/new
625
- // (ACP delivers it there), so activation runs during prepare.
625
+ // (delivered via _meta.systemPromptOverride where the host honors it), so
626
+ // activation runs during prepare.
626
627
  try {
627
628
  await emit("session_start", { reason: request.continuation.kind });
628
629
  const inputResults = await emit("input", { text: request.continuation.prompt, source: "interactive" });
@@ -3,7 +3,7 @@ import { createInterface } from "node:readline";
3
3
 
4
4
  import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
5
5
  import { renderAgentStartMaterials } from "../agent-start-materials.ts";
6
- import type { AcpHostDescription } from "./description.ts";
6
+ import { acpModelId, type AcpHostDescription } from "./description.ts";
7
7
 
8
8
  /** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
9
9
  export interface AcpConnection {
@@ -64,6 +64,12 @@ export type AcpRoleTurnHostConfig = Readonly<{
64
64
  sessionIdentity: AcpSessionIdentityAuthority;
65
65
  /** Whether a bound resume reuses the native session or mints a fresh one. */
66
66
  boundResume: AcpHostDescription["boundResume"];
67
+ /**
68
+ * How the seat model reaches the agent: "set_model" sends an ACP
69
+ * `session/set_model` RPC with modelId `provider:model` once the session
70
+ * exists (new or loaded); "argv" leaves it to the connect argv (--model).
71
+ */
72
+ modelPassing: AcpHostDescription["modelPassing"];
67
73
  connect(request: RoleTurnRequest): Promise<AcpConnection>;
68
74
  prepare(request: RoleTurnRequest): Promise<AcpPreparedTurn>;
69
75
  }>;
@@ -201,6 +207,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
201
207
  const execution = serial.then(async (): Promise<RoleTurnResult> => {
202
208
  const continuation = request.continuation;
203
209
  const prepared = await config.prepare(request);
210
+ const systemPromptOverride = renderAcpSystemPromptOverride(prepared.systemPrompt);
204
211
  let connection: AcpConnection | undefined;
205
212
  let sessionId: string | undefined;
206
213
  let accepted = false;
@@ -220,7 +227,8 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
220
227
  const modelState = initializeMeta?.modelState;
221
228
  const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
222
229
  if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
223
- typeof entry === "object" && entry !== null && (entry as { modelId?: unknown }).modelId === request.model?.model)) {
230
+ typeof entry === "object" && entry !== null
231
+ && (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
224
232
  return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
225
233
  provider: request.model.provider,
226
234
  model: request.model.model,
@@ -233,32 +241,33 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
233
241
  continuation.kind === "resume"
234
242
  ? request.hostTransition?.priorNativePaths
235
243
  : undefined;
244
+ const loadConnection = connection;
245
+ // Shared session/new + session/load body (cwd/mcpServers/_meta).
246
+ const sessionBindParams = {
247
+ cwd: request.cwd,
248
+ mcpServers: prepared.mcpServers,
249
+ _meta: { systemPromptOverride, yoloMode: false },
250
+ };
251
+ const loadSession = async (bindSessionId: string): Promise<string> => {
252
+ const loaded = await loadConnection.request("session/load", {
253
+ sessionId: bindSessionId,
254
+ ...sessionBindParams,
255
+ });
256
+ return typeof loaded.sessionId === "string" && loaded.sessionId !== ""
257
+ ? loaded.sessionId
258
+ : bindSessionId;
259
+ };
236
260
  if (continuation.kind === "resume" && config.boundResume === "session/load") {
237
261
  // Same-host resume reuses the native ACP session via session/load.
238
262
  const boundSessionId = await config.sessionIdentity.load(request.principal);
239
263
  if (boundSessionId !== undefined && boundSessionId !== "") {
240
- const loaded = await connection.request("session/load", {
241
- sessionId: boundSessionId,
242
- cwd: request.cwd,
243
- mcpServers: prepared.mcpServers,
244
- _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
245
- });
246
- sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
247
- ? loaded.sessionId
248
- : boundSessionId;
264
+ sessionId = await loadSession(boundSessionId);
249
265
  }
250
266
  }
251
267
  if (sessionId === undefined) {
252
268
  // Initial run, unbound resume (cross-host / lost binding), or a host
253
269
  // whose bound resume is session/new: mint the session and bind it.
254
- const session = await connection.request(
255
- "session/new",
256
- {
257
- cwd: request.cwd,
258
- mcpServers: prepared.mcpServers,
259
- _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
260
- },
261
- );
270
+ const session = await connection.request("session/new", sessionBindParams);
262
271
  sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
263
272
  if (sessionId === undefined || sessionId === "") {
264
273
  return failure("session", "AcpSessionFailure", "session-id-missing");
@@ -266,6 +275,24 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
266
275
  await config.sessionIdentity.bind(request.principal, sessionId);
267
276
  }
268
277
 
278
+ // set_model hosts address the seat model by `provider:model` once the
279
+ // session exists; argv hosts never reach this RPC. Provider is the seat
280
+ // table value after owner host-alias projection (#778) — concatenated
281
+ // here, never dropped, never remapped in package code. set_model may
282
+ // rebuild the session agent and drop ACP-injected mcpServers, so re-bind
283
+ // via the same loadSession authority (return value is the live session id).
284
+ if (
285
+ config.modelPassing === "set_model"
286
+ && request.model !== undefined
287
+ && sessionId !== undefined
288
+ ) {
289
+ await connection.request("session/set_model", {
290
+ sessionId,
291
+ modelId: acpModelId(config.modelPassing, request.model),
292
+ });
293
+ sessionId = await loadSession(sessionId);
294
+ }
295
+
269
296
  let prompt =
270
297
  priorNativePaths !== undefined && priorNativePaths.length > 0
271
298
  ? `${prepared.prompt}\n${priorNativePaths.join("\n")}`
@@ -319,12 +346,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
319
346
  for (let attempt = 0; attempt < 8; attempt += 1) {
320
347
  let result: Readonly<Record<string, unknown>>;
321
348
  try {
322
- const promptParts: Array<Record<string, unknown>> = [
323
- { type: "text", text: prompt },
324
- ];
325
349
  result = await promptOrAbort({
326
350
  sessionId,
327
- prompt: promptParts,
351
+ prompt: [{ type: "text", text: prompt }],
328
352
  });
329
353
  } catch (error) {
330
354
  // Envelope abort (typed infra declaration): closeRound owns the failure record.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Seat-scoped host profile soul delivery (#644 / 拍 2).
3
+ *
4
+ * Hermes has no per-session systemPrompt channel; identity is the profile's
5
+ * SOUL.md. Each seat owns `profiles/<namePrefix><role>/`, and SOUL.md is a
6
+ * symlink to the packaged `souls/<role>.md` (package is the sole soul source).
7
+ */
8
+ import { constants } from "node:fs";
9
+ import { access, copyFile, lstat, mkdir, readlink, symlink, unlink } from "node:fs/promises";
10
+ import { dirname, join, relative, resolve } from "node:path";
11
+
12
+ export type SeatProfileSoul = Readonly<{
13
+ /** Argv flag selecting the profile (hermes pre-argparse `-p`). */
14
+ flag: string;
15
+ /** Profile id = `${namePrefix}${role}` (e.g. `ak-judge`). */
16
+ namePrefix: string;
17
+ /** Profile directory root relative to the operator home (`.hermes/profiles`). */
18
+ profilesRootFromHome: readonly string[];
19
+ /** Soul filename inside the profile directory. */
20
+ soulFileName: string;
21
+ }>;
22
+
23
+ /** Profile id for one seat role. */
24
+ export function seatProfileName(spec: SeatProfileSoul, role: string): string {
25
+ return `${spec.namePrefix}${role}`;
26
+ }
27
+
28
+ /** Packaged soul path for one role (`souls/<role>.md`). */
29
+ export function packageRoleSoulPath(packageRoot: string, role: string): string {
30
+ return join(packageRoot, "souls", `${role}.md`);
31
+ }
32
+
33
+ async function pathExists(path: string): Promise<boolean> {
34
+ try {
35
+ await access(path, constants.F_OK);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Ensure the seat profile directory exists and SOUL.md is a symlink to the
44
+ * packaged role soul. On first create, copy credential surfaces from the host
45
+ * root (parent of the profiles root) so the profile can authenticate without
46
+ * touching the default profile in place. Returns the profile id for argv.
47
+ */
48
+ export async function ensureSeatProfileSoul(options: {
49
+ readonly spec: SeatProfileSoul;
50
+ readonly operatorHome: string;
51
+ readonly packageRoot: string;
52
+ readonly role: string;
53
+ }): Promise<string> {
54
+ const { spec, operatorHome, packageRoot, role } = options;
55
+ const profileName = seatProfileName(spec, role);
56
+ const soulTarget = resolve(packageRoleSoulPath(packageRoot, role));
57
+ if (!(await pathExists(soulTarget))) {
58
+ throw new Error(`packaged role soul missing: ${soulTarget}`);
59
+ }
60
+
61
+ const profilesRoot = join(operatorHome, ...spec.profilesRootFromHome);
62
+ const profileDir = join(profilesRoot, profileName);
63
+ const hostRoot = dirname(profilesRoot);
64
+ const soulPath = join(profileDir, spec.soulFileName);
65
+
66
+ if (!(await pathExists(profileDir))) {
67
+ await mkdir(profileDir, { recursive: true });
68
+ // First-create credential bootstrap only. Never rewrite an existing profile's
69
+ // auth/config; never write into the host root / default profile.
70
+ for (const name of ["auth.json", ".env", "config.yaml"] as const) {
71
+ const source = join(hostRoot, name);
72
+ if (!(await pathExists(source))) continue;
73
+ await copyFile(source, join(profileDir, name));
74
+ }
75
+ } else {
76
+ await mkdir(profileDir, { recursive: true });
77
+ }
78
+
79
+ const desiredLink = relative(profileDir, soulTarget);
80
+ let current: string | undefined;
81
+ try {
82
+ const st = await lstat(soulPath);
83
+ if (st.isSymbolicLink()) {
84
+ current = await readlink(soulPath);
85
+ }
86
+ } catch {
87
+ current = undefined;
88
+ }
89
+ if (current === desiredLink || current === soulTarget) {
90
+ return profileName;
91
+ }
92
+ if (await pathExists(soulPath) || current !== undefined) {
93
+ await unlink(soulPath);
94
+ }
95
+ await symlink(desiredLink, soulPath);
96
+ return profileName;
97
+ }
@@ -24,6 +24,7 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
24
24
  suffix: Object.freeze(["stdio"]),
25
25
  modelFlag: "--model",
26
26
  }),
27
+ modelPassing: "argv",
27
28
  boundResume: "session/load",
28
29
  sessionBindingFile: "grok-acp-session.json",
29
30
  childEnv: Object.freeze({
@@ -32,6 +33,31 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
32
33
  GROK_SUBAGENTS: "0",
33
34
  }),
34
35
  }),
36
+ /**
37
+ * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
38
+ * Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
39
+ * (seat table provider + model concatenated). Reasoning is the global
40
+ * `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
41
+ * (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
42
+ */
43
+ "hermes": Object.freeze({
44
+ binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
45
+ argv: Object.freeze({
46
+ prefix: Object.freeze(["acp"]),
47
+ suffix: Object.freeze([]),
48
+ thinkingFlag: "--reasoning",
49
+ }),
50
+ modelPassing: "set_model",
51
+ boundResume: "session/load",
52
+ sessionBindingFile: "hermes-acp-session.json",
53
+ childEnv: Object.freeze({}),
54
+ seatProfileSoul: Object.freeze({
55
+ flag: "-p",
56
+ namePrefix: "ak-",
57
+ profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
58
+ soulFileName: "SOUL.md",
59
+ }),
60
+ }),
35
61
  });
36
62
 
37
63
  export function lookupHostDescription(host: string): AcpHostDescription | undefined {