@akagilnc/pi-workflow-roles 0.1.3758 → 0.1.3783

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.
Files changed (63) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +3 -2
  3. package/dist/acp-host/production-host.js +1735 -948
  4. package/dist/collector-config.js +0 -1
  5. package/dist/collector-github.js +199 -2
  6. package/dist/collector-identity.js +128 -41
  7. package/dist/collector-ledger.js +38 -9
  8. package/dist/collector-receipt.js +19 -7
  9. package/dist/collector-role.js +330 -369
  10. package/dist/collector-target.js +169 -0
  11. package/dist/collector-tool-schemas.js +51 -14
  12. package/dist/headless-host/description.js +77 -0
  13. package/dist/headless-host/mcp-relay.mjs +119 -0
  14. package/dist/headless-host/production-host.js +26590 -0
  15. package/dist/host-descriptions.js +44 -3
  16. package/dist/package-contracts/collector-output.js +32 -0
  17. package/dist/package-contracts/terminating-infrastructure.js +13 -12
  18. package/dist/pi/role-turn-host.js +1 -2
  19. package/dist/public-cli/github-remote.js +45 -0
  20. package/dist/public-cli/invocation.js +53 -54
  21. package/dist/public-cli/load-production-external-host.js +19 -0
  22. package/dist/public-cli/load-production-headless-host.js +36 -0
  23. package/dist/public-cli/main.js +857 -310
  24. package/dist/public-cli/option-definitions.js +6 -4
  25. package/dist/public-cli/run-lifecycle.js +3 -3
  26. package/dist/public-cli/settlement.js +83 -6
  27. package/dist/public-role-summons.js +40 -5
  28. package/dist/role-runtime.js +137 -7
  29. package/dist/submission-correctable-error.js +24 -0
  30. package/extensions/role-runtime.ts +0 -1
  31. package/package.json +1 -1
  32. package/scripts/build-package.mjs +28 -0
  33. package/src/acp-host/role-envelope.ts +146 -103
  34. package/src/acp-host/role-turn-host.ts +12 -0
  35. package/src/collector-config.ts +0 -1
  36. package/src/collector-github.ts +236 -2
  37. package/src/collector-identity.ts +148 -40
  38. package/src/collector-ledger.ts +48 -10
  39. package/src/collector-receipt.ts +33 -14
  40. package/src/collector-role.ts +376 -450
  41. package/src/collector-target.ts +207 -0
  42. package/src/collector-tool-schemas.ts +62 -15
  43. package/src/headless-host/description.ts +123 -0
  44. package/src/headless-host/production-host.ts +75 -0
  45. package/src/headless-host/role-turn-host.ts +424 -0
  46. package/src/host-contracts.ts +2 -1
  47. package/src/host-descriptions.ts +53 -6
  48. package/src/package-contracts/collector-output.ts +72 -0
  49. package/src/package-contracts/terminating-infrastructure.ts +24 -13
  50. package/src/pi/role-turn-host.ts +1 -2
  51. package/src/public-cli/cli.ts +12 -3
  52. package/src/public-cli/collector-run.ts +3 -2
  53. package/src/public-cli/github-remote.ts +45 -0
  54. package/src/public-cli/invocation.ts +60 -59
  55. package/src/public-cli/load-production-external-host.ts +29 -0
  56. package/src/public-cli/load-production-headless-host.ts +48 -0
  57. package/src/public-cli/main.ts +5 -0
  58. package/src/public-cli/option-definitions.ts +6 -4
  59. package/src/public-cli/run-lifecycle.ts +2 -2
  60. package/src/public-cli/settlement.ts +82 -6
  61. package/src/public-role-summons.ts +62 -9
  62. package/src/role-runtime.ts +166 -13
  63. package/src/submission-correctable-error.ts +38 -0
