@akagilnc/pi-workflow-roles 0.1.4673 → 0.1.4698

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.
@@ -78,7 +78,11 @@ export type TerminalRoleOutcome =
78
78
  /** Original diagnostic identity retained for the caller. */
79
79
  diagnostic: string;
80
80
  decisiveFacts: Readonly<Record<string, unknown>>;
81
- /** Already-recorded original payloads, coexist with host failure (#836 A3). */
81
+ /**
82
+ * Optional current-failure payloads (rare intentional face, e.g. reviewer
83
+ * child terminals). Run history does not live here — #836 / #953 keep it
84
+ * on TerminalResult.submissions so receivers can tell it from this failure.
85
+ */
82
86
  payloads?: readonly unknown[];
83
87
  };
84
88
 
@@ -165,13 +169,31 @@ export type TerminalGateFact = {
165
169
  * auto-resumes occurred during this single LLM call; it is not persisted to
166
170
  * run-state.json and does not participate in limit decisions.
167
171
  */
168
- /** Original payloads on a terminal — role result for accepted/audit; coexist on failure. */
172
+ /**
173
+ * Current-result payloads on a terminal — accepted/audit role result, or a
174
+ * rare intentional failure face. Run history is TerminalResult.submissions
175
+ * (#836 / #953), not this helper.
176
+ */
169
177
  export function roleResultPayloads(outcome: TerminalRoleOutcome): readonly unknown[] {
170
178
  if (outcome.kind === "accepted" || outcome.kind === "audit_escalation") return outcome.payloads ?? [];
171
179
  if (outcome.kind === "failure") return outcome.payloads ?? [];
172
180
  return [];
173
181
  }
174
182
 
183
+ /**
184
+ * Prefer non-empty current-result payloads; an empty array must not shadow
185
+ * top-level submissions (#953 / #836). Callers that need failure history must
186
+ * read TerminalResult.submissions directly — do not coalesce into failure.payloads.
187
+ */
188
+ export function coalesceSubmissionRows(
189
+ payloads: readonly unknown[] | undefined,
190
+ submissions: readonly unknown[] | undefined,
191
+ ): readonly unknown[] {
192
+ if (payloads !== undefined && payloads.length > 0) return payloads;
193
+ if (submissions !== undefined && submissions.length > 0) return submissions;
194
+ return payloads ?? submissions ?? [];
195
+ }
196
+
175
197
  export type TerminalResult = {
176
198
  roleOutcome: TerminalRoleOutcome;
177
199
  /** Default Reviewer parent: original child Terminals, keyed by frozen axis. */
@@ -187,8 +209,9 @@ export type TerminalResult = {
187
209
  navigator: TerminalNavigatorFact;
188
210
  artifacts: readonly TerminalArtifactRef[];
189
211
  /**
190
- * Mirror of recorded original payloads (same bytes as roleOutcome.payloads).
191
- * Kept so officer/compliance readers share one array with the role-result block.
212
+ * Run-scoped recorded submission history (#836). For accepted/audit this often
213
+ * mirrors roleOutcome.payloads; for failure it is the sole historical carrier
214
+ * (#953 — not copied onto failure.payloads).
192
215
  */
193
216
  submissions?: readonly unknown[];
194
217
  /**
@@ -316,16 +339,28 @@ export function formatTerminalResult(result: TerminalResult): string {
316
339
  }
317
340
  // Role-result block: original payloads, newest first for humans (#961).
318
341
  // Typed payloads/submissions stay ledger order; only this presentation reverses.
319
- const payloads =
320
- result.roleOutcome.kind === "accepted" || result.roleOutcome.kind === "audit_escalation"
321
- ? result.roleOutcome.payloads ?? result.submissions ?? []
322
- : result.roleOutcome.kind === "failure"
323
- ? result.roleOutcome.payloads ?? result.submissions ?? []
342
+ // #953: failure history is the top-level submissions carrier (#836) — present
343
+ // as recorded-submission, never as this-turn submission/receipt.
344
+ if (result.roleOutcome.kind === "failure") {
345
+ const recorded = result.submissions ?? [];
346
+ for (let i = recorded.length - 1; i >= 0; i -= 1) {
347
+ const payload = recorded[i]!;
348
+ const rendered =
349
+ typeof payload === "string" ? payload : JSON.stringify(payload);
350
+ lines.push(`recorded-submission\t${encodeTerminalField(rendered)}`);
351
+ }
352
+ } else {
353
+ const payloads =
354
+ result.roleOutcome.kind === "accepted" ||
355
+ result.roleOutcome.kind === "audit_escalation"
356
+ ? coalesceSubmissionRows(result.roleOutcome.payloads, result.submissions)
324
357
  : result.submissions ?? [];
325
- for (let i = payloads.length - 1; i >= 0; i -= 1) {
326
- const payload = payloads[i]!;
327
- const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
328
- lines.push(`submission\t${encodeTerminalField(rendered)}`);
358
+ for (let i = payloads.length - 1; i >= 0; i -= 1) {
359
+ const payload = payloads[i]!;
360
+ const rendered =
361
+ typeof payload === "string" ? payload : JSON.stringify(payload);
362
+ lines.push(`submission\t${encodeTerminalField(rendered)}`);
363
+ }
329
364
  }
330
365
  return `${lines.join("\n")}\n`;
331
366
  }
@@ -37,6 +37,7 @@ import {
37
37
  } from "./engine-detour.ts";
38
38
  import { engineSessionMaterialFromOptions } from "./package-resources/engine-material.ts";
39
39
  import { registerEngineDetourTool } from "./engine-detour-tool.ts";
40
+ import { readableGateItem } from "./readable-gate-item.ts";
40
41
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
41
42
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
42
43
  import type { AnyCanonicalSkillBinding } from "./canonical-skill-binding.ts";
@@ -1226,18 +1227,15 @@ export function createSecretariatRoleRuntime(
1226
1227
  : { packageRoot: dependencies.packageRoot }),
1227
1228
  });
1228
1229
  const details = projectSecretariatSummonResult(summoned);
1229
- const outcomeKind =
1230
- typeof details.outcomeKind === "string" ? details.outcomeKind : "unknown";
1231
- const contentText =
1232
- outcomeKind === "accepted" || outcomeKind === "audit_escalation"
1233
- ? "给事中回执已送达中书省"
1234
- : outcomeKind === "failure"
1235
- ? "给事中传召失败"
1236
- : outcomeKind === "no_receipt"
1237
- ? "给事中无回执"
1238
- : "给事中传召未得终局";
1230
+ // #953 / #775: parent-visible text from typed details via readableGateItem
1231
+ // sole source — payloads + receipt (latest) already carry 传话 facts.
1239
1232
  return {
1240
- content: [{ type: "text" as const, text: contentText }],
1233
+ content: [
1234
+ {
1235
+ type: "text" as const,
1236
+ text: readableGateItem(details),
1237
+ },
1238
+ ],
1241
1239
  details,
1242
1240
  };
1243
1241
  },
@@ -170,6 +170,7 @@ export function runIdFromRunDirectory(runDirectory: string): string | undefined
170
170
  * Shared parent-directory unique fallback may be adopted only when the
171
171
  * publisher-owned body.runId equals this run directory's runId. Same-run
172
172
  * artifactsDir / runDirectory candidates keep path ownership and skip this.
173
+ * expectedRunId undefined (unparseable run dir) → never bound.
173
174
  */
174
175
  function presentUniqueFallbackBoundToRun(
175
176
  body: Record<string, unknown>,
@@ -180,51 +181,103 @@ function presentUniqueFallbackBoundToRun(
180
181
  }
181
182
 
182
183
  /**
183
- * Read the first present typed terminal artifact for a run directory.
184
- * Order:
184
+ * Sole authority for seam-owned unique error.<uuid>.json candidates.
185
+ * Used by both clearOpposite (settlement) and readRunTerminalArtifact — do not
186
+ * re-enumerate the same-run / parent unique set elsewhere.
187
+ * Ownership:
188
+ * - same-run dirs (artifacts/, runDir): path ownership — all unique names
189
+ * - parent runs/: only body.runId-bound faces; unparseable runId or unreadable body → none
190
+ */
191
+ export async function listSeamOwnedUniqueErrorFacePaths(
192
+ runDirectory: string,
193
+ ): Promise<readonly string[]> {
194
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
195
+ const owned: string[] = await listUniqueErrorFallbackPaths([
196
+ artifactsDir,
197
+ runDirectory,
198
+ ]);
199
+ const expectedRunId = runIdFromRunDirectory(runDirectory);
200
+ for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
201
+ const read = await readTerminalArtifactAtPath(path, "error.json");
202
+ if (read === undefined || read.status !== "present") continue;
203
+ if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId)) continue;
204
+ owned.push(path);
205
+ }
206
+ return owned;
207
+ }
208
+
209
+ type PresentOrUnreadable = Exclude<RunTerminalArtifactRead, { status: "absent" }>;
210
+
211
+ /**
212
+ * Publish contract (#953): only failure publish continues after clearOpposite
213
+ * failure, so a multi-class residue (residual report/audit beside a new error
214
+ * face) means the current settlement is failure. Prefer failure-class faces
215
+ * over success/audit — never filesystem mtime (copy/restore/utimes can lie).
216
+ */
217
+ function failureClassRank(file: RunTerminalArtifactFile): number {
218
+ return file === "error.json" ? 1 : 0;
219
+ }
220
+
221
+ /**
222
+ * Read the current typed terminal artifact for a run directory.
223
+ *
224
+ * Candidate set (publisher-owned faces only):
185
225
  * 1) conventional artifacts/{report,error,audit-incomplete}.json
186
226
  * 2) publisher fixed failure fallbacks (error.settlement.json faces)
187
- * 3) publisher unique error.<uuid>.json fallbacks under same-run dirs
188
- * 4) shared parent-directory unique fallbacks bound by body.runId
227
+ * 3) seam-owned unique error.<uuid>.json via listSeamOwnedUniqueErrorFacePaths
228
+ * (same-run path ownership + parent body.runId binding — shared with clear)
229
+ *
230
+ * Invariant: when more than one present face remains (e.g. clearOpposite failed
231
+ * during failure publish and a fallback was settled beside a residual report),
232
+ * adopt failure-class over success/audit by the publish contract — not mtime.
233
+ * Same-class ties keep candidate enumeration order (conventional before
234
+ * fallbacks before unique).
189
235
  *
190
- * Absence of every known durable face is a valid no-receipt state (not unreadable).
191
- * A present file that cannot be parsed as a usable typed JSON object is unreadable.
236
+ * Unreadable faces never outrank a present face. Parent unique unreadable
237
+ * files never enter the shared enumerator (cannot prove run identity).
238
+ * Absence of every known durable face is a valid no-receipt state.
192
239
  */
193
240
  export async function readRunTerminalArtifact(
194
241
  runDirectory: string,
195
242
  ): Promise<RunTerminalArtifactRead> {
196
243
  const artifactsDir = roleRunArtifactsDirectory(runDirectory);
244
+ const present: Array<Extract<PresentOrUnreadable, { status: "present" }>> = [];
245
+ const unreadable: Array<Extract<PresentOrUnreadable, { status: "unreadable" }>> =
246
+ [];
247
+
248
+ const consider = (read: RunTerminalArtifactRead | undefined): void => {
249
+ if (read === undefined || read.status === "absent") return;
250
+ if (read.status === "present") {
251
+ present.push(read);
252
+ return;
253
+ }
254
+ unreadable.push(read);
255
+ };
256
+
197
257
  for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
198
- const path = join(artifactsDir, file);
199
- const read = await readTerminalArtifactAtPath(path, file);
200
- if (read !== undefined) return read;
258
+ consider(await readTerminalArtifactAtPath(join(artifactsDir, file), file));
201
259
  }
202
260
 
203
- // Publisher settled a durable failure outside the conventional error.json name.
204
261
  for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
205
- const path = join(runDirectory, relative);
206
- const read = await readTerminalArtifactAtPath(path, "error.json");
207
- if (read !== undefined) return read;
262
+ consider(
263
+ await readTerminalArtifactAtPath(join(runDirectory, relative), "error.json"),
264
+ );
208
265
  }
