@akagilnc/pi-workflow-roles 0.1.5307 → 0.1.5312

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5307",
3
+ "version": "0.1.5312",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -4,13 +4,10 @@
4
4
  * optional seat-profile soul — is data here; the lifecycle in role-turn-host.ts
5
5
  * stays one copy (#732).
6
6
  */
7
- import { join } from "node:path";
8
-
7
+ import type { HostIdentityDescription } from "../host-descriptions.ts";
9
8
  import type { SeatProfileSoul } from "./seat-profile-soul.ts";
10
9
 
11
- export type AcpHostDescription = Readonly<{
12
- /** Binary path segments relative to the operator home. */
13
- binaryFromHome: readonly string[];
10
+ export type AcpHostDescription = HostIdentityDescription & Readonly<{
14
11
  argv: Readonly<{
15
12
  prefix: readonly string[];
16
13
  suffix: readonly string[];
@@ -26,8 +23,6 @@ export type AcpHostDescription = Readonly<{
26
23
  * `provider:model` (hermes).
27
24
  */
28
25
  modelPassing: "argv" | "set_model";
29
- /** Durable ACP binding filename written beside the session principal. */
30
- sessionBindingFile: string;
31
26
  /**
32
27
  * When set, the production factory ensures a seat profile whose SOUL.md is a
33
28
  * symlink to the packaged role soul, and prefixes argv with `flag <name>`.
@@ -36,11 +31,6 @@ export type AcpHostDescription = Readonly<{
36
31
  seatProfileSoul?: SeatProfileSoul;
37
32
  }>;
38
33
 
39
- /** Absolute agent binary for one operator home. */
40
- export function resolveAcpBinary(description: AcpHostDescription, operatorHome: string): string {
41
- return join(operatorHome, ...description.binaryFromHome);
42
- }
43
-
44
34
  /** Stdio argv: optional profile flag, thinking flag, prefix, model, suffix. */
45
35
  export function acpStdioArgs(
46
36
  description: AcpHostDescription,
@@ -9,11 +9,12 @@
9
9
  import { randomUUID } from "node:crypto";
10
10
 
11
11
  import type { DurablePrincipalAuthority, RoleTurnHost, RoleTurnRequest } from "../host-contracts.ts";
12
+ import { resolveHostBinary } from "../host-descriptions.ts";
12
13
  import { prepareRoleEnvelope } from "../role-envelope.ts";
13
14
  import type { RoleRuntimeDependencies } from "../role-runtime.ts";
14
15
  import { createRoleRuntimeDependencies } from "../role-runtime-dependencies.ts";
15
16
  import { createSessionIdentityAuthority } from "../session-identity.ts";
16
- import { acpStdioArgs, resolveAcpBinary, type AcpHostDescription } from "./description.ts";
17
+ import { acpStdioArgs, type AcpHostDescription } from "./description.ts";
17
18
  import {
18
19
  connectAcpStdio,
19
20
  createAcpRoleTurnHost,
@@ -74,7 +75,7 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
74
75
  role: request.activation.role,
75
76
  });
76
77
  return connectAcpStdio({
77
- binary: resolveAcpBinary(description, request.home),
78
+ binary: resolveHostBinary(description, request.home),
78
79
  args: acpStdioArgs(
79
80
  description,
80
81
  request.model,
@@ -2,11 +2,13 @@ import { join } from "node:path";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createInterface } from "node:readline";
4
4
 
5
- import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
5
+ import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
6
6
  import {
7
7
  createSerializedRoleTurnHost,
8
8
  driveExternalRoleTurnRounds,
9
+ externalHostFailure as failure,
9
10
  raceAgainstHostAbort,
11
+ withExternalHostCleanupFailure,
10
12
  } from "../external-host-turn-loop.ts";
11
13
 
12
14
  import {
@@ -19,6 +21,7 @@ import {
19
21
  } from "../package-contracts/navigator-output.ts";
20
22
  import {
21
23
  renderSystemPromptOverride,
24
+ resolveBoundHostSessionId,
22
25
  type PreparedRoleTurn,
23
26
  type SessionIdentityAuthority,
24
27
  } from "../prepared-role-turn.ts";
@@ -96,41 +99,6 @@ export type AcpRoleTurnHostConfig = Readonly<{
96
99
  prepare(request: RoleTurnRequest): Promise<PreparedRoleTurn>;
97
100
  }>;
98
101
 
99
- function failure(
100
- cause: "activation" | "session" | "output",
101
- name: string,
102
- code: string,
103
- details?: Readonly<Record<string, unknown>>,
104
- diagnostic?: string,
105
- ): RoleTurnResult {
106
- return {
107
- code: null,
108
- stderr: "",
109
- timedOut: false,
110
- knownFailure: {
111
- cause,
112
- identity: { name, code },
113
- ...(diagnostic === undefined ? {} : { diagnostic }),
114
- ...(details === undefined ? {} : { details }),
115
- },
116
- };
117
- }
118
-
119
- /** Success→dispose failure; existing failure keeps primary cause + cleanup detail. */
120
- function withCleanupFailure(outcome: RoleTurnResult, cleanupError: unknown): RoleTurnResult {
121
- const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
122
- if (outcome.knownFailure === undefined) {
123
- return failure("session", "AcpDisposeFailure", "dispose-failed", { cleanupError: message }, message);
124
- }
125
- return {
126
- ...outcome,
127
- knownFailure: {
128
- ...outcome.knownFailure,
129
- details: { ...(outcome.knownFailure.details ?? {}), cleanupError: message },
130
- },
131
- };
132
- }
133
-
134
102
  type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
135
103
 
136
104
  function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
@@ -259,7 +227,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
259
227
  let sessionOpened = false;
260
228
  let accepted = false;
261
229
  const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
262
- // Mutable so dispose failure can outrank a clean turn (headless withCleanupFailure face).
230
+ // Mutable so dispose failure can outrank a clean turn.
263
231
  let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
264
232
  try {
265
233
  if (prepared.mcpServers.length === 0) {
@@ -324,10 +292,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
324
292
  };
325
293
  let sessionReady = true;
326
294
  if (request.continuation.kind === "resume") {
327
- const explicitHostSessionId = request.continuation.hostSessionId;
328
- const boundSessionId = explicitHostSessionId !== undefined && explicitHostSessionId !== ""
329
- ? explicitHostSessionId
330
- : await config.sessionIdentity.load(request.principal);
295
+ const boundSessionId = await resolveBoundHostSessionId(request, config.sessionIdentity);
331
296
  if (boundSessionId !== undefined && boundSessionId !== "") {
332
297
  sessionId = boundSessionId;
333
298
  sessionId = await loadSession(boundSessionId);
@@ -446,7 +411,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
446
411
  try {
447
412
  await prepared.dispose?.();
448
413
  } catch (cleanupError) {
449
- outcome = withCleanupFailure(outcome, cleanupError);
414
+ outcome = withExternalHostCleanupFailure(outcome, cleanupError, "AcpDisposeFailure");
450
415
  }
451
416
  }
452
417
  return outcome;
@@ -9,25 +9,11 @@ import type {
9
9
  RoleTurnRequest,
10
10
  RoleTurnResult,
11
11
  } from "./host-contracts.ts";
12
+ import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
12
13
 
13
14
  export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
14
15
 
15
- export type ExternalPreparedTurn = Readonly<{
16
- readonly prompt: string;
17
- readonly abortSignal?: AbortSignal;
18
- closeRound(): Promise<
19
- | { readonly accepted: true }
20
- | {
21
- readonly accepted: false;
22
- readonly retry: {
23
- readonly code: string;
24
- readonly toolCallIds: readonly string[];
25
- readonly message: string;
26
- };
27
- }
28
- | { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
29
- >;
30
- }>;
16
+ export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
31
17
 
32
18
  export type ExternalHostRoundOutcome =
33
19
  | { readonly status: "delivered"; readonly stderr?: string }
@@ -96,6 +82,40 @@ export function raceAgainstHostAbort<T>(
96
82
  });
97
83
  }
98
84
 
85
+ export function externalHostFailure(
86
+ cause: NonNullable<RoleTurnKnownFailure["cause"]>,
87
+ name: string,
88
+ code: string,
89
+ details?: Readonly<Record<string, unknown>>,
90
+ diagnostic?: string,
91
+ ): RoleTurnResult {
92
+ return asFailure({
93
+ cause,
94
+ identity: { name, code },
95
+ ...(diagnostic === undefined ? {} : { diagnostic }),
96
+ ...(details === undefined ? {} : { details }),
97
+ });
98
+ }
99
+
100
+ /** A dispose failure replaces success; a primary failure retains its cause. */
101
+ export function withExternalHostCleanupFailure(
102
+ outcome: RoleTurnResult,
103
+ cleanupError: unknown,
104
+ name: string,
105
+ ): RoleTurnResult {
106
+ const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
107
+ if (outcome.knownFailure === undefined) {
108
+ return externalHostFailure("session", name, "dispose-failed", { cleanupError: message }, message);
109
+ }
110
+ return {
111
+ ...outcome,
112
+ knownFailure: {
113
+ ...outcome.knownFailure,
114
+ details: { ...(outcome.knownFailure.details ?? {}), cleanupError: message },
115
+ },
116
+ };
117
+ }
118
+
99
119
  function asFailure(knownFailure: RoleTurnKnownFailure, stderr = ""): RoleTurnResult {
100
120
  return { code: null, stderr, timedOut: false, knownFailure };
101
121
  }
@@ -4,17 +4,10 @@
4
4
  * Claude print-mode and codex exec differ enough that #752 host-specific
5
5
  * assembly lives here as sibling helpers — not a third unified abstraction.
6
6
  */
7
- import { join } from "node:path";
8
-
9
- type HeadlessHostBase = Readonly<{
10
- /** Binary path segments relative to the operator home. */
11
- binaryFromHome: readonly string[];
12
- /** Durable session-id binding filename beside the session principal. */
13
- sessionBindingFile: string;
14
- }>;
7
+ import type { HostIdentityDescription } from "../host-descriptions.ts";
15
8
 
16
9
  /** Claude Code print-mode (#645). */
17
- export type ClaudePrintHostDescription = HeadlessHostBase & Readonly<{
10
+ export type ClaudePrintHostDescription = HostIdentityDescription & Readonly<{
18
11
  protocol: "claude-print";
19
12
  /**
20
13
  * Host-native print-mode flags that never change per turn (no prompt).
@@ -48,7 +41,7 @@ export type ClaudePrintHostDescription = HeadlessHostBase & Readonly<{
48
41
  * Protocol differences (JSONL, resume subcommand, schema file, `-c` MCP) stay
49
42
  * in codex-specific helpers — description only carries identity + binary path.
50
43
  */
51
- export type CodexExecHostDescription = HeadlessHostBase & Readonly<{
44
+ export type CodexExecHostDescription = HostIdentityDescription & Readonly<{
52
45
  protocol: "codex-exec";
53
46
  }>;
54
47
 
@@ -66,14 +59,6 @@ export function isCodexExecDescription(
66
59
  return description.protocol === "codex-exec";
67
60
  }
68
61
 
69
- /** Absolute agent binary for one operator home. */
70
- export function resolveHeadlessBinary(
71
- description: HeadlessHostDescription,
72
- operatorHome: string,
73
- ): string {
74
- return join(operatorHome, ...description.binaryFromHome);
75
- }
76
-
77
62
  /**
78
63
  * Build one Claude print-mode argv for a single process turn.
79
64
  * Shape: `<promptFlag> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
@@ -12,10 +12,11 @@
12
12
  import { randomUUID } from "node:crypto";
13
13
 
14
14
  import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
15
+ import { resolveHostBinary } from "../host-descriptions.ts";
15
16
  import { createRoleRuntimeDependencies } from "../role-runtime-dependencies.ts";
16
17
  import { prepareRoleEnvelope } from "../role-envelope.ts";
17
18
  import { createSessionIdentityAuthority } from "../session-identity.ts";
18
- import { resolveHeadlessBinary, type HeadlessHostDescription } from "./description.ts";
19
+ import type { HeadlessHostDescription } from "./description.ts";
19
20
  import { createHeadlessRoleTurnHost } from "./role-turn-host.ts";
20
21
 
21
22
  export type ProductionHeadlessHostOptions = Readonly<{
@@ -46,7 +47,7 @@ export function createProductionHeadlessRoleTurnHost(
46
47
  description,
47
48
  hostName,
48
49
  sessionIdentity,
49
- binary: resolveHeadlessBinary(description, operatorHome),
50
+ binary: resolveHostBinary(description, operatorHome),
50
51
  env: {
51
52
  ...process.env,
52
53
  AK_PACKAGE_ROOT: packageRoot,
@@ -13,11 +13,14 @@ import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-cont
13
13
  import {
14
14
  createSerializedRoleTurnHost,
15
15
  driveExternalRoleTurnRounds,
16
+ externalHostFailure as failure,
16
17
  hostAbortedError,
17
18
  isHostAbortedError,
19
+ withExternalHostCleanupFailure,
18
20
  } from "../external-host-turn-loop.ts";
19
21
  import {
20
22
  renderSystemPromptOverride,
23
+ resolveBoundHostSessionId,
21
24
  type PreparedRoleTurn,
22
25
  type SessionIdentityAuthority,
23
26
  } from "../prepared-role-turn.ts";
@@ -48,26 +51,6 @@ export type HeadlessRoleTurnHostConfig = Readonly<{
48
51
  env?: NodeJS.ProcessEnv;
49
52
  }>;
50
53
 
51
- function failure(
52
- cause: "activation" | "session" | "output" | "provider",
53
- name: string,
54
- code: string,
55
- details?: Readonly<Record<string, unknown>>,
56
- diagnostic?: string,
57
- ): RoleTurnResult {
58
- return {
59
- code: null,
60
- stderr: "",
61
- timedOut: false,
62
- knownFailure: {
63
- cause,
64
- identity: { name, code },
65
- ...(diagnostic === undefined ? {} : { diagnostic }),
66
- ...(details === undefined ? {} : { details }),
67
- },
68
- };
69
- }
70
-
71
54
  /** One headless CLI result envelope (stream-json last line, or single json doc). */
72
55
  export type HeadlessCliResult = Readonly<{
73
56
  session_id?: string;
@@ -93,39 +76,18 @@ function isHeadlessResultCandidate(value: unknown): value is HeadlessCliResult {
93
76
  }
94
77
 
95
78
  /**
96
- * Parse Claude host stdout into the result envelope.
97
- * Production uses `--output-format stream-json` (#811 live records); last-result
98
- * line is the typed receipt. A single-document `json` body still parses so a
99
- * misconfigured description yields a typed miss rather than a silent empty parse.
100
- * Callers must not retain the full stream — only the rolling result candidate.
79
+ * Parse the rolling result-candidate line retained by spawnHeadlessTurn.
80
+ * The full stream is delivered live, not passed to this parser.
101
81
  */
102
82
  export function parseHeadlessCliStdout(stdout: string): HeadlessCliResult | undefined {
103
83
  const trimmed = stdout.trim();
104
84
  if (trimmed === "") return undefined;
105
85
  try {
106
- const single = JSON.parse(trimmed) as unknown;
107
- if (isHeadlessResultCandidate(single)) return single;
86
+ const value = JSON.parse(trimmed) as unknown;
87
+ return isHeadlessResultCandidate(value) ? value : undefined;
108
88
  } catch {
109
- // fall through
110
- }
111
- // stream-json: keep the last result line (structured_output / is_error live here).
112
- let last: HeadlessCliResult | undefined;
113
- for (const line of trimmed.split("\n")) {
114
- const text = line.trim();
115
- if (text === "") continue;
116
- try {
117
- const value = JSON.parse(text) as unknown;
118
- if (!isPlainObject(value)) continue;
119
- const record = value as HeadlessCliResult & { type?: unknown };
120
- // Multi-line stream: only explicit result / structured_output lines (not bare objects).
121
- if (record.type === "result" || record.structured_output !== undefined) {
122
- last = record;
123
- }
124
- } catch {
125
- // skip non-JSON noise lines
126
- }
89
+ return undefined;
127
90
  }
128
- return last;
129
91
  }
130
92
 
131
93
  /**
@@ -380,21 +342,6 @@ function spawnHeadlessTurn(options: {
380
342
  });
381
343
  }
382
344
 
383
- /** Success→dispose failure; existing failure keeps primary cause + cleanup detail. */
384
- function withCleanupFailure(outcome: RoleTurnResult, cleanupError: unknown): RoleTurnResult {
385
- const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
386
- if (outcome.knownFailure === undefined) {
387
- return failure("session", "HeadlessDisposeFailure", "dispose-failed", { cleanupError: message }, message);
388
- }
389
- return {
390
- ...outcome,
391
- knownFailure: {
392
- ...outcome.knownFailure,
393
- details: { ...(outcome.knownFailure.details ?? {}), cleanupError: message },
394
- },
395
- };
396
- }
397
-
398
345
  function terminalFromSpawned(
399
346
  spawned: { code: number | null; stderr: string; timedOut: boolean },
400
347
  knownFailure: NonNullable<RoleTurnResult["knownFailure"]>,
@@ -469,12 +416,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
469
416
  try {
470
417
  // Claude mints a package UUID for --session-id; codex waits for thread.started.
471
418
  // Public explicit resume already read the stored native id. Auto-resume omits it.
472
- const explicitHostSessionId = request.continuation.kind === "resume"
473
- ? request.continuation.hostSessionId
474
- : undefined;
475
- let sessionId = explicitHostSessionId !== undefined && explicitHostSessionId !== ""
476
- ? explicitHostSessionId
477
- : await config.sessionIdentity.load(request.principal);
419
+ let sessionId = await resolveBoundHostSessionId(request, config.sessionIdentity);
478
420
  if (request.continuation.kind === "resume" && (sessionId === undefined || sessionId === "")) {
479
421
  outcome = failure(
480
422
  "session",
@@ -821,7 +763,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
821
763
  try {
822
764
  await prepared.dispose?.();
823
765
  } catch (cleanupError) {
824
- outcome = withCleanupFailure(outcome, cleanupError);
766
+ outcome = withExternalHostCleanupFailure(outcome, cleanupError, "HeadlessDisposeFailure");
825
767
  }
826
768
  }
827
769
  return outcome;
@@ -6,9 +6,27 @@
6
6
  * pi is the in-process default, not a row.
7
7
  * Unregistered names fail closed (#510); these tables do not fallback.
8
8
  */
9
+ import { join } from "node:path";
10
+
9
11
  import type { AcpHostDescription } from "./acp-host/description.ts";
10
12
  import type { HeadlessHostDescription } from "./headless-host/description.ts";
11
13
 
14
+ /** Shared identity fields consumed by both external host families. */
15
+ export type HostIdentityDescription = Readonly<{
16
+ /** Binary path segments relative to the operator home. */
17
+ binaryFromHome: readonly string[];
18
+ /** Durable host session binding filename beside the session principal. */
19
+ sessionBindingFile: string;
20
+ }>;
21
+
22
+ /** Resolve a host description's agent binary under the operator home. */
23
+ export function resolveHostBinary(
24
+ description: HostIdentityDescription,
25
+ operatorHome: string,
26
+ ): string {
27
+ return join(operatorHome, ...description.binaryFromHome);
28
+ }
29
+
12
30
  export const DEFAULT_ROLE_TURN_HOST = "pi" as const;
13
31
 
14
32
  export type HostFamily = "acp" | "headless";
@@ -60,6 +60,14 @@ export function renderSystemPromptOverride(authority: {
60
60
  return renderAgentStartMaterials(authority.body, authority.materials);
61
61
  }
62
62
 
63
+ export async function resolveBoundHostSessionId(
64
+ request: RoleTurnRequest,
65
+ identity: SessionIdentityAuthority,
66
+ ): Promise<string | undefined> {
67
+ const explicit = request.continuation.kind === "resume" ? request.continuation.hostSessionId : undefined;
68
+ return explicit !== undefined && explicit !== "" ? explicit : identity.load(request.principal);
69
+ }
70
+
63
71
  export type SessionIdentityAuthority = Readonly<{
64
72
  load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
65
73
  bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
@@ -2,7 +2,7 @@ import { accessSync, constants, statSync } from "node:fs";
2
2
  import { lstat, mkdir, symlink } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
- import { HEADLESS_HOST_DESCRIPTIONS, HOST_DESCRIPTIONS } from "../host-descriptions.ts";
5
+ import { HEADLESS_HOST_DESCRIPTIONS, HOST_DESCRIPTIONS, resolveHostBinary } from "../host-descriptions.ts";
6
6
  import { packagedMethodSkillNames } from "../packaged-role-registry.ts";
7
7
 
8
8
  /** Install source only. Which names are required comes from the role registry. */
@@ -126,11 +126,11 @@ export async function runMachineSkillSetup(home: string, stdout: (text: string)
126
126
 
127
127
  for (const host of ["claude-code", "hermes"] as const) {
128
128
  const root = linkedSkillRoot(home, host)!;
129
- const binaryFromHome = host === "claude-code"
130
- ? HEADLESS_HOST_DESCRIPTIONS.claude!.binaryFromHome
131
- : HOST_DESCRIPTIONS.hermes!.binaryFromHome;
129
+ const description = host === "claude-code"
130
+ ? HEADLESS_HOST_DESCRIPTIONS.claude!
131
+ : HOST_DESCRIPTIONS.hermes!;
132
132
  try {
133
- accessSync(join(home, ...binaryFromHome), constants.X_OK);
133
+ accessSync(resolveHostBinary(description, home), constants.X_OK);
134
134
  } catch (error) {
135
135
  if (["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) continue;
136
136
  throw error;