@@ -65,6 +65,12 @@ export type PublicSummonRequest = {
65
65
  * Never folded into argv / parent-run lookup keys (ADR 0079 卷宗指针 stays pure).
66
66
  */
67
67
  readonly gateReviewInstruction?: string;
68
+ /**
69
+ * Parent-invocation host axis (#617 / #645). Nested institutional legs inherit
70
+ * the live parent `--host` so a single public entry does not require mutating
71
+ * persistent seat host. Seat model/thinking stay on the officer seat table.
72
+ */
73
+ readonly host?: string;
68
74
  /**
69
75
  * Offline test inject — same face as public CLI env.roleTurnHost. Production
70
76
  * summons leave this unset and use the seat-resolved host.
@@ -211,14 +217,13 @@ async function createSummonEnv(options: {
211
217
  const hostName = options.seat.host ?? "pi";
212
218
  let roleTurnHost = piHost;
213
219
  if (hostName !== "pi") {
214
- const { lookupHostDescription } = await import("./host-descriptions.ts");
215
- const { loadProductionAcpHostFactory } = await import(
216
- "./public-cli/load-production-acp-host.ts"
220
+ const { lookupHostFamily } = await import("./host-descriptions.ts");
221
+ const { loadProductionExternalHostFactory } = await import(
222
+ "./public-cli/load-production-external-host.ts"
217
223
  );
218
- // #729: the description table is the sole host authority — every registered
219
- // key loads the same generic ACP factory (no per-host fork). Unregistered
220
- // keys fail closed before any turn (#510), as the pre-merge fork did.
221
- if (lookupHostDescription(hostName) === undefined) {
224
+ // #729 / #645: description tables are the sole host authority — family
225
+ // dispatch (ACP vs headless) is data-driven. Unregistered keys fail closed.
226
+ if (lookupHostFamily(hostName) === undefined) {
222
227
  throw new Error(
223
228
  `public role summons host unregistered: host=${hostName} seat=${options.role}`,
224
229
  );
@@ -226,7 +231,7 @@ async function createSummonEnv(options: {
226
231
  let hostPromise: Promise<RoleTurnHost> | undefined;
227
232
  roleTurnHost = {
228
233
  executeTurn: async (request) => {
229
- hostPromise ??= loadProductionAcpHostFactory(options.packageRoot, hostName).then(
234
+ hostPromise ??= loadProductionExternalHostFactory(options.packageRoot, hostName).then(
230
235
  (create) =>
231
236
  create({
232
237
  packageRoot: options.packageRoot,
@@ -286,7 +291,10 @@ export async function summonPublicRole(
286
291
  const credentials =
287
292
  options.credentials ?? (await loadCredentialProviders(agentDir));
288
293
  const config = await loadPublicCliConfig(home);
289
- const seat = resolveEffectiveSeat(config, options.role, credentials);
294
+ // Nested legs inherit parent invocation host when provided (#645 review path).
295
+ // Model/thinking stay on the officer seat — host-only inherit (seat-table law).
296
+ const invocation = options.host === undefined ? undefined : { host: options.host };
297
+ const seat = resolveEffectiveSeat(config, options.role, credentials, invocation);
290
298
  const env = {
291
299
  ...(await createSummonEnv({
292
300
  role: options.role,
@@ -412,6 +420,47 @@ export async function summonPublicRole(
412
420
  }
413
421
 
414
422
  /** Gate officer summons: notary/auditor via --source-run; inspector via pointer instruction. */
423
+ /**
424
+ * Read parent invocation host so nested officers inherit live --host only (#645).
425
+ * Gate source runs necessarily own invocation.json — missing page, bad JSON, or
426
+ * bad shape stay loud (failure-honesty). Absent host field is lawful (seat default).
427
+ */
428
+ async function parentInvocationHost(sourceRunDirectory: string): Promise<string | undefined> {
429
+ const { readFile } = await import("node:fs/promises");
430
+ const path = join(sourceRunDirectory, "invocation.json");
431
+ let text: string;
432
+ try {
433
+ text = await readFile(path, "utf8");
434
+ } catch (error) {
435
+ const code =
436
+ error instanceof Error && "code" in error
437
+ ? (error as NodeJS.ErrnoException).code
438
+ : undefined;
439
+ throw new Error(
440
+ `parent invocation.json required for gate source run at ${path}: ${code ?? (error instanceof Error ? error.message : String(error))}`,
441
+ { cause: error },
442
+ );
443
+ }
444
+ let raw: unknown;
445
+ try {
446
+ raw = JSON.parse(text) as unknown;
447
+ } catch (error) {
448
+ throw new Error(
449
+ `parent invocation.json unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
450
+ { cause: error },
451
+ );
452
+ }
453
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
454
+ throw new Error(`parent invocation.json has non-object shape at ${path}`);
455
+ }
456
+ const host = (raw as Record<string, unknown>).host;
457
+ if (host === undefined) return undefined;
458
+ if (typeof host !== "string" || host.trim() === "") {
459
+ throw new Error(`parent invocation.json host must be a non-empty string at ${path}`);
460
+ }
461
+ return host;
462
+ }
463
+
415
464
  export async function summonGateOfficer(options: {
416
465
  readonly officer: "inspector" | "notary" | "auditor";
417
466
  readonly sourceRunDirectory: string;
@@ -452,6 +501,9 @@ export async function summonGateOfficer(options: {
452
501
  submission: options.submission,
453
502
  });
454
503
  }
504
+ // Inherit parent live --host only so review roundtrip stays on one host without
505
+ // mutating persistent seat tables (#645). Officer model/thinking stay seat-owned.
506
+ const parentHost = await parentInvocationHost(options.sourceRunDirectory);
455
507
  const common = {
456
508
  cwd: options.cwd,
457
509
  ...(home === undefined ? {} : { home }),
@@ -462,6 +514,7 @@ export async function summonGateOfficer(options: {
462
514
  ...(gateReviewInstruction === undefined
463
515
  ? {}
464
516
  : { gateReviewInstruction }),
517
+ ...(parentHost === undefined ? {} : { host: parentHost }),
465
518
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
466
519
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
467
520
  } as const;
@@ -43,12 +43,10 @@ import type { AnyCanonicalSkillBinding } from "./canonical-skill-binding.ts";
43
43
  import type { CollectorClock } from "./collector-evidence.ts";
44
44
  import type { CollectorGitHubTransport } from "./collector-github.ts";
45
45
  import {
46
- COLLECTOR_OBSERVE_TOOL,
47
- COLLECTOR_OUTPUT_TOOL,
48
- COLLECTOR_READ_TOOL,
49
- COLLECTOR_REQUEST_TOOL,
50
- COLLECTOR_WAIT_TOOL,
46
+ COLLECTOR_REQUIRED_TOOLS,
47
+ COLLECTOR_TRANSPORT_FLAGS,
51
48
  createCollectorRoleRuntime,
49
+ type CollectorActivation,
52
50
  } from "./collector-role.ts";
53
51
  import type { ComplianceDecision } from "./compliance-transport.ts";
54
52
  import { createDoctorRoleRuntime } from "./doctor-role.ts";
@@ -449,7 +447,9 @@ type ActivationRuntime = {
449
447
  decodeReviewerAdmitted(): ReviewerAdmittedInputs;
450
448
  /** Envelope stores live parent activation for agent_start prompt assembly. */
451
449
  bindReviewerParent(activation: ReviewerActivation): void;
452
- collector: { activate(context: HostContext, event: { reason: string }): Promise<void> };
450
+ collector: {
451
+ activate(context: HostContext, event: { reason: string }): Promise<void>;
452
+ };
453
453
  doctor: { activate(): Promise<void> };
454
454
  notary: {
455
455
  activate(admitted?: import("./notary-role.ts").NotaryAdmittedTicket): Promise<void>;
@@ -629,7 +629,6 @@ export type RoleRuntimeDependencies = {
629
629
  createMergerGitState?(repositoryRoot: string): MergerRoleDependencies["gitState"];
630
630
  auditDoctorCompliance?(options: { context: HostContext; signal?: AbortSignal }): Promise<ComplianceDecision>;
631
631
  createCollectorClock?(): CollectorClock;
632
- collectorPackageExtensionPath?: string;
633
632
  createNavigatorAttendance?(options: { context: HostContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
634
633
  loadNavigatorWorkContext?(options: { context: HostContext; role: string; phase: NavigatorPhase; getFlag?: (name: string) => unknown }): Promise<NavigatorWorkContext>;
635
634
  loadCanonicalSkillBinding?(
@@ -1144,6 +1143,10 @@ export function createRoleRuntimeExtension(
1144
1143
  for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) {
1145
1144
  roleHost.registerFlag(flag.name, flag.definition);
1146
1145
  }
1146
+ // Collector transport flags: shared envelope owns registration (ADR 0018 / #676 E).
1147
+ for (const flag of COLLECTOR_TRANSPORT_FLAGS) {
1148
+ roleHost.registerFlag(flag.name, flag.definition);
1149
+ }
1147
1150
 
1148
1151
  let admitted = false;
1149
1152
  let selectedRole: string | undefined;
@@ -1305,10 +1308,46 @@ export function createRoleRuntimeExtension(
1305
1308
  }),
1306
1309
  };
1307
1310
  }
1311
+ // #676 E / J1: collector materials + drift gates share this envelope hook (no parallel register).
1312
+ if (role === "collector" && activeCollector !== undefined) {
1313
+ const options = event.systemPromptOptions;
1314
+ if (options.skills && options.skills.length > 0) {
1315
+ failInfrastructure(
1316
+ activeCollector.ledger.latchFatal("通进司检测到系统提示中的环境 skills"),
1317
+ ctx,
1318
+ );
1319
+ }
1320
+ if (options.contextFiles && options.contextFiles.length > 0) {
1321
+ failInfrastructure(
1322
+ activeCollector.ledger.latchFatal("通进司检测到系统提示中的环境 context files"),
1323
+ ctx,
1324
+ );
1325
+ }
1326
+ if (
1327
+ typeof options.appendSystemPrompt === "string"
1328
+ && options.appendSystemPrompt.trim().length > 0
1329
+ ) {
1330
+ failInfrastructure(
1331
+ activeCollector.ledger.latchFatal("通进司检测到 appendSystemPrompt 漂移"),
1332
+ ctx,
1333
+ );
1334
+ }
1335
+ if (!collectorFirstDispatchDone) {
1336
+ collectorFirstDispatchDone = true;
1337
+ activeCollector.ledger.recordActivation(activeCollector.clock);
1338
+ }
1339
+ return {
1340
+ systemPrompt: collectorBusiness.assembleMaterials(activeCollector, event.systemPrompt),
1341
+ };
1342
+ }
1308
1343
  });
1309
1344
  roleHost.on("tool_result", async (event) => {
1310
1345
  const role = selectedRole;
1311
1346
  if (role === undefined) return;
1347
+ // #676 E / J1: collector operational bookkeeping on the shared tool_result seam.
1348
+ if (role === "collector" && activeCollector !== undefined) {
1349
+ collectorBusiness.onToolResult(activeCollector, event);
1350
+ }
1312
1351
  const pendingInfra = pendingInfrastructureFailures.get(event.toolCallId);
1313
1352
  const isRoleInfrastructureFailure = pendingInfra !== undefined;
1314
1353
  if (pendingInfra !== undefined) pendingInfrastructureFailures.delete(event.toolCallId);
@@ -1439,6 +1478,16 @@ export function createRoleRuntimeExtension(
1439
1478
  priorFetch = undefined;
1440
1479
  fetchWrapped = false;
1441
1480
  }
1481
+ // #676 J4: collector fatal latch must surface nonzero exit on shutdown (envelope-owned).
1482
+ if (
1483
+ selectedRole === "collector"
1484
+ && activeCollector !== undefined
1485
+ && activeCollector.ledger.fatal
1486
+ ) {
1487
+ if (process.exitCode === undefined || process.exitCode === 0) {
1488
+ process.exitCode = 1;
1489
+ }
1490
+ }
1442
1491
  // Flush any still-pending affirmative attendance before teardown. Accepted
1443
1492
  // grace-timeout paths normally emit on agent_settled; abort can skip that hook.
1444
1493
  const presentation = pendingNavigatorPresentation;
@@ -1630,7 +1679,11 @@ export function createRoleRuntimeExtension(
1630
1679
  completedMerge(mergeCommitId, automaticMergeTreeId) { if (!sessionMergerGitState) throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.completedMerge(mergeCommitId, automaticMergeTreeId); },
1631
1680
  },
1632
1681
  }, hostActions);
1633
- const collector = createCollectorRoleRuntime(
1682
+ // #676 E: shared envelope owns collector lifecycle (mode/fork, tool surface,
1683
+ // event gates). Role module supplies business activate/tools/materials only.
1684
+ let activeCollector: CollectorActivation | undefined;
1685
+ let collectorFirstDispatchDone = false;
1686
+ const collectorBusiness = createCollectorRoleRuntime(
1634
1687
  roleHost,
1635
1688
  {
1636
1689
  async loadSoul() {
@@ -1664,14 +1717,114 @@ export function createRoleRuntimeExtension(
1664
1717
  ...(dependencies.createCollectorClock === undefined
1665
1718
  ? {}
1666
1719
  : { createClock: dependencies.createCollectorClock }),
1667
- ...(dependencies.collectorPackageExtensionPath === undefined
1668
- ? {}
1669
- : {
1670
- packageExtensionPath: dependencies.collectorPackageExtensionPath,
1671
- }),
1672
1720
  },
1673
1721
  hostActions,
1674
1722
  );
1723
+ // #676 J1: business tools + tool_call gate register only behind admission/activation.
1724
+ // before_agent_start / tool_result collector branches live on the shared envelope hooks above.
1725
+ //
1726
+ // #676 J4 dispositions for guards removed from the role-private collector module:
1727
+ // - skills/contextFiles/appendSystemPrompt fail-closed → shared before_agent_start (above).
1728
+ // - ambient skill/prompt/template commands → activate (below).
1729
+ // - session_shutdown fatal exitCode → shared session_shutdown (above).
1730
+ // - subsequent-input latchFatal + fixed-kickoff rewrite → intentionally not migrated:
1731
+ // #676 materials-first multi-turn abolished the single-shot fixed kickoff; multi-turn
1732
+ // observe/request/wait requires later inputs. Judge r1: fixed-kickoff equality delete is authorized.
1733
+ // - tool sourceInfo path override check → intentionally not migrated: depended on
1734
+ // packageExtensionPath deleted under ADR 0018 / #676 E envelope ownership; uniqueness
1735
+ // + setActiveTools inventory checks remain on activate.
1736
+ let collectorToolCallRegistered = false;
1737
+ const collector = {
1738
+ async activate(context: HostContext, event: { reason: string }) {
1739
+ activeCollector = undefined;
1740
+ collectorFirstDispatchDone = false;
1741
+ // Envelope-owned mode / fork-reload gates (not role-private lifecycle).
1742
+ if (context.mode !== "print" && context.mode !== "json") {
1743
+ throw new Error(
1744
+ `Collector supports only print or json mode (got ${context.mode})`,
1745
+ );
1746
+ }
1747
+ if (event.reason === "fork" || event.reason === "reload") {
1748
+ throw new Error(
1749
+ `Collector does not support session_start reason ${event.reason}`,
1750
+ );
1751
+ }
1752
+ // #676 J4: ambient skill/prompt/template commands fail closed at activation.
1753
+ const commands = roleHost.getCommands?.() ?? [];
1754
+ const ambientCommands = commands.filter((command) => {
1755
+ const name = command.name.toLowerCase();
1756
+ return (
1757
+ name.includes("skill")
1758
+ || name.includes("prompt")
1759
+ || name.startsWith("template")
1760
+ );
1761
+ });
1762
+ if (ambientCommands.length > 0) {
1763
+ throw new Error(
1764
+ `Collector detected ambient instruction commands: ${
1765
+ ambientCommands.map((c) => c.name).join(", ")
1766
+ }`,
1767
+ );
1768
+ }
1769
+ // Business tools behind admission barrier (inert-without-role invariant).
1770
+ // First activation: fail closed if a required name is already occupied.
1771
+ // Later activations reuse the once-registered tools (registerBusinessTools is idempotent).
1772
+ const preExisting = roleHost.getAllTools();
1773
+ const alreadyRegistered = COLLECTOR_REQUIRED_TOOLS.every((required) =>
1774
+ preExisting.some((tool) => tool.name === required),
1775
+ );
1776
+ if (!alreadyRegistered) {
1777
+ for (const required of COLLECTOR_REQUIRED_TOOLS) {
1778
+ const prior = preExisting.filter((tool) => tool.name === required);
1779
+ if (prior.length > 0) {
1780
+ throw new Error(`Collector required tool name collision: ${required}`);
1781
+ }
1782
+ }
1783
+ }
1784
+ collectorBusiness.registerBusinessTools(() => activeCollector);
1785
+ const allTools = roleHost.getAllTools();
1786
+ for (const required of COLLECTOR_REQUIRED_TOOLS) {
1787
+ const matches = allTools.filter((tool) => tool.name === required);
1788
+ if (matches.length === 0) {
1789
+ throw new Error(`Collector required tool missing: ${required}`);
1790
+ }
1791
+ if (matches.length > 1) {
1792
+ throw new Error(`Collector required tool name collision: ${required}`);
1793
+ }
1794
+ }
1795
+ roleHost.setActiveTools([...COLLECTOR_REQUIRED_TOOLS]);
1796
+ const active = new Set(roleHost.getActiveTools());
1797
+ for (const required of COLLECTOR_REQUIRED_TOOLS) {
1798
+ if (!active.has(required)) {
1799
+ throw new Error(`Collector failed to activate required tool ${required}`);
1800
+ }
1801
+ }
1802
+ for (const name of active) {
1803
+ if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(name)) {
1804
+ throw new Error(`Collector active tool surface includes unexpected ${name}`);
1805
+ }
1806
+ }
1807
+ // Seat-scoped tool_call gate — registered only after collector admission (no install-time tool_call).
1808
+ if (!collectorToolCallRegistered) {
1809
+ collectorToolCallRegistered = true;
1810
+ roleHost.on("tool_call", (toolEvent) => {
1811
+ if (activeCollector === undefined || selectedRole !== "collector") return;
1812
+ if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(toolEvent.toolName)) {
1813
+ return {
1814
+ block: true,
1815
+ reason: `通进司禁用工具 ${toolEvent.toolName}`,
1816
+ };
1817
+ }
1818
+ return collectorBusiness.onToolCall(activeCollector, toolEvent);
1819
+ });
1820
+ }
1821
+ const activation = await collectorBusiness.activate(context);
1822
+ if (activation.ledger.activationRecorded) {
1823
+ collectorFirstDispatchDone = true;
1824
+ }
1825
+ activeCollector = activation;
1826
+ },
1827
+ };
1675
1828
 
1676
1829
  const clock = dependencies.activationClock ?? (() => new Date().toISOString());
1677
1830
  const writeTrace = dependencies.activationTraceWriter ?? writeActivationTraceRecord;
@@ -32,3 +32,41 @@ export function isCorrectableExecuteError(error: unknown): boolean {
32
32
  || error instanceof WorkerUnfinishedReasonReminderError
33
33
  );
34
34
  }
35
+
36
+ /**
37
+ * Durable projection for ACP envelope tool catches only.
38
+ * Pi path keeps native throw → isError toolResult (no shared projection consumer).
39
+ */
40
+ export type CorrectableExecuteRejectionProjection = {
41
+ readonly diagnostic: string;
42
+ readonly details: Record<string, unknown>;
43
+ };
44
+
45
+ /**
46
+ * One authority for correctable execute → diagnostic text + structured details.
47
+ * Consumed by the ACP envelope tool catch; host adapters wrap transport shape only.
48
+ */
49
+ export function projectCorrectableExecuteRejection(
50
+ error: unknown,
51
+ ): CorrectableExecuteRejectionProjection {
52
+ const diagnostic = error instanceof Error ? error.message : String(error);
53
+ if (error instanceof GatekeeperDecisionError) {
54
+ return { diagnostic, details: { ...(error.result as Record<string, unknown>) } };
55
+ }
56
+ if (
57
+ error instanceof WorkerCommitReminderError
58
+ || error instanceof WorkerPrefixReminderError
59
+ || error instanceof WorkerUnfinishedReasonReminderError
60
+ ) {
61
+ return { diagnostic, details: { code: error.code } };
62
+ }
63
+ if (typeof (error as { code?: unknown }).code === "string") {
64
+ return { diagnostic, details: { code: (error as { code: string }).code } };
65
+ }
66
+ return {
67
+ diagnostic,
68
+ details: {
69
+ code: error instanceof Error && error.name ? error.name : "correctable-submission-error",
70
+ },
71
+ };
72
+ }