209
266
 
210
- // Same-run unique faces: path ownership is the run itself — no cross-run risk.
211
- for (const path of await listUniqueErrorFallbackPaths([artifactsDir, runDirectory])) {
212
- const read = await readTerminalArtifactAtPath(path, "error.json");
213
- if (read !== undefined) return read;
267
+ // Unique same-run + parent-bound faces: one enumerator shared with clear.
268
+ for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
269
+ consider(await readTerminalArtifactAtPath(path, "error.json"));
214
270
  }
215
271
 
216
- // Shared parent (runs/) unique faces: require publisher runId binding.
217
- const expectedRunId = runIdFromRunDirectory(runDirectory);
218
- for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
219
- const read = await readTerminalArtifactAtPath(path, "error.json");
220
- if (read === undefined) continue;
221
- if (read.status === "present") {
222
- if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId)) continue;
223
- return read;
224
- }
225
- // Unreadable parent unique file cannot prove run identity — do not adopt.
272
+ if (present.length > 0) {
273
+ present.sort(
274
+ (a, b) => failureClassRank(b.file) - failureClassRank(a.file),
275
+ );
276
+ return present[0]!;
277
+ }
278
+ if (unreadable.length > 0) {
279
+ return unreadable[0]!;
226
280
  }
227
-
228
281
  return { status: "absent" };
