@akagilnc/pi-workflow-roles 0.1.4243 → 0.1.4274

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.
@@ -77,11 +77,17 @@ function hostSelectionFailureFromUnknown(error) {
77
77
  const failure = error.failure;
78
78
  return failure;
79
79
  }
80
- async function createSummonEnv(options) {
81
- const [{ piDurablePrincipalAuthority }, { appendPiSessionCustomEntry }, { resolveRoleTurnHost }] = await Promise.all([
80
+ async function createSummonEnv(options, afterHost) {
81
+ const [
82
+ { piDurablePrincipalAuthority },
83
+ { appendPiSessionCustomEntry },
84
+ { resolveRoleTurnHost },
85
+ { missingResolvedSeatModelMessage, resolvedSeatWithModel }
86
+ ] = await Promise.all([
82
87
  import("./pi/durable-principal.js"),
83
88
  import("./pi/role-turn-host.js"),
84
- import("./public-cli/role-turn-host-resolution.js")
89
+ import("./public-cli/role-turn-host-resolution.js"),
90
+ import("./public-cli/config.js")
85
91
  ]);
86
92
  const principalAuthority = piDurablePrincipalAuthority;
87
93
  const roleTurnHost = resolveRoleTurnHost(
@@ -93,26 +99,37 @@ async function createSummonEnv(options) {
93
99
  },
94
100
  { role: options.role, seat: options.seat, principalAuthority }
95
101
  );
96
- const hostName = options.seat.host ?? "pi";
102
+ afterHost?.();
103
+ const seatWithModel = resolvedSeatWithModel(options.seat);
104
+ if (seatWithModel === void 0) {
105
+ return {
106
+ ok: false,
107
+ missingModelMessage: missingResolvedSeatModelMessage(options.role)
108
+ };
109
+ }
110
+ const hostName = seatWithModel.host ?? "pi";
97
111
  const { loadHostProvidersTable, projectHostFacingProvider } = await import("./public-cli/host-providers.js");
98
- const hostFacingSelection = options.seat.selection === void 0 ? void 0 : projectHostFacingProvider(
99
- options.seat.selection,
112
+ const hostFacingSelection = projectHostFacingProvider(
113
+ seatWithModel.selection,
100
114
  hostName,
101
115
  loadHostProvidersTable(options.home),
102
116
  options.home
103
117
  );
104
118
  return {
105
- home: options.home,
106
- principalAuthority,
107
- agentDir: options.agentDir,
108
- sessionAppender: appendPiSessionCustomEntry,
109
- packageRoot: options.packageRoot,
110
- roleTurnHost,
111
- cwd: options.cwd,
112
- credentials: options.credentials,
113
- ...hostFacingSelection === void 0 ? {} : { model: hostFacingSelection },
114
- ...projectSeatEngine(options.seat),
115
- ...projectSeatHost(options.seat)
119
+ ok: true,
120
+ env: {
121
+ home: options.home,
122
+ principalAuthority,
123
+ agentDir: options.agentDir,
124
+ sessionAppender: appendPiSessionCustomEntry,
125
+ packageRoot: options.packageRoot,
126
+ roleTurnHost,
127
+ cwd: options.cwd,
128
+ credentials: options.credentials,
129
+ ...hostFacingSelection === void 0 ? {} : { model: hostFacingSelection },
130
+ ...projectSeatEngine(seatWithModel),
131
+ ...projectSeatHost(seatWithModel)
132
+ }
116
133
  };
117
134
  }
118
135
  async function summonPublicRole(options) {
@@ -127,46 +144,72 @@ async function summonPublicRole(options) {
127
144
  const credentials = options.credentials ?? await loadCredentialProviders(agentDir);
128
145
  const config = await loadPublicCliConfig(home);
129
146
  const seat = resolveEffectiveSeat(config, options.role, credentials);
130
- let summonEnv;
131
- try {
132
- summonEnv = await createSummonEnv({
133
- role: options.role,
134
- home,
135
- agentDir,
136
- cwd: options.cwd,
137
- packageRoot,
138
- credentials,
139
- seat,
140
- ...options.extraPiArgs === void 0 ? {} : { extraPiArgs: options.extraPiArgs },
141
- ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
142
- ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters }
143
- });
144
- } catch (error) {
145
- const failure = hostSelectionFailureFromUnknown(error);
146
- if (failure !== void 0) {
147
- const { formatHostSelectionFailure } = await import("./public-cli/role-turn-host-resolution.js");
148
- return { exitCode: 1, stderr: formatHostSelectionFailure(failure) };
149
- }
150
- throw error;
151
- }
152
- const env = {
153
- ...summonEnv,
154
- // Station child role run (#840): omit automatic navigator attendance.
155
- stationChild: true,
156
- // Host config passthrough only — same face as public CLI (#422 / #675).
157
- ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit },
158
- // Parent cancellation reaches the nested activation's own turn dispatch.
159
- ...options.signal === void 0 ? {} : { signal: options.signal },
160
- // #753: reask rides the existing notary same-ticket resume summons.instruction.
161
- ...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
162
- // #879: verbatim submission body on officer dialogue content channel.
163
- ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
164
- ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
165
- // createRunId is the only remaining env overlay — host is seat-selected above.
166
- ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
167
- };
168
147
  const captured = options.io === void 0 ? createCapturingIo() : void 0;
169
148
  const io = options.io ?? captured.io;
149
+ const summonEnvInput = {
150
+ role: options.role,
151
+ home,
152
+ agentDir,
153
+ cwd: options.cwd,
154
+ packageRoot,
155
+ credentials,
156
+ seat,
157
+ ...options.extraPiArgs === void 0 ? {} : { extraPiArgs: options.extraPiArgs },
158
+ ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
159
+ ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters }
160
+ };
161
+ async function prepareSummonEnv(afterHost) {
162
+ try {
163
+ const built = await createSummonEnv(summonEnvInput, afterHost);
164
+ if (!built.ok) {
165
+ return { ok: false, exitCode: 1, stderr: built.missingModelMessage };
166
+ }
167
+ return {
168
+ ok: true,
169
+ env: {
170
+ ...built.env,
171
+ stationChild: true,
172
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit },
173
+ ...options.signal === void 0 ? {} : { signal: options.signal },
174
+ ...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
175
+ ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
176
+ ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
177
+ ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
178
+ }
179
+ };
180
+ } catch (error) {
181
+ const failure = hostSelectionFailureFromUnknown(error);
182
+ if (failure !== void 0) {
183
+ const { formatHostSelectionFailure } = await import("./public-cli/role-turn-host-resolution.js");
184
+ return { ok: false, exitCode: 1, stderr: formatHostSelectionFailure(failure) };
185
+ }
186
+ const { CliUsageError } = await import("./public-cli/cli-errors.js");
187
+ if (error instanceof CliUsageError) {
188
+ const { presentStructuralRejection } = await import("./public-cli/settlement.js");
189
+ presentStructuralRejection(error, io);
190
+ return { ok: false, exitCode: 2 };
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+ function projectPrepareFailure(prepared) {
196
+ const parts = [prepared.stderr, captured?.stderrText()].filter(
197
+ (s) => typeof s === "string" && s !== ""
198
+ );
199
+ const stderr2 = parts.length === 0 ? void 0 : parts.join("");
200
+ return {
201
+ exitCode: prepared.exitCode,
202
+ ...stderr2 === void 0 ? {} : { stderr: stderr2 }
203
+ };
204
+ }
205
+ async function runPrepared(parse, run) {
206
+ let parsedArgv;
207
+ const prepared = await prepareSummonEnv(() => {
208
+ parsedArgv = parse(options.argv);
209
+ });
210
+ if (!prepared.ok) return { fail: projectPrepareFailure(prepared) };
211
+ return { ok: await run(prepared.env, () => parsedArgv) };
212
+ }
170
213
  let result;
171
214
  switch (options.role) {
172
215
  case "notary": {
@@ -174,7 +217,9 @@ async function summonPublicRole(options) {
174
217
  import("./public-cli/notary-run.js"),
175
218
  import("./public-cli/invocation.js")
176
219
  ]);
177
- result = await runPublicNotary(options.argv, env, io, parseNotaryArgv);
220
+ const stepped = await runPrepared(parseNotaryArgv, (env, once) => runPublicNotary(options.argv, env, io, once));
221
+ if ("fail" in stepped) return stepped.fail;
222
+ result = stepped.ok;
178
223
  break;
179
224
  }
180
225
  case "inspector": {
@@ -182,49 +227,23 @@ async function summonPublicRole(options) {
182
227
  import("./public-cli/inspector-run.js"),
183
228
  import("./public-cli/invocation.js")
184
229
  ]);
185
- result = await runPublicInspector(options.argv, env, io, parseInspectorArgv);
186
- break;
187
- }
188
- case "auditor": {
189
- const [{ runPublicInstructionSeat }, { parseAuditorArgv }] = await Promise.all([
190
- import("./public-cli/instruction-seat-run.js"),
191
- import("./public-cli/invocation.js")
192
- ]);
193
- result = await runPublicInstructionSeat(
194
- options.argv,
195
- env,
196
- io,
197
- "auditor",
198
- parseAuditorArgv
199
- );
200
- break;
201
- }
202
- case "navigator": {
203
- const [{ runPublicInstructionSeat }, { parseNavigatorArgv }] = await Promise.all([
204
- import("./public-cli/instruction-seat-run.js"),
205
- import("./public-cli/invocation.js")
206
- ]);
207
- result = await runPublicInstructionSeat(
208
- options.argv,
209
- env,
210
- io,
211
- "navigator",
212
- parseNavigatorArgv
213
- );
230
+ const stepped = await runPrepared(parseInspectorArgv, (env, once) => runPublicInspector(options.argv, env, io, once));
231
+ if ("fail" in stepped) return stepped.fail;
232
+ result = stepped.ok;
214
233
  break;
215
234
  }
235
+ case "auditor":
236
+ case "navigator":
216
237
  case "gatekeeper": {
217
- const [{ runPublicInstructionSeat }, { parseGatekeeperArgv }] = await Promise.all([
238
+ const seat2 = options.role;
239
+ const [{ runPublicInstructionSeat }, invocation] = await Promise.all([
218
240
  import("./public-cli/instruction-seat-run.js"),
219
241
  import("./public-cli/invocation.js")
220
242
  ]);
221
- result = await runPublicInstructionSeat(
222
- options.argv,
223
- env,
224
- io,
225
- "gatekeeper",
226
- parseGatekeeperArgv
227
- );
243
+ const parse = seat2 === "auditor" ? invocation.parseAuditorArgv : seat2 === "navigator" ? invocation.parseNavigatorArgv : invocation.parseGatekeeperArgv;
244
+ const stepped = await runPrepared(parse, (env, once) => runPublicInstructionSeat(options.argv, env, io, seat2, once));
245
+ if ("fail" in stepped) return stepped.fail;
246
+ result = stepped.ok;
228
247
  break;
229
248
  }
230
249
  case "judge": {
@@ -232,7 +251,9 @@ async function summonPublicRole(options) {
232
251
  import("./public-cli/judge-run.js"),
233
252
  import("./public-cli/invocation.js")
234
253
  ]);
235
- result = await runPublicJudge(options.argv, env, io, parseJudgeArgv);
254
+ const stepped = await runPrepared(parseJudgeArgv, (env, once) => runPublicJudge(options.argv, env, io, once));
255
+ if ("fail" in stepped) return stepped.fail;
256
+ result = stepped.ok;
236
257
  break;
237
258
  }
238
259
  case "doctor": {
@@ -240,7 +261,9 @@ async function summonPublicRole(options) {
240
261
  import("./public-cli/doctor-run.js"),
241
262
  import("./public-cli/invocation.js")
242
263
  ]);
243
- result = await runPublicDoctor(options.argv, env, io, parseDoctorArgv);
264
+ const stepped = await runPrepared(parseDoctorArgv, (env, once) => runPublicDoctor(options.argv, env, io, once));
265
+ if ("fail" in stepped) return stepped.fail;
266
+ result = stepped.ok;
244
267
  break;
245
268
  }
246
269
  case "diarist": {
@@ -248,7 +271,9 @@ async function summonPublicRole(options) {
248
271
  import("./public-cli/diarist-run.js"),
249
272
  import("./public-cli/invocation.js")
250
273
  ]);
251
- result = await runPublicDiarist(options.argv, env, io, parseDiaristArgv);
274
+ const stepped = await runPrepared(parseDiaristArgv, (env, once) => runPublicDiarist(options.argv, env, io, once));
275
+ if ("fail" in stepped) return stepped.fail;
276
+ result = stepped.ok;
252
277
  break;
253
278
  }
254
279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4243",
3
+ "version": "0.1.4274",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -5,6 +5,7 @@
5
5
  * Engine process failures stop through the host infrastructure-failure seam.
6
6
  * Caller AbortSignal cancellation propagates unchanged.
7
7
  */
8
+ import { basename } from "node:path";
8
9
  import { Type, type Static } from "typebox";
9
10
  import type { HostContext, HostToolDefinition, HostToolResult, RoleHost } from "./host-contracts.ts";
10
11
 
@@ -16,6 +17,18 @@ import {
16
17
  resolveEngineName,
17
18
  runEngineDetourOnce,
18
19
  } from "./engine-detour.ts";
20
+ import {
21
+ engineDetourStdoutByteLength,
22
+ reportEngineDetourCall,
23
+ } from "./engine-detour-usage.ts";
24
+
25
+ /** runDirectory leaf is `<runId>@<role>`; subject is optional. */
26
+ function basenameRunId(runDirectory: string): string | undefined {
27
+ const leaf = basename(runDirectory);
28
+ const at = leaf.indexOf("@");
29
+ if (at <= 0) return undefined;
30
+ return leaf.slice(0, at);
31
+ }
19
32
 
20
33
  // #836 r16 class 3: argv required/minItems/element-minLength stay — execute()
21
34
  // must obtain the first item as the executable and spawn it (below; #82-98).
@@ -32,7 +45,14 @@ const engineDetourArgsSchema = Type.Object(
32
45
 
33
46
  type EngineDetourArgs = Static<typeof engineDetourArgsSchema>;
34
47
 
35
- type EngineDetourContext = Pick<HostContext, "cwd" | "mode" | "abort">;
48
+ type EngineDetourContext = Pick<HostContext, "cwd" | "mode" | "abort"> & {
49
+ sessionManager?: Pick<HostContext["sessionManager"], "getSessionFile">;
50
+ runDirectory?: string;
51
+ /** Public-invocation scope from the shared Host envelope (#537). */
52
+ invocationScopeId?: string;
53
+ /** Selected host from the shared Host envelope (#537 / ADR 0082). */
54
+ host?: string;
55
+ };
36
56
 
37
57
  export type EngineDetourHostActions = {
38
58
  failInfrastructure(
@@ -58,6 +78,12 @@ function isCallerCancellation(
58
78
  return false;
59
79
  }
60
80
 
81
+ function asError(error: unknown, fallback: string): Error {
82
+ return error instanceof Error
83
+ ? error
84
+ : new Error(String(error).trim() || fallback);
85
+ }
86
+
61
87
  /**
62
88
  * Build one detour tool definition for a configured engine name.
63
89
  * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
@@ -100,6 +126,68 @@ export function createEngineDetourToolDefinition(input: {
100
126
  );
101
127
  }
102
128
 
129
+ const sessionParent = ctx.sessionManager?.getSessionFile?.();
130
+ const startedAt = Date.now();
131
+ const runDirectory = typeof ctx.runDirectory === "string" && ctx.runDirectory.length > 0
132
+ ? ctx.runDirectory
133
+ : undefined;
134
+ const runId = runDirectory === undefined ? undefined : basenameRunId(runDirectory);
135
+ // Public-invocation scope + selected host from Host envelope only — never
136
+ // courtAttemptId, never sidecar file, never pre-spawn invocation.json I/O.
137
+ const invocationScopeId =
138
+ typeof ctx.invocationScopeId === "string" && ctx.invocationScopeId.trim() !== ""
139
+ ? ctx.invocationScopeId.trim()
140
+ : undefined;
141
+ const host =
142
+ typeof ctx.host === "string" && ctx.host.trim() !== ""
143
+ ? ctx.host.trim()
144
+ : undefined;
145
+
146
+ const recordCall = (observed: {
147
+ code?: number;
148
+ stdoutByteLength?: number;
149
+ }): void => {
150
+ if (typeof sessionParent !== "string" || sessionParent.length === 0) return;
151
+ reportEngineDetourCall({
152
+ toolCallId,
153
+ durationMs: Math.max(0, Date.now() - startedAt),
154
+ cwd: ctx.cwd,
155
+ sessionParent,
156
+ ...(runId === undefined ? {} : { runId }),
157
+ ...(invocationScopeId === undefined ? {} : { invocationScopeId }),
158
+ ...(host === undefined ? {} : { host }),
159
+ ...(observed.code === undefined ? {} : { code: observed.code }),
160
+ ...(observed.stdoutByteLength === undefined
161
+ ? {}
162
+ : { stdoutByteLength: observed.stdoutByteLength }),
163
+ });
164
+ };
165
+
166
+ /** Preserve engine cause; if ledger write also fails, keep both (失败诚实). */
167
+ const failAfterLedger = (
168
+ engineCause: Error,
169
+ observed: { code?: number; stdoutByteLength?: number },
170
+ aggregateMessage: string,
171
+ ): never => {
172
+ try {
173
+ recordCall(observed);
174
+ } catch (recordError) {
175
+ input.fail(
176
+ new AggregateError(
177
+ [
178
+ engineCause,
179
+ asError(recordError, "engine detour usage ledger write failed"),
180
+ ],
181
+ aggregateMessage,
182
+ { cause: engineCause },
183
+ ),
184
+ toolCallId,
185
+ ctx,
186
+ );
187
+ }
188
+ input.fail(engineCause, toolCallId, ctx);
189
+ };
190
+
103
191
  let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
104
192
  try {
105
193
  result = await runEngineDetourOnce({
@@ -109,20 +197,30 @@ export function createEngineDetourToolDefinition(input: {
109
197
  });
110
198
  } catch (error) {
111
199
  if (isCallerCancellation(error, signal)) throw error;
112
- const cause = error instanceof Error
113
- ? error
114
- : new Error(String(error).trim() || "劳务引擎 spawn 失败");
115
- input.fail(cause, toolCallId, ctx);
200
+ // Spawn path: duration only — code/stdout bytes absent (not forged 0).
201
+ return failAfterLedger(
202
+ asError(error, "劳务引擎 spawn 失败"),
203
+ {},
204
+ "engine detour spawn and usage ledger both failed",
205
+ );
116
206
  }
117
207
 
208
+ const stdoutByteLength = engineDetourStdoutByteLength(result.stdout);
209
+ const observed = { code: result.code, stdoutByteLength };
210
+
211
+ // Classify closed-child failure before ledger write so a sitian failure
212
+ // cannot erase nonzero/empty engine facts.
118
213
  if (isEngineDetourFailure(result)) {
119
- input.fail(
214
+ return failAfterLedger(
120
215
  new Error(engineDetourFailureDiagnostic(result)),
121
- toolCallId,
122
- ctx,
216
+ observed,
217
+ "engine detour child-close and usage ledger both failed",
123
218
  );
124
219
  }
125
220
 
221
+ recordCall(observed);
222
+
223
+ // Usage ledger lives in sitian + decisiveFacts only (#537) — not tool details.
126
224
  return {
127
225
  content: [{ type: "text" as const, text: result.stdout }],
128
226
  details: {