@akagilnc/pi-workflow-roles 0.1.2369 → 0.1.2377

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.2369",
3
+ "version": "0.1.2377",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Sole nested-volume reader for gate-cycle facts under session/auditor-roles/.
3
3
  *
4
- * Called only from the Analyst sole ledger scan (classifyScopedRun). Metric
5
- * families must not open a second disk scan — they consume retained facts.
4
+ * Consumers: Analyst sole ledger scan (classifyScopedRun) and Terminal menxia
5
+ * projection (#478). Metric families must not open a second disk scan — they
6
+ * consume retained facts. Terminal settlement reuses this same pairing seam
7
+ * (no second auditor-roles scanner).
6
8
  *
7
9
  * Naming: records may carry pre-#440 menxia/jishizhong/fubaolang tool faces or
8
10
  * the current gatekeeper/inspector/notary English face. Projection always uses
@@ -40,8 +42,20 @@ export type AnalystGateCycleRound = {
40
42
  readonly officerWallMs: number;
41
43
  readonly officerStartedAt: string;
42
44
  readonly officerEndedAt: string;
43
- /** findings[] length only — prose never retained. */
45
+ /**
46
+ * Typed string findings retained from the accepted officer receipt (#478 Terminal
47
+ * projection). Analyst metrics still consume findingsCount only — never prose.
48
+ */
49
+ readonly findings: readonly string[];
50
+ /** findings.length — retained so metric families need not re-derive. */
44
51
  readonly findingsCount: number;
52
+ /** Accepted dispatch receipt status (paired rounds are always "dispatch"). */
53
+ readonly dispatchStatus: string;
54
+ /**
55
+ * Optional seat-reduction reason from the accepted dispatch receipt.
56
+ * Absent when the dispatch did not write a non-empty reason — never invented.
57
+ */
58
+ readonly dispatchReason?: string;
45
59
  };
46
60
 
47
61
  const DISPATCH_TOOLS = new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
@@ -80,6 +94,22 @@ function normalizeOfficerArg(raw: unknown): "inspector" | "notary" | undefined {
80
94
  return OFFICER_ARG_ALIASES[raw.trim()];
81
95
  }
82
96
 
97
+ /** Typed string findings only — non-strings dropped; missing/non-array → []. */
98
+ function asStringFindings(value: unknown): readonly string[] {
99
+ if (!Array.isArray(value)) return [];
100
+ return value.filter((item): item is string => typeof item === "string");
101
+ }
102
+
103
+ /**
104
+ * Optional non-empty dispatch reason. Trim only decides emptiness; a non-empty
105
+ * durable reason is returned as written (gatekeeper keeps reason as-is).
106
+ */
107
+ function optionalDispatchReason(raw: unknown): string | undefined {
108
+ if (typeof raw !== "string") return undefined;
109
+ if (raw.trim() === "") return undefined;
110
+ return raw;
111
+ }
112
+
83
113
  type AcceptedGateToolCall = {
84
114
  readonly toolName: string;
85
115
  readonly args: Record<string, unknown> | undefined;
@@ -177,6 +207,8 @@ type ClassifiedVolume =
177
207
  readonly kind: "dispatch";
178
208
  readonly startedAt: string;
179
209
  readonly officer: "inspector" | "notary";
210
+ readonly status: string;
211
+ readonly reason?: string;
180
212
  }
181
213
  | {
182
214
  readonly kind: "officer";
@@ -184,6 +216,7 @@ type ClassifiedVolume =
184
216
  readonly endedAt: string;
185
217
  readonly officer: "inspector" | "notary";
186
218
  readonly status: string;
219
+ readonly findings: readonly string[];
187
220
  readonly findingsCount: number;
188
221
  readonly officerWallMs: number;
189
222
  };
@@ -200,8 +233,8 @@ async function classifyAuditorVolume(
200
233
 
201
234
  const span = requireAcceptedGateSpan(rows, filePath);
202
235
  const status = requireAcceptedGateStatus(call.args, filePath);
203
- const findings = call.args?.findings;
204
- const findingsCount = Array.isArray(findings) ? findings.length : 0;
236
+ const findings = asStringFindings(call.args?.findings);
237
+ const findingsCount = findings.length;
205
238
 
206
239
  if (DISPATCH_TOOLS.has(call.toolName)) {
207
240
  // Gatekeeper contract terminals: dispatch | incomplete. Incomplete is a
@@ -218,10 +251,13 @@ async function classifyAuditorVolume(
218
251
  `accepted dispatch receipt missing or unknown officer in ${filePath}`,
219
252
  );
220
253
  }
254
+ const reason = optionalDispatchReason(call.args?.reason);
221
255
  return {
222
256
  kind: "dispatch",
223
257
  startedAt: span.startedAt,
224
258
  officer,
259
+ status,
260
+ ...(reason === undefined ? {} : { reason }),
225
261
  };
226
262
  }
227
263
 
@@ -238,6 +274,7 @@ async function classifyAuditorVolume(
238
274
  endedAt: span.endedAt,
239
275
  officer,
240
276
  status,
277
+ findings,
241
278
  findingsCount,
242
279
  officerWallMs: span.wallMs,
243
280
  };
@@ -278,7 +315,10 @@ function pairGateRounds(
278
315
  officerWallMs: match.officer.officerWallMs,
279
316
  officerStartedAt: match.officer.startedAt,
280
317
  officerEndedAt: match.officer.endedAt,
318
+ findings: match.officer.findings,
281
319
  findingsCount: match.officer.findingsCount,
320
+ dispatchStatus: vol.status,
321
+ ...(vol.reason === undefined ? {} : { dispatchReason: vol.reason }),
282
322
  });
283
323
  }
284
324
 
@@ -8,6 +8,11 @@ import { constants as fsConstants } from "node:fs";
8
8
  import { appendFile, lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
9
9
  import { dirname, join } from "node:path";
10
10
 
11
+ import {
12
+ readAnalystGateCyclesFromAuditorRoles,
13
+ type AnalystGateCycleRound,
14
+ } from "../analyst-gate-cycles-read.ts";
15
+
11
16
  import { isAuditEscalationResult } from "../audit-escalation.ts";
12
17
  import { AUDITOR_SOUL_ROLES } from "../auditor-soul.ts";
13
18
  import { DOCTOR_AUDIT_TOOL_NAME } from "../doctor-auditor.ts";
@@ -119,6 +124,8 @@ import {
119
124
  type AuditIncompleteResidual,
120
125
  type ControlledFailureCause,
121
126
  type TerminalArtifactRef,
127
+ type TerminalMenxiaFact,
128
+ type TerminalMenxiaSeat,
122
129
  type TerminalNavigatorFact,
123
130
  type TerminalResult,
124
131
  type TerminalResume,
@@ -2088,21 +2095,27 @@ export async function trySettleComplianceAuditIncompleteTerminalResult(
2088
2095
  outputToolName,
2089
2096
  );
2090
2097
  if (extracted === undefined) return undefined;
2098
+ // Catch covers publication only — menxia read errors keep their session/JSONL identity
2099
+ // and must not be washed into the publication-failure label.
2100
+ let evidence: TerminalArtifactRef;
2091
2101
  try {
2092
- const evidence = await publishComplianceAuditIncompleteEvidence(
2102
+ evidence = await publishComplianceAuditIncompleteEvidence(
2093
2103
  admitted,
2094
2104
  extracted.outcome,
2095
2105
  );
2096
- return {
2097
- roleOutcome: extracted.outcome,
2098
- navigator: extractNavigatorFact(entries),
2099
- artifacts: [evidence],
2100
- runId: admitted.runId,
2101
- };
2102
2106
  } catch (error) {
2103
2107
  // Publication failure is a non-lawful terminal, never an accepted audit result.
2104
2108
  return auditPublicationFailureTerminal(admitted, entries, extracted.outcome, error);
2105
2109
  }
2110
+ return withOptionalMenxiaProjection(
2111
+ {
2112
+ roleOutcome: extracted.outcome,
2113
+ navigator: extractNavigatorFact(entries),
2114
+ artifacts: [evidence],
2115
+ runId: admitted.runId,
2116
+ },
2117
+ admitted.sessionDirectory,
2118
+ );
2106
2119
  }
2107
2120
 
2108
2121
  /** Lawful Judge outcomes extracted from session (never a fabricated failure Receipt). */
@@ -2249,6 +2262,75 @@ function parseNavigatorAttendanceDetails(
2249
2262
  };
2250
2263
  }
2251
2264
 
2265
+ /**
2266
+ * Project paired gate-cycle rounds onto the public Terminal menxia region (#478).
2267
+ * actualSeats are derived only from accepted paired receipts — never from soul
2268
+ * expected/missing officer judgments.
2269
+ */
2270
+ export function projectTerminalMenxiaFact(
2271
+ rounds: readonly AnalystGateCycleRound[],
2272
+ ): TerminalMenxiaFact | undefined {
2273
+ if (rounds.length === 0) return undefined;
2274
+ const seen = new Set<TerminalMenxiaSeat>();
2275
+ // Every paired round implies an accepted gatekeeper dispatch volume.
2276
+ seen.add("gatekeeper");
2277
+ for (const round of rounds) seen.add(round.officer);
2278
+ const actualSeats = (["gatekeeper", "inspector", "notary"] as const).filter(
2279
+ (seat) => seen.has(seat),
2280
+ );
2281
+ return {
2282
+ actualSeats,
2283
+ rounds: rounds.map((round) => ({
2284
+ roundIndex: round.roundIndex,
2285
+ dispatch: {
2286
+ status: round.dispatchStatus,
2287
+ officer: round.officer,
2288
+ ...(round.dispatchReason === undefined
2289
+ ? {}
2290
+ : { reason: round.dispatchReason }),
2291
+ },
2292
+ officer: {
2293
+ seat: round.officer,
2294
+ status: round.status,
2295
+ findings: round.findings,
2296
+ },
2297
+ })),
2298
+ };
2299
+ }
2300
+
2301
+ /**
2302
+ * Read menxia facts from the run's session/auditor-roles nest via the sole
2303
+ * nested-volume reader (#446/#478). Missing directory → undefined (no-gate
2304
+ * zero change). Damaged discovered volumes propagate — never wash to "no gate".
2305
+ */
2306
+ export async function extractMenxiaFactFromSessionDirectory(
2307
+ sessionDirectory: string,
2308
+ ): Promise<TerminalMenxiaFact | undefined> {
2309
+ const rounds = await readAnalystGateCyclesFromAuditorRoles(
2310
+ join(sessionDirectory, "auditor-roles"),
2311
+ );
2312
+ return projectTerminalMenxiaFact(rounds);
2313
+ }
2314
+
2315
+ /**
2316
+ * Attach optional menxia projection onto a settled Terminal base.
2317
+ * Shared by every settle path so auditor-roles is scanned once here only.
2318
+ * `runId` is not required — resumable failures omit it by contract.
2319
+ * Menxia read damage propagates with its real identity (never washed to no-gate
2320
+ * or swallowed); callers that already hold a controlled failure still surface the
2321
+ * JSONL/session cause rather than pretend the gate was absent.
2322
+ */
2323
+ async function withOptionalMenxiaProjection<
2324
+ T extends {
2325
+ roleOutcome: TerminalRoleOutcome;
2326
+ navigator: TerminalNavigatorFact;
2327
+ artifacts: readonly TerminalArtifactRef[];
2328
+ },
2329
+ >(base: T, sessionDirectory: string): Promise<T & { menxia?: TerminalMenxiaFact }> {
2330
+ const menxia = await extractMenxiaFactFromSessionDirectory(sessionDirectory);
2331
+ return menxia === undefined ? base : { ...base, menxia };
2332
+ }
2333
+
2252
2334
  export function extractNavigatorFact(
2253
2335
  entries: readonly SessionEntry[],
2254
2336
  ): TerminalNavigatorFact {
@@ -2580,12 +2662,15 @@ async function settleLawfulJudgeTerminalResult(
2580
2662
  roleOutcome,
2581
2663
  admitted.sessionDirectory,
2582
2664
  );
2583
- return {
2584
- roleOutcome,
2585
- navigator,
2586
- artifacts,
2587
- runId: admitted.runId,
2588
- };
2665
+ return withOptionalMenxiaProjection(
2666
+ {
2667
+ roleOutcome,
2668
+ navigator,
2669
+ artifacts,
2670
+ runId: admitted.runId,
2671
+ },
2672
+ admitted.sessionDirectory,
2673
+ );
2589
2674
  }
2590
2675
 
2591
2676
  /**
@@ -2638,12 +2723,15 @@ async function settleLawfulCoderTerminalResult(
2638
2723
  : { methodProvenance: options.methodProvenance }),
2639
2724
  },
2640
2725
  );
2641
- return {
2642
- roleOutcome: extracted.outcome,
2643
- navigator,
2644
- artifacts,
2645
- runId: admitted.runId,
2646
- };
2726
+ return withOptionalMenxiaProjection(
2727
+ {
2728
+ roleOutcome: extracted.outcome,
2729
+ navigator,
2730
+ artifacts,
2731
+ runId: admitted.runId,
2732
+ },
2733
+ admitted.sessionDirectory,
2734
+ );
2647
2735
  }
2648
2736
 
2649
2737
  /** Settle a lawful Coder Terminal from the admitted session (shared #106 success interface). */
@@ -2847,12 +2935,15 @@ async function settleLawfulFixerTerminalResult(
2847
2935
  methodInvocations,
2848
2936
  },
2849
2937
  );
