@akagilnc/pi-workflow-roles 0.1.4489 → 0.1.4503

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,10 +1,10 @@
1
- import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.js";
1
+ import { engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
2
2
  import { CliUsageError } from "./cli-errors.js";
3
- import { projectCourtTicketNumbers, } from "../diarist-contracts.js";
3
+ import { projectCourtTicketNumbers } from "../diarist-contracts.js";
4
4
  import { isSafePositiveTicketNumber } from "../run-ticket-number.js";
5
- import { admitCountersignInvocation, bindAdmittedTicketNumber, bindCourtTicketNumbersOnAdmitted, buildCountersignTransportPrompt, relocateAdmittedRunToTicket, } from "./invocation.js";
5
+ import { admitCountersignInvocation, bindAdmittedTicketNumber, bindCourtTicketNumbersOnAdmitted, buildCountersignTransportPrompt, freezePreparedAttachmentsIntoRun, materializeCountersignInvocation, relocateAdmittedRunToTicket, withPreparedAttachments, } from "./invocation.js";
6
6
  import { prepareSummonsResumeMaterials, presentControlledFailure, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, StationChildExhaustedError, } from "./post-admission.js";
7
- import { loadResumableCountersignRun, markRunAdmitted, markRunTerminal, } from "./run-lifecycle.js";
7
+ import { loadResumableCountersignRun, markRunAdmitted, } from "./run-lifecycle.js";
8
8
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
9
9
  import { presentStructuralRejection, trySettleCountersignTerminalResult, } from "./settlement.js";
10
10
  import { projectRoleTurnRequest, } from "./turn-request.js";
@@ -36,11 +36,13 @@ function courtTicketNumbersFromOutcome(roleOutcome, principalTicket) {
36
36
  const payloads = roleOutcome.kind === "accepted" ||
37
37
  roleOutcome.kind === "audit_escalation" ||
38
38
  roleOutcome.kind === "failure"
39
- ? roleOutcome.payloads ?? []
39
+ ? (roleOutcome.payloads ?? [])
40
40
  : [];
41
41
  let latest;
42
42
  for (const payload of payloads) {
43
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
43
+ if (typeof payload !== "object" ||
44
+ payload === null ||
45
+ Array.isArray(payload)) {
44
46
  continue;
45
47
  }
46
48
  const record = payload;
@@ -57,12 +59,12 @@ function courtTicketNumbersFromOutcome(roleOutcome, principalTicket) {
57
59
  if (members.length === 0) {
58
60
  // Literal [] is the intentional principal-only replace; non-empty garbage is not.
59
61
  if (raw.length === 0) {
60
- latest = projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
62
+ latest =
63
+ projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
61
64
  }
62
65
  continue;
63
66
  }
64
- latest =
65
- projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
67
+ latest = projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
66
68
  }
67
69
  return latest;
68
70
  }
@@ -75,10 +77,12 @@ function courtDiaristEscalated(roleOutcome) {
75
77
  if (roleOutcome.kind !== "accepted")
76
78
  return false;
77
79
  return (roleOutcome.payloads ?? []).some((payload) => {
78
- if (typeof payload !== "object" || payload === null || Array.isArray(payload))
80
+ if (typeof payload !== "object" ||
81
+ payload === null ||
82
+ Array.isArray(payload))
79
83
  return false;
80
84
  const record = payload;
81
- return record.status === "escalate" || record.countersignStatus === "escalate";
85
+ return (record.status === "escalate" || record.countersignStatus === "escalate");
82
86
  });
83
87
  }
84
88
  /**
@@ -116,13 +120,17 @@ export async function invokeCourtDiarist(input, env, io) {
116
120
  io: quietIo,
117
121
  ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
118
122
  ...(env.signal === undefined ? {} : { signal: env.signal }),
119
- ...(input.correlationId === undefined ? {} : { correlationId: input.correlationId }),
123
+ ...(input.correlationId === undefined
124
+ ? {}
125
+ : { correlationId: input.correlationId }),
120
126
  ...(input.boundTicketNumber === undefined
121
127
  ? {}
122
128
  : { boundTicketNumber: input.boundTicketNumber }),
123
129
  // Child seat selects from the composition-root table. Do not pass the
124
130
  // already-selected parent adapter (#840 / ADR 0082: --host 旗标>席位配置>缺省 pi).
125
- ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
131
+ ...(env.hostAdapters === undefined
132
+ ? {}
133
+ : { hostAdapters: env.hostAdapters }),
126
134
  });
127
135
  const roleOutcome = result.terminal?.roleOutcome;
128
136
  // Escalate routing is a boolean over the preserved sequence (#881). Reasons and
@@ -143,7 +151,8 @@ export async function invokeCourtDiarist(input, env, io) {
143
151
  },
144
152
  };
145
153
  }
146
- const asserted = result.admitted?.ticketNumber;
154
+ const asserted = result.admitted
155
+ ?.ticketNumber;
147
156
  if (isSafePositiveTicketNumber(asserted)) {
148
157
  const courtTicketNumbers = courtTicketNumbersFromOutcome(roleOutcome, asserted);
149
158
  return {
@@ -230,9 +239,14 @@ export async function runPublicCountersign(argv, env, io, parseCountersignArgv)
230
239
  instruction: parsed.instruction,
231
240
  attachmentPaths: parsed.attachmentPaths,
232
241
  ...(parsed.project === undefined ? {} : { project: parsed.project }),
233
- ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
242
+ ...(env.createRunId === undefined
243
+ ? {}
244
+ : { createRunId: env.createRunId }),
234
245
  ...(env.model === undefined ? {} : { model: env.model }),
235
- ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
246
+ ...(env.correlationId === undefined
247
+ ? {}
248
+ : { correlationId: env.correlationId }),
249
+ deferPersistence: true,
236
250
  });
237
251
  }
238
252
  catch (error) {
@@ -242,152 +256,197 @@ export async function runPublicCountersign(argv, env, io, parseCountersignArgv)
242
256
  }
243
257
  throw error;
244
258
  }
245
- await markRunAdmitted(admitted, env.principalAuthority);
246
- // #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
247
- // (never mechanical matching of summons text). Resolve that typed key after
248
- // admit so the countersign run page exists first; same-ticket re-summons
249
- // resume the prior run on the typed key (unused mint is abandoned). The test
250
- // seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
251
- // hook failures stay on the parent call-local budget, exhausted nested
252
- // station children still skip parent auto-resume (#840 父子不层叠).
253
- let typedTicket;
254
- let typedCourtTicketNumbers;
255
- let identityDiaristRan = false;
256
- if (env.runCourtDiaristStation === undefined) {
257
- const outcome = await invokeCourtDiarist({
258
- instruction: parsed.instruction,
259
- projectRoot: admitted.projectRoot,
260
- failureLabel: "unbound summons",
261
- correlationId: admitted.runId,
262
- }, env, io);
263
- identityDiaristRan = true;
264
- if (outcome.identity.kind === "escalate") {
265
- // 御批: 识别不了就上抛 — settle on the admitted countersign run.
266
- return await presentControlledFailure(admitted, {
267
- timedOut: false,
268
- code: null,
269
- stderr: "",
270
- thrown: new Error("court diarist station escalated (cannot identify court target)"),
271
- }, countersignAdapters(), env.principalAuthority, io);
272
- }
273
- // Typed failure terminal (verification / infra / non-zero without escalate)
274
- // is not 真无票 — settle controlled failure on the admitted run (失败诚实).
275
- // Only a true missing lawful typed terminal keeps the r5 unbound-continue.
276
- if (outcome.failedWithoutEscalate !== undefined) {
277
- return await presentControlledFailure(admitted, {
278
- timedOut: false,
279
- code: null,
280
- stderr: "",
281
- thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
282
- }, countersignAdapters(), env.principalAuthority, io);
283
- }
284
- // Missing lawful 起居郎 terminal is not countersign body failure: leave
285
- // unbound and continue (true-unbound face). Escalate / typed failure above;
286
- // bound refresh still fails honest via runCountersignCourtDiaristStation.
287
- if (outcome.identity.kind === "ticket") {
288
- const assertedTicket = outcome.identity.ticketNumber;
289
- typedTicket = assertedTicket;
290
- // #871: identity may hand a co-review set; absent field defaults to [main].
291
- typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
292
- const summons = {
293
- instruction: parsed.instruction,
294
- instructionEmpty: parsed.instruction.trim() === "",
295
- attachmentPaths: parsed.attachmentPaths,
259
+ // One owner spans prepare, lookup/load/resume or new-run materialization.
260
+ // It preserves both causes when its final cleanup also fails.
261
+ try {
262
+ return await withPreparedAttachments(parsed.attachmentPaths, async (preparedAttachments) => {
263
+ const materializeAdmission = async () => {
264
+ await materializeCountersignInvocation(admitted, {
265
+ home: env.home,
266
+ principalAuthority: env.principalAuthority,
267
+ preparedAttachments,
268
+ ...(env.model === undefined ? {} : { model: env.model }),
269
+ });
296
270
  };
297
- const resumed = await tryResumeSameTicketSeatRun({
298
- home: env.home,
299
- projectRoot: admitted.projectRoot,
300
- role: "countersign",
301
- ticketNumber: typedTicket,
302
- freshSummons: env.freshSummons,
303
- summons,
304
- resume: async (runId, materials) => {
305
- // Resume selected: file the provisional run under the asserted ticket
306
- // before abandoning it, so typed identity never leaves an unbound row.
307
- await bindAdmittedTicketNumber(admitted, assertedTicket);
271
+ // #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
272
+ // (never mechanical matching of summons text). Resolve that typed key before
273
+ // materializing a run: same-ticket re-summons write only to the retained run.
274
+ // Controlled failures materialize below so they still have a durable page. The test
275
+ // seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
276
+ // hook failures stay on the parent call-local budget, exhausted nested
277
+ // station children still skip parent auto-resume (#840 父子不层叠).
278
+ let typedTicket;
279
+ let typedCourtTicketNumbers;
280
+ let identityDiaristRan = false;
281
+ if (env.runCourtDiaristStation === undefined) {
282
+ let outcome;
283
+ try {
284
+ // The identity child cannot name the newly minted countersign id as parent:
285
+ // same-ticket lookup may intentionally never materialize that run. A selected
286
+ // retained run receives its normally correlated refresh child during resume.
287
+ outcome = await invokeCourtDiarist({
288
+ instruction: parsed.instruction,
289
+ projectRoot: admitted.projectRoot,
290
+ failureLabel: "unbound summons",
291
+ }, env, io);
292
+ }
293
+ catch (error) {
294
+ await materializeAdmission();
295
+ await markRunAdmitted(admitted, env.principalAuthority);
296
+ return await presentControlledFailure(admitted, { timedOut: false, code: null, stderr: "", thrown: error }, countersignAdapters(), env.principalAuthority, io);
297
+ }
298
+ identityDiaristRan = true;
299
+ if (outcome.identity.kind === "escalate") {
300
+ // 御批: 识别不了就上抛 — materialize and settle this countersign run.
301
+ await materializeAdmission();
302
+ await markRunAdmitted(admitted, env.principalAuthority);
303
+ return await presentControlledFailure(admitted, {
304
+ timedOut: false,
305
+ code: null,
306
+ stderr: "",
307
+ thrown: new Error("court diarist station escalated (cannot identify court target)"),
308
+ }, countersignAdapters(), env.principalAuthority, io);
309
+ }
310
+ // Typed failure terminal (verification / infra / non-zero without escalate)
311
+ // is not 真无票 — settle controlled failure on the admitted run (失败诚实).
312
+ // Only a true missing lawful typed terminal keeps the r5 unbound-continue.
313
+ if (outcome.failedWithoutEscalate !== undefined) {
314
+ await materializeAdmission();
315
+ await markRunAdmitted(admitted, env.principalAuthority);
316
+ return await presentControlledFailure(admitted, {
317
+ timedOut: false,
318
+ code: null,
319
+ stderr: "",
320
+ thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
321
+ }, countersignAdapters(), env.principalAuthority, io);
322
+ }
323
+ // Missing lawful 起居郎 terminal is not countersign body failure: leave
324
+ // unbound and continue (true-unbound face). Escalate / typed failure above;
325
+ // bound refresh still fails honest via runCountersignCourtDiaristStation.
326
+ if (outcome.identity.kind === "ticket") {
327
+ const assertedTicket = outcome.identity.ticketNumber;
328
+ typedTicket = assertedTicket;
329
+ // #871: identity may hand a co-review set; absent field defaults to [main].
330
+ typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
331
+ const summons = {
332
+ instruction: parsed.instruction,
333
+ instructionEmpty: parsed.instruction.trim() === "",
334
+ };
335
+ const resumed = await tryResumeSameTicketSeatRun({
336
+ home: env.home,
337
+ projectRoot: admitted.projectRoot,
338
+ role: "countersign",
339
+ ticketNumber: typedTicket,
340
+ freshSummons: env.freshSummons,
341
+ summons,
342
+ resume: async (runId, materials) => {
343
+ // Consume the same pre-identity snapshot into the retained run; never
344
+ // reopen caller paths after identity has run.
345
+ const retained = await loadResumableCountersignRun(env.home, runId, env.principalAuthority);
346
+ if (retained.admitted === undefined) {
347
+ throw new Error(`retained countersign run disappeared before resume: ${runId}`);
348
+ }
349
+ const frozenPaths = (await freezePreparedAttachmentsIntoRun(preparedAttachments, retained.admitted.runDirectory, `s-${Date.now().toString(36)}`)).map((attachment) => attachment.frozenPath);
350
+ const preparedMaterials = {
351
+ ...(materials ?? {}),
352
+ ...(frozenPaths.length === 0
353
+ ? {}
354
+ : { attachmentPaths: frozenPaths }),
355
+ };
356
+ // Identity 起居郎 asserted unbound (no issue face). Resume still runs
357
+ // the bound refresh station under the typed key (ADR 0075: 每次过庭都跑是调用者用法).
358
+ // #871: hand the identity set so resume can whole-replace the run fact
359
+ // when this summons produced a new typed set (never union).
360
+ return await runPublicCountersignResume({
361
+ runId,
362
+ summons: preparedMaterials,
363
+ }, {
364
+ ...env,
365
+ ...(typedCourtTicketNumbers === undefined
366
+ ? {}
367
+ : { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
368
+ }, io);
369
+ },
370
+ });
371
+ if (resumed !== undefined) {
372
+ return resumed;
373
+ }
374
+ }
375
+ }
376
+ // No prior run was selected. Materialize this invocation now; true-unbound,
377
+ // first-ticket and deferred test-seam paths all retain their own durable page.
378
+ await materializeAdmission();
379
+ await markRunAdmitted(admitted, env.principalAuthority);
380
+ if (identityDiaristRan && typedTicket !== undefined) {
381
+ try {
382
+ await bindAdmittedTicketNumber(admitted, typedTicket);
308
383
  await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
309
- await markRunTerminal(admitted.runDirectory);
310
- // Identity 起居郎 asserted unbound (no issue face). Resume still runs
311
- // the bound refresh station under the typed key (ADR 0075: 每次过庭都跑是调用者用法).
312
- // #871: hand the identity set so resume can whole-replace the run fact
313
- // when this summons produced a new typed set (never union).
314
- return await runPublicCountersignResume({
315
- runId,
316
- ...(materials === undefined ? {} : { summons: materials }),
317
- }, {
318
- ...env,
319
- ...(typedCourtTicketNumbers === undefined
320
- ? {}
321
- : { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
322
- }, io);
384
+ // First court: explicit set wins; omitted field → single-ticket face [main].
385
+ // Set persistence is outside beforeDispatch — route write failures into the
386
+ // same controlled-failure settlement as station children (#871 B7).
387
+ await bindCourtTicketNumbersOnAdmitted(admitted, typedCourtTicketNumbers ?? [typedTicket]);
388
+ }
389
+ catch (error) {
390
+ return await presentControlledFailure(admitted, {
391
+ timedOut: false,
392
+ code: null,
393
+ stderr: "",
394
+ thrown: error,
395
+ }, countersignAdapters(), env.principalAuthority, io);
396
+ }
397
+ }
398
+ const turnProjection = {
399
+ packageRoot: env.packageRoot,
400
+ home: env.home,
401
+ agentDir: env.agentDir,
402
+ ...(env.model === undefined ? {} : { model: env.model }),
403
+ ...pickEngineAxis(env),
404
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
405
+ ...(env.correlationId === undefined || env.correlationId.trim() === ""
406
+ ? {}
407
+ : { correlationId: env.correlationId }),
408
+ continuation: {
409
+ kind: "initial",
410
+ prompt: buildCountersignTransportPrompt(admitted, engineSessionMaterialFromOptions({
411
+ ...pickEngineAxis(env),
412
+ packageRoot: env.packageRoot,
413
+ })),
323
414
  },
415
+ };
416
+ // Mutable shell: ticket bind re-projects activation before executeTurn.
417
+ const turnRequest = buildCountersignTurnRequest(admitted, turnProjection);
418
+ const result = await runPostAdmissionOneShot({
419
+ admitted,
420
+ env,
421
+ io,
422
+ request: turnRequest,
423
+ adapters: countersignAdapters({
424
+ beforeDispatch: async (admittedSeat, lease) => {
425
+ // Dossier pointer delivery rides post-admission after this hook (#709).
426
+ if (!identityDiaristRan) {
427
+ // Test seam (or any deferred identity): station owns assert + bind.
428
+ await runCountersignCourtDiaristStation(admittedSeat, env, io);
429
+ }
430
+ else if (typedTicket !== undefined) {
431
+ await runCountersignCourtDiaristStation(admittedSeat, env, io);
432
+ }
433
+ await relocateAdmittedRunToTicket(admittedSeat, env.principalAuthority, lease);
434
+ Object.assign(turnRequest, buildCountersignTurnRequest(admittedSeat, turnProjection));
435
+ },
436
+ }),
437
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
324
438
  });
325
- if (resumed !== undefined) {
326
- return resumed;
327
- }
328
- }
329
- }
330
- if (identityDiaristRan && typedTicket !== undefined) {
331
- try {
332
- await bindAdmittedTicketNumber(admitted, typedTicket);
333
439
  await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
334
- // First court: explicit set wins; omitted field → single-ticket face [main].
335
- // Set persistence is outside beforeDispatch — route write failures into the
336
- // same controlled-failure settlement as station children (#871 B7).
337
- await bindCourtTicketNumbersOnAdmitted(admitted, typedCourtTicketNumbers ?? [typedTicket]);
338
- }
339
- catch (error) {
340
- return await presentControlledFailure(admitted, {
341
- timedOut: false,
342
- code: null,
343
- stderr: "",
344
- thrown: error,
345
- }, countersignAdapters(), env.principalAuthority, io);
440
+ return result;
441
+ });
442
+ }
443
+ catch (error) {
444
+ if (error instanceof CliUsageError) {
445
+ presentStructuralRejection(error, io);
446
+ return { exitCode: 2 };
346
447
  }
448
+ throw error;
347
449
  }
348
- const turnProjection = {
349
- packageRoot: env.packageRoot,
350
- home: env.home,
351
- agentDir: env.agentDir,
352
- ...(env.model === undefined ? {} : { model: env.model }),
353
- ...pickEngineAxis(env),
354
- ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
355
- ...(env.correlationId === undefined || env.correlationId.trim() === ""
356
- ? {}
357
- : { correlationId: env.correlationId }),
358
- continuation: {
359
- kind: "initial",
360
- prompt: buildCountersignTransportPrompt(admitted, engineSessionMaterialFromOptions({
361
- ...pickEngineAxis(env),
362
- packageRoot: env.packageRoot,
363
- })),
364
- },
365
- };
366
- // Mutable shell: ticket bind re-projects activation before executeTurn.
367
- const turnRequest = buildCountersignTurnRequest(admitted, turnProjection);
368
- const result = await runPostAdmissionOneShot({
369
- admitted,
370
- env,
371
- io,
372
- request: turnRequest,
373
- adapters: countersignAdapters({
374
- beforeDispatch: async (admittedSeat, lease) => {
375
- // Dossier pointer delivery rides post-admission after this hook (#709).
376
- if (!identityDiaristRan) {
377
- // Test seam (or any deferred identity): station owns assert + bind.
378
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
379
- }
380
- else if (typedTicket !== undefined) {
381
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
382
- }
383
- await relocateAdmittedRunToTicket(admittedSeat, env.principalAuthority, lease);
384
- Object.assign(turnRequest, buildCountersignTurnRequest(admittedSeat, turnProjection));
385
- },
386
- }),
387
- ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
388
- });
389
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
390
- return result;
391
450
  }
392
451
  function countersignAdapters(options) {
393
452
  return {
@@ -1,6 +1,6 @@
1
1
  import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.js";
2
2
  import { CliUsageError } from "./cli-errors.js";
3
- import { admitDiaristInvocation, bindAdmittedTicketNumber, buildInstructionTransportPrompt, relocateAdmittedRunToTicket, } from "./invocation.js";
3
+ import { admitDiaristInvocation, bindAdmittedTicketNumber, buildInstructionTransportPrompt, relocateAdmittedRunToTicket, recordAdmittedCorrelation, } from "./invocation.js";
4
4
  import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
5
5
  import { loadResumableDiaristRun, markRunAdmitted, } from "./run-lifecycle.js";
6
6
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
@@ -30,6 +30,9 @@ function diaristAdapters(env) {
30
30
  shouldPresentSettled: () => true,
31
31
  // Resume interrupted state: board ticket already on pages, run still under unbound/.
32
32
  beforeDispatch: async (admitted, lease) => {
33
+ if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
34
+ await recordAdmittedCorrelation(admitted, env.correlationId);
35
+ }
33
36
  await bindAndRelocateDiaristIfBoardBound(admitted, env.principalAuthority, lease);
34
37
  },
35
38
  // First post-turn assert: accept hook wrote board pages; relocate before lease release.