229
282
  }
230
283
 
@@ -128,6 +128,8 @@ function latestObjectPayload(
128
128
  * Project nested countersign PublicSummonResult onto tool details.
129
129
  * Authority: keep typed terminal kind + payloads/diagnostic intact (gatekeeper
130
130
  * projectOfficerTerminal precedent / ADR 0052 / 失败诚实). No content gate.
131
+ * Parent-visible text uses this details object via readableGateItem directly
132
+ * (#953 — no second isomorphic projection).
131
133
  */
132
134
  export function projectSecretariatSummonResult(
133
135
  summoned: PublicSummonResult,
@@ -178,7 +180,11 @@ export function projectSecretariatSummonResult(
178
180
  }
179
181
 
180
182
  if (roleOutcome.kind === "failure") {
181
- const payloads = Array.isArray(roleOutcome.payloads) ? roleOutcome.payloads : undefined;
183
+ // #953: current failure face is diagnostic/cause/decisiveFacts only.
184
+ // Historical receipts ride terminal.submissions — never as receipt/payloads.
185
+ const submissions = Array.isArray(terminal?.submissions)
186
+ ? terminal.submissions
187
+ : undefined;
182
188
  return {
183
189
  ...base,
184
190
  outcomeKind: "failure",
@@ -187,10 +193,9 @@ export function projectSecretariatSummonResult(
187
193
  ...(roleOutcome.decisiveFacts === undefined
188
194
  ? {}
189
195
  : { decisiveFacts: roleOutcome.decisiveFacts }),
190
- ...(payloads === undefined ? {} : { payloads }),
191
- ...(latestObjectPayload(payloads) === undefined
196
+ ...(submissions === undefined || submissions.length === 0
192
197
  ? {}
193
- : { receipt: latestObjectPayload(payloads) }),
198
+ : { submissions }),
194
199
  };
195
200
  }
196
201