2850
- return {
2851
- roleOutcome: extracted.outcome,
2852
- navigator,
2853
- artifacts,
2854
- runId: admitted.runId,
2855
- };
2938
+ return withOptionalMenxiaProjection(
2939
+ {
2940
+ roleOutcome: extracted.outcome,
2941
+ navigator,
2942
+ artifacts,
2943
+ runId: admitted.runId,
2944
+ },
2945
+ admitted.sessionDirectory,
2946
+ );
2856
2947
  }
2857
2948
 
2858
2949
  /** Settle a lawful Fixer Terminal from the admitted session (shared #106 success interface). */
@@ -3005,12 +3096,15 @@ async function settleLawfulCollectorTerminalResult(
3005
3096
  admitted.sessionDirectory,
3006
3097
  { collectorReceipt: extracted.receipt },
3007
3098
  );
3008
- return {
3009
- roleOutcome: extracted.outcome,
3010
- navigator,
3011
- artifacts,
3012
- runId: admitted.runId,
3013
- };
3099
+ return withOptionalMenxiaProjection(
3100
+ {
3101
+ roleOutcome: extracted.outcome,
3102
+ navigator,
3103
+ artifacts,
3104
+ runId: admitted.runId,
3105
+ },
3106
+ admitted.sessionDirectory,
3107
+ );
3014
3108
  }
3015
3109
 
3016
3110
  /** Settle a lawful Collector Terminal from the admitted session. */
@@ -3189,12 +3283,15 @@ async function settleLawfulDoctorTerminalResult(
3189
3283
  admitted.sessionDirectory,
3190
3284
  extracted.output === undefined ? {} : { doctorOutput: extracted.output },
3191
3285
  );
3192
- return {
3193
- roleOutcome: extracted.outcome,
3194
- navigator,
3195
- artifacts,
3196
- runId: admitted.runId,
3197
- };
3286
+ return withOptionalMenxiaProjection(
3287
+ {
3288
+ roleOutcome: extracted.outcome,
3289
+ navigator,
3290
+ artifacts,
3291
+ runId: admitted.runId,
3292
+ },
3293
+ admitted.sessionDirectory,
3294
+ );
3198
3295
  }
3199
3296
 
3200
3297
  /** Settle a lawful Doctor Terminal from the admitted session. */
@@ -3283,12 +3380,15 @@ async function settleLawfulNotaryTerminalResult(
3283
3380
  return undefined;
3284
3381
  }
3285
3382
  const navigator = extractNavigatorFact(entries);
3286
- return {
3287
- roleOutcome: extracted.outcome,
3288
- navigator,
3289
- artifacts: [],
3290
- runId: admitted.runId,
3291
- };
3383
+ return withOptionalMenxiaProjection(
3384
+ {
3385
+ roleOutcome: extracted.outcome,
3386
+ navigator,
3387
+ artifacts: [],
3388
+ runId: admitted.runId,
3389
+ },
3390
+ admitted.sessionDirectory,
3391
+ );
3292
3392
  }
3293
3393
 
3294
3394
  /** Settle a lawful Notary Terminal from the admitted session. */
@@ -3549,12 +3649,15 @@ async function settleLawfulReviewerTerminalResult(
3549
3649
  methodInvocations,
3550
3650
  },
3551
3651
  );
3552
- return {
3553
- roleOutcome: extracted.outcome,
3554
- navigator,
3555
- artifacts,
3556
- runId: admitted.runId,
3557
- };
3652
+ return withOptionalMenxiaProjection(
3653
+ {
3654
+ roleOutcome: extracted.outcome,
3655
+ navigator,
3656
+ artifacts,
3657
+ runId: admitted.runId,
3658
+ },
3659
+ admitted.sessionDirectory,
3660
+ );
3558
3661
  }
3559
3662
 
3560
3663
  /** Settle a lawful Reviewer Terminal from the admitted session (shared #106 success interface). */
@@ -3815,12 +3918,15 @@ async function settleLawfulMergerTerminalResult(
3815
3918
  methodInvocations,
3816
3919
  },
3817
3920
  );
3818
- return {
3819
- roleOutcome: extracted.outcome,
3820
- navigator,
3821
- artifacts,
3822
- runId: admitted.runId,
3823
- };
3921
+ return withOptionalMenxiaProjection(
3922
+ {
3923
+ roleOutcome: extracted.outcome,
3924
+ navigator,
3925
+ artifacts,
3926
+ runId: admitted.runId,
3927
+ },
3928
+ admitted.sessionDirectory,
3929
+ );
3824
3930
  }
3825
3931
 
3826
3932
  /** Settle a lawful Merger Terminal from the admitted session (shared #106 success interface). */
@@ -4170,10 +4276,16 @@ export async function settleFailureTerminalResult(
4170
4276
  const facts = parseNoReceiptLifecycleFacts(raw);
4171
4277
  if (facts.runPointer === admitted.runDirectory && facts.attemptPointer === `current:${admitted.runDirectory}`) {
4172
4278
  const decisiveFacts: NoReceiptLifecycleFacts = facts;
4173
- return {
4174
- roleOutcome: { kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt", ...facts, decisiveFacts },
4175
- navigator: await extractNavigatorFactFromAdmittedSession(admitted), artifacts: [], runId: admitted.runId,
4176
- };
4279
+ // #478: no_receipt is still a public Terminal — project accepted gate facts.
4280
+ return withOptionalMenxiaProjection(
4281
+ {
4282
+ roleOutcome: { kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt", ...facts, decisiveFacts },
4283
+ navigator: await extractNavigatorFactFromAdmittedSession(admitted),
4284
+ artifacts: [],
4285
+ runId: admitted.runId,
4286
+ },
4287
+ admitted.sessionDirectory,
4288
+ );
4177
4289
  }
4178
4290
  } catch { /* malformed lifecycle bytes remain the existing nonzero output failure */ }
4179
4291
  }
@@ -4213,12 +4325,16 @@ export async function settleFailureTerminalResult(
4213
4325
  diagnostic: publicDiagnostic,
4214
4326
  decisiveFacts: publicFacts,
4215
4327
  };
4216
- return {
4217
- roleOutcome,
4218
- navigator: redactNavigatorFactForPublicTerminal(navigator, admitted.runId),
4219
- artifacts: [],
4220
- resume: options.resume,
4221
- };
4328
+ // #478: resume desensitization stays; menxia is additive typed fact only.
4329
+ return withOptionalMenxiaProjection(
4330
+ {
4331
+ roleOutcome,
4332
+ navigator: redactNavigatorFactForPublicTerminal(navigator, admitted.runId),
4333
+ artifacts: [],
4334
+ resume: options.resume,
4335
+ },
4336
+ admitted.sessionDirectory,
4337
+ );
4222
4338
  }
4223
4339
  const roleOutcome: TerminalRoleOutcome = {
4224
4340
  kind: "failure",
@@ -4227,12 +4343,16 @@ export async function settleFailureTerminalResult(
4227
4343
  diagnostic: failure.diagnostic,
4228
4344
  decisiveFacts,
4229
4345
  };
4230
- return {
4231
- roleOutcome,
4232
- navigator,
4233
- artifacts,
4234
- runId: admitted.runId,
4235
- };
4346
+ // #478: ordinary controlled failure still surfaces accepted gate facts.
4347
+ return withOptionalMenxiaProjection(
4348
+ {
4349
+ roleOutcome,
4350
+ navigator,
4351
+ artifacts,
4352
+ runId: admitted.runId,
4353
+ },
4354
+ admitted.sessionDirectory,
4355
+ );
4236
4356
  }
4237
4357
 
4238
4358
  /** Judge-named alias retained for #107 call sites. */
@@ -215,6 +215,42 @@ export type TerminalResume = {
215
215
  readonly command: string;
216
216
  };
217
217
 
218
+ /** Current English menxia seat faces projected on Terminal (#478). */
219
+ export type TerminalMenxiaSeat = "gatekeeper" | "inspector" | "notary";
220
+
221
+ /** One accepted province dispatch receipt (status/officer + optional reduce-seat reason). */
222
+ export type TerminalMenxiaDispatch = {
223
+ readonly status: string;
224
+ readonly officer: "inspector" | "notary";
225
+ /** Present only when the accepted dispatch wrote a non-empty reason. */
226
+ readonly reason?: string;
227
+ };
228
+
229
+ /** One accepted officer report: seat, status, full typed findings. */
230
+ export type TerminalMenxiaOfficerReport = {
231
+ readonly seat: "inspector" | "notary";
232
+ readonly status: string;
233
+ readonly findings: readonly string[];
234
+ };
235
+
236
+ /** One paired dispatch↔officer gate round on the public Terminal. */
237
+ export type TerminalMenxiaRound = {
238
+ readonly roundIndex: number;
239
+ readonly dispatch: TerminalMenxiaDispatch;
240
+ readonly officer: TerminalMenxiaOfficerReport;
241
+ };
242
+
243
+ /**
244
+ * Optional menxia province projection (#478).
245
+ * Absent when no accepted paired gate rounds exist (no-gate zero change).
246
+ * Only durable accepted child receipts — never soul-derived expected/missing seats.
247
+ */
248
+ export type TerminalMenxiaFact = {
249
+ /** Seats that actually ran, derived from accepted paired receipts. */
250
+ readonly actualSeats: readonly TerminalMenxiaSeat[];
251
+ readonly rounds: readonly TerminalMenxiaRound[];
252
+ };
253
+
218
254
  /** Public free-text stand-in when an exact Role run ID is stripped outside resume.command. */
219
255
  export const REDACTED_RUN_ID_TOKEN = "[run-id]" as const;
220
256
 
@@ -240,6 +276,11 @@ export type TerminalResult = {
240
276
  roleOutcome: TerminalRoleOutcome;
241
277
  navigator: TerminalNavigatorFact;
242
278
  artifacts: readonly TerminalArtifactRef[];
279
+ /**
280
+ * Optional menxia province facts (#478). Present only when accepted paired
281
+ * gate rounds exist under session/auditor-roles; omitted on no-gate runs.
282
+ */
283
+ menxia?: TerminalMenxiaFact;
243
284
  /** Call-local auto-resume observation (0..2) for this single LLM call; read-only, not persisted. */
244
285
  autoResumeCount?: number;
245
286
  } & (
@@ -330,6 +371,22 @@ export function formatTerminalResult(result: TerminalResult): string {
330
371
  for (const artifact of result.artifacts) {
331
372
  lines.push(`artifact\t${artifact.kind}\t${encodeTerminalField(artifact.path)}`);
332
373
  }
374
+ // Menxia region is unfrozen presentation of typed facts (ADR 0052) — machines
375
+ // read result.menxia, never these row labels.
376
+ if (result.menxia !== undefined) {
377
+ lines.push(
378
+ `menxia\t${encodeTerminalField(result.menxia.actualSeats.join(","))}\t${result.menxia.rounds.length}`,
379
+ );
380
+ for (const round of result.menxia.rounds) {
381
+ const reason =
382
+ round.dispatch.reason === undefined
383
+ ? ""
384
+ : encodeTerminalField(round.dispatch.reason);
385
+ lines.push(
386
+ `menxia-round\t${round.roundIndex}\t${round.dispatch.status}\t${round.dispatch.officer}\t${reason}\t${round.officer.seat}\t${encodeTerminalField(round.officer.status)}\t${encodeTerminalField(JSON.stringify(round.officer.findings))}`,
387
+ );
388
+ }
389
+ }
333
390
  if (result.resume !== undefined) {
334
391
  // Resumable failure: run ID is revealed only inside the complete resume command.
335
392
  lines.push(`resume\t${encodeTerminalField(result.resume.command)}`);