@akagilnc/pi-workflow-roles 0.1.5365 → 0.1.5375

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 (170) hide show
  1. package/dist/acp-host/production-host.js +5585 -5655
  2. package/dist/activation-ledger-git.js +2 -2
  3. package/dist/activation-ledger-topology.js +1 -7
  4. package/dist/analyst-entry.js +4 -8
  5. package/dist/analyst-gate-cycles-read.js +3 -11
  6. package/dist/analyst-index.js +3 -9
  7. package/dist/analyst-ledger.js +1 -11
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +1 -3
  9. package/dist/analyst-metric-families/round-timeline.js +1 -3
  10. package/dist/analyst-page.js +2 -2
  11. package/dist/archivist-record-topology.js +2 -2
  12. package/dist/audit-escalation.js +2 -1
  13. package/dist/auditor-soul.js +2 -1
  14. package/dist/collector-config.js +6 -8
  15. package/dist/compliance-transport.js +2 -1
  16. package/dist/countersign-contracts.js +2 -1
  17. package/dist/diarist-contracts.js +3 -2
  18. package/dist/doctor-contracts.js +1 -3
  19. package/dist/doctor-evidence.js +7 -8
  20. package/dist/dossier-resolution.js +1 -3
  21. package/dist/engine-detour-usage.js +1 -3
  22. package/dist/gatekeeper-role.js +2 -9
  23. package/dist/gleaner-left-contracts.js +2 -1
  24. package/dist/headless-host/description.js +14 -17
  25. package/dist/headless-host/production-host.js +1665 -1737
  26. package/dist/host-session-record.js +3 -2
  27. package/dist/inspector-contracts.js +2 -1
  28. package/dist/ledger-session-read.js +1 -3
  29. package/dist/merger-contracts.js +9 -9
  30. package/dist/migrate-book-topology.js +348 -314
  31. package/dist/navigator-attendance.js +9 -11
  32. package/dist/navigator-invocation-identity.js +3 -2
  33. package/dist/navigator-public-session.js +3 -5
  34. package/dist/navigator-session-contracts.js +8 -10
  35. package/dist/navigator-work-base.js +5 -8
  36. package/dist/notary-contracts.js +3 -5
  37. package/dist/package-contracts/auditor-output.js +1 -3
  38. package/dist/package-contracts/fixer-packet.js +3 -6
  39. package/dist/package-contracts/gatekeeper-output.js +1 -3
  40. package/dist/package-contracts/navigator-output.js +8 -8
  41. package/dist/package-contracts/terminating-infrastructure.js +5 -9
  42. package/dist/package-contracts/terminating-tools.js +4 -3
  43. package/dist/public-cli/analyst-run.js +3 -2
  44. package/dist/public-cli/auto-resume.js +4 -9
  45. package/dist/public-cli/cli.js +66 -64
  46. package/dist/public-cli/config.js +12 -11
  47. package/dist/public-cli/countersign-run.js +1 -3
  48. package/dist/public-cli/host-providers.js +7 -14
  49. package/dist/public-cli/instruction-seat-run.js +69 -43
  50. package/dist/public-cli/invocation.js +141 -136
  51. package/dist/public-cli/main.js +1919 -1968
  52. package/dist/public-cli/post-admission.js +89 -89
  53. package/dist/public-cli/role-seat-runtime.js +29 -0
  54. package/dist/public-cli/run-lifecycle.js +20 -32
  55. package/dist/public-cli/settlement.js +10 -26
  56. package/dist/public-cli/turn-request.js +14 -0
  57. package/dist/public-role-summons.js +23 -35
  58. package/dist/receipt-delivery-policy.js +1 -3
  59. package/dist/role-run-placement.js +7 -5
  60. package/dist/role-run-relocation.js +12 -19
  61. package/dist/run-terminal-artifacts.js +1 -11
  62. package/dist/run-ticket-number.js +2 -4
  63. package/dist/session-assistant-usage.js +2 -1
  64. package/dist/session-dialogue.js +1 -14
  65. package/dist/sitian-reader.js +2 -4
  66. package/dist/submission-correctable-error.js +2 -1
  67. package/dist/submission-errors.js +2 -1
  68. package/dist/submission-gate.js +2 -1
  69. package/dist/submission-ledger.js +4 -5
  70. package/dist/ticket-provenance-contracts.js +1 -3
  71. package/dist/ticket-provenance.js +2 -1
  72. package/dist/typed-provider-http.js +4 -7
  73. package/dist/unknown-value.js +28 -0
  74. package/dist/user-dialogue-stdin.js +2 -1
  75. package/package.json +1 -1
  76. package/src/acp-host/role-turn-host.ts +4 -2
  77. package/src/activation-ledger-git.ts +3 -2
  78. package/src/activation-ledger-topology.ts +4 -12
  79. package/src/analyst-entry.ts +3 -11
  80. package/src/analyst-gate-cycles-read.ts +3 -15
  81. package/src/analyst-index.ts +3 -13
  82. package/src/analyst-ledger.ts +2 -16
  83. package/src/analyst-metric-families/acceptance-success-rework.ts +2 -4
  84. package/src/analyst-metric-families/round-timeline.ts +2 -4
  85. package/src/analyst-page.ts +2 -2
  86. package/src/archivist-record-topology.ts +2 -2
  87. package/src/audit-escalation.ts +2 -1
  88. package/src/auditor-soul.ts +3 -1
  89. package/src/book-topology-migration-placement.ts +8 -21
  90. package/src/book-topology-migrators-t11.ts +16 -15
  91. package/src/book-topology-mixed-volume-migrators.ts +3 -6
  92. package/src/book-topology-record-class-migrators.ts +7 -10
  93. package/src/book-topology-runs-migrator.ts +4 -3
  94. package/src/collector-config.ts +7 -9
  95. package/src/compliance-transport.ts +2 -1
  96. package/src/countersign-contracts.ts +2 -1
  97. package/src/diarist-contracts.ts +3 -2
  98. package/src/doctor-contracts.ts +3 -1
  99. package/src/doctor-evidence.ts +8 -7
  100. package/src/doctor-role.ts +4 -2
  101. package/src/dossier-resolution.ts +2 -4
  102. package/src/engine-detour-tool.ts +0 -2
  103. package/src/engine-detour-usage.ts +2 -4
  104. package/src/external-host-turn-loop.ts +3 -1
  105. package/src/factory-board.ts +1 -8
  106. package/src/gatekeeper-role.ts +2 -10
  107. package/src/gleaner-left-contracts.ts +3 -1
  108. package/src/headless-host/description.ts +15 -18
  109. package/src/headless-host/role-turn-host.ts +10 -9
  110. package/src/host-session-record.ts +4 -2
  111. package/src/inspector-contracts.ts +2 -1
  112. package/src/ledger-session-read.ts +2 -5
  113. package/src/merger-contracts.ts +10 -9
  114. package/src/navigator-attendance.ts +10 -12
  115. package/src/navigator-invocation-identity.ts +3 -2
  116. package/src/navigator-public-session.ts +4 -6
  117. package/src/navigator-session-contracts.ts +8 -11
  118. package/src/navigator-work-base.ts +5 -8
  119. package/src/navigator-work-context.ts +2 -1
  120. package/src/notary-contracts.ts +4 -6
  121. package/src/oauth-keepalive.ts +5 -18
  122. package/src/package-contracts/auditor-output.ts +2 -4
  123. package/src/package-contracts/fixer-packet.ts +4 -6
  124. package/src/package-contracts/gatekeeper-output.ts +2 -4
  125. package/src/package-contracts/judge-output.ts +0 -1
  126. package/src/package-contracts/navigator-output.ts +8 -8
  127. package/src/package-contracts/terminating-infrastructure.ts +6 -20
  128. package/src/package-contracts/terminating-tools.ts +8 -12
  129. package/src/pi/adapter.ts +0 -1
  130. package/src/pi/role-turn-host.ts +0 -1
  131. package/src/public-cli/analyst-run.ts +4 -2
  132. package/src/public-cli/auto-resume.ts +5 -12
  133. package/src/public-cli/cli.ts +77 -83
  134. package/src/public-cli/config.ts +16 -27
  135. package/src/public-cli/countersign-run.ts +2 -4
  136. package/src/public-cli/host-providers.ts +10 -27
  137. package/src/public-cli/instruction-seat-run.ts +87 -58
  138. package/src/public-cli/invocation.ts +159 -172
  139. package/src/public-cli/post-admission.ts +115 -103
  140. package/src/public-cli/role-seat-runtime.ts +72 -0
  141. package/src/public-cli/run-lifecycle.ts +27 -67
  142. package/src/public-cli/settlement.ts +11 -33
  143. package/src/public-cli/terminal.ts +2 -17
  144. package/src/public-cli/turn-request.ts +26 -0
  145. package/src/public-role-summons.ts +24 -48
  146. package/src/receipt-delivery-policy.ts +1 -4
  147. package/src/reviewer-role.ts +0 -1
  148. package/src/role-envelope.ts +5 -3
  149. package/src/role-run-placement.ts +9 -10
  150. package/src/role-run-relocation.ts +13 -23
  151. package/src/role-runtime.ts +9 -15
  152. package/src/run-terminal-artifacts.ts +2 -16
  153. package/src/run-ticket-number.ts +4 -10
  154. package/src/session-assistant-usage.ts +3 -1
  155. package/src/session-dialogue.ts +1 -4
  156. package/src/session-identity.ts +0 -1
  157. package/src/sitian-reader.ts +2 -4
  158. package/src/submission-correctable-error.ts +3 -1
  159. package/src/submission-errors.ts +2 -1
  160. package/src/submission-gate.ts +3 -1
  161. package/src/submission-ledger.ts +5 -6
  162. package/src/ticket-provenance-contracts.ts +1 -4
  163. package/src/ticket-provenance.ts +3 -1
  164. package/src/ticket-snapshot.ts +2 -4
  165. package/src/ticket-trajectory.ts +1 -8
  166. package/src/typed-provider-http.ts +5 -15
  167. package/src/unknown-value.ts +34 -0
  168. package/src/user-dialogue-stdin.ts +3 -1
  169. package/src/worker-role.ts +2 -5
  170. package/src/worker-submission-gates.ts +5 -7
@@ -1,6 +1,8 @@
1
- import { createHash } from "node:crypto";
2
1
  import { readFile } from "node:fs/promises";
3
2
 
3
+ import { sha256Hex } from "./sha256.ts";
4
+ import { isRecord } from "./unknown-value.ts";
5
+
4
6
  export const COLLECTOR_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
5
7
  export const COLLECTOR_REPO_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
6
8
 
@@ -51,17 +53,13 @@ export function parseCollectorPrNumber(raw: unknown): number {
51
53
  return value;
52
54
  }
53
55
 
54
- function record(value: unknown): value is Record<string, unknown> {
55
- return typeof value === "object" && value !== null && !Array.isArray(value);
56
- }
57
-
58
56
  function canonicalManifest(requests: readonly CollectorRequestConfig[]): string {
59
57
  return `${JSON.stringify({ requests: requests.map((request) => ({ id: request.id, body: request.requestBody })) })}\n`;
60
58
  }
61
59
 
62
60
  export function emptyCollectorManifest(): CollectorManifest {
63
61
  const canonicalJson = canonicalManifest([]);
64
- return { requests: [], canonicalJson, digest: createHash("sha256").update(canonicalJson).digest("hex") };
62
+ return { requests: [], canonicalJson, digest: sha256Hex(canonicalJson) };
65
63
  }
66
64
 
67
65
  /** Optional request configuration. It names requests, never expected observers. */
@@ -70,17 +68,17 @@ export async function loadCollectorManifest(path: string): Promise<CollectorMani
70
68
  try { bytes = await readFile(path); } catch (error) { fail(`Collector request manifest is unreadable at ${path}`, error); }
71
69
  let parsed: unknown;
72
70
  try { parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); } catch (error) { fail("Collector request manifest must be UTF-8 JSON", error); }
73
- if (!record(parsed)) fail("Collector request manifest must be an object");
71
+ if (!isRecord(parsed)) fail("Collector request manifest must be an object");
74
72
  const rawRequests = parsed.requests ?? [];
75
73
  if (!Array.isArray(rawRequests)) fail("Collector request manifest requests must be an array");
76
74
  const requests: CollectorRequestConfig[] = [];
77
75
  const ids = new Set<string>();
78
76
  for (const [index, item] of rawRequests.entries()) {
79
- if (!record(item) || typeof item.id !== "string" || item.id.length === 0 || typeof item.body !== "string" || item.body.trim() === "") fail(`Collector request manifest requests[${index}] is invalid`);
77
+ if (!isRecord(item) || typeof item.id !== "string" || item.id.length === 0 || typeof item.body !== "string" || item.body.trim() === "") fail(`Collector request manifest requests[${index}] is invalid`);
80
78
  if (ids.has(item.id)) fail(`Collector request manifest has duplicate request id "${item.id}"`);
81
79
  ids.add(item.id);
82
80
  requests.push({ id: item.id, requestBody: item.body });
83
81
  }
84
82
  const canonicalJson = canonicalManifest(requests);
85
- return { requests, canonicalJson, digest: createHash("sha256").update(canonicalJson).digest("hex"), sourcePath: path };
83
+ return { requests, canonicalJson, digest: sha256Hex(canonicalJson), sourcePath: path };
86
84
  }
@@ -8,6 +8,7 @@ import { officerConclusionReask } from "./gatekeeper-role.ts";
8
8
  import { receivedDiscriminator } from "./submission-errors.ts";
9
9
  import { coalesceSubmissionRows } from "./public-cli/terminal.ts";
10
10
  import { readableGateItem } from "./readable-gate-item.ts";
11
+ import { isRecord } from "./unknown-value.ts";
11
12
 
12
13
  export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
13
14
  /**
@@ -63,7 +64,7 @@ function readListField(value: unknown): readonly unknown[] { return Array.isArra
63
64
 
64
65
  /** Try to project a known three-state compliance decision; undefined when not converged/continue/escalate. */
65
66
  export function tryReadComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision | undefined {
66
- if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
67
+ if (!isRecord(arguments_)) {
67
68
  return undefined;
68
69
  }
69
70
  const args = arguments_ as Record<string, unknown>;
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
9
+ import { isRecord } from "./unknown-value.ts";
9
10
 
10
11
  export const COUNTERSIGN_OUTPUT_TOOL_NAME = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
11
12
 
@@ -25,7 +26,7 @@ export type CountersignVerdict =
25
26
  };
26
27
 
27
28
  export function validateRecordedCountersignOutput(verdict: unknown): CountersignVerdict {
28
- if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) {
29
+ if (!isRecord(verdict)) {
29
30
  throw new Error("Countersign verdict has no execution discriminator");
30
31
  }
31
32
  let status: unknown;
@@ -12,6 +12,7 @@ import {
12
12
  type TicketProvenanceSession,
13
13
  } from "./ticket-provenance-contracts.ts";
14
14
  import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
15
+ import { isRecord } from "./unknown-value.ts";
15
16
 
16
17
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
17
18
 
@@ -43,7 +44,7 @@ export type DiaristOutput =
43
44
  };
44
45
 
45
46
  export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
46
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
47
+ if (!isRecord(value)) {
47
48
  throw new Error("Diarist output has no execution discriminator");
48
49
  }
49
50
  let status: unknown;
@@ -78,7 +79,7 @@ export function projectDiaristTicketSessions(
78
79
  if (!Array.isArray(raw)) return undefined;
79
80
  const perTicket = new Map<number, TicketProvenanceSession[]>();
80
81
  for (const entry of raw) {
81
- if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return undefined;
82
+ if (!isRecord(entry)) return undefined;
82
83
  const { ticketNumber, sessions: rawSessions } = entry as Record<string, unknown>;
83
84
  if (!isSafePositiveTicketNumber(ticketNumber)) return undefined;
84
85
  const sessions = projectTicketProvenanceSessions(rawSessions);
@@ -3,6 +3,8 @@ import { canonicalJson } from "./canonical-json.ts";
3
3
  import { openToolObject } from "./open-tool-schema.ts";
4
4
  import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
5
5
 
6
+ import { isRecord } from "./unknown-value.ts";
7
+
6
8
  export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
7
9
  export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
8
10
  export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
@@ -85,7 +87,7 @@ export const doctorSubmissionSchema = withTerminatingOutputDeclarations(
85
87
  // consumes extra fields, so a closed object only rejects the role for saying more.
86
88
  export const doctorEvidenceReadSchema = Type.Object({ evidenceId: Type.String({ minLength: 1, description: "待读留存证据标识" }), offset: Type.Optional(Type.Integer({ minimum: 0, description: "起始字节偏移(从 0 计)" })), limit: Type.Optional(Type.Integer({ minimum: 1, description: "返回字节数(无上限)" })) }, { additionalProperties: true });
87
89
  export class DoctorSubmissionContractError extends Error { override readonly name = "DoctorSubmissionContractError"; }
88
- function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
90
+
89
91
  function read(value: unknown, key: string): unknown { if (!isRecord(value)) return undefined; try { return value[key]; } catch { return undefined; } }
90
92
  export function validateDoctorSubmissionShape(value: unknown): DoctorSubmission {
91
93
  return value as DoctorSubmission;
@@ -4,26 +4,27 @@ import { sha256Hex } from "./sha256.ts";
4
4
  import type { DoctorCase, DoctorCaseCost, DoctorCount, DoctorEvidenceEntry } from "./doctor-contracts.ts";
5
5
  import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, validateAcceptedDetails } from "./package-contracts/terminating-tools.ts";
6
6
 
7
- function record(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
7
+ import { isRecord, isMissingPathError } from "./unknown-value.ts";
8
+
8
9
  async function discoverCaseFiles(root: string): Promise<string[]> { const found: string[] = []; async function walk(dir: string, depth: number) { for (const item of await readdir(dir, { withFileTypes: true })) { const path = resolve(dir, item.name); if (item.isDirectory()) { await walk(path, depth + 1); } else if (item.isFile() && (item.name.endsWith(".jsonl") || (item.name === "stderr.log" && depth === 1))) found.push(path); } } await walk(root, 0); return found.sort(); }
9
10
  function sourceList(count: number, sources: string[]) { return { count, sources: [...new Set(sources)].sort() }; }
10
11
  function accumulate(metric: DoctorCount, value: number, source: string) { metric.count += value; if (value) metric.sources.push(source); }
11
12
  function timestamp(row: Record<string, unknown>) { return typeof row.timestamp === "string" && Number.isFinite(Date.parse(row.timestamp)) ? row.timestamp : undefined; }
12
- function isMissingPathError(error: unknown): boolean { return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"); }
13
+
13
14
  async function stableRunsIdentity(root: string): Promise<string> { let cursor = root; while (true) { try { const git = await stat(resolve(cursor, ".git")); if (git.isDirectory() || git.isFile()) return relative(cursor, root).split(sep).join("/"); } catch (error) { if (!isMissingPathError(error)) throw error; } const parent = dirname(cursor); if (parent === cursor) return root; cursor = parent; } }
14
15
 
15
16
  type SessionDerivation = { session: DoctorCaseCost["sessions"][number]; turns: number; calls: number; tokens: number; statuses: DoctorCaseCost["statuses"]; commits: DoctorCaseCost["commits"] };
16
17
  function deriveSession(content: string, id: string): SessionDerivation {
17
18
  const rows: Record<string, unknown>[] = []; const degradationReasons: string[] = [];
18
- for (const line of content.split("\n")) if (line.trim()) { try { const row: unknown = JSON.parse(line); if (!record(row)) { degradationReasons.push(`non-object session row in ${id}`); break; } rows.push(row); } catch (error) { if (error instanceof SyntaxError) { degradationReasons.push(`malformed JSON tail in ${id}: ${error.message}`); break; } throw error; } }
19
+ for (const line of content.split("\n")) if (line.trim()) { try { const row: unknown = JSON.parse(line); if (!isRecord(row)) { degradationReasons.push(`non-object session row in ${id}`); break; } rows.push(row); } catch (error) { if (error instanceof SyntaxError) { degradationReasons.push(`malformed JSON tail in ${id}: ${error.message}`); break; } throw error; } }
19
20
  const started = rows.find((row) => row.type === "session"); const startedAt = started && timestamp(started); if (!startedAt) degradationReasons.push(`Pi session header is missing: ${id}`);
20
21
  let accepted: Record<string, unknown> | undefined, observedCommit: string | undefined, turns = 0, calls = 0, tokens = 0;
21
22
  const statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
22
23
  for (const row of rows) {
23
- if (row.type === "custom" && row.customType === "ak-role-submission-closure" && record(row.data) && row.data.isError === false && typeof row.data.toolName === "string" && isTerminatingToolName(row.data.toolName)) accepted = row;
24
- const message = record(row.message) ? row.message : undefined;
25
- if (message?.role === "assistant") { for (const part of Array.isArray(message.content) ? message.content : []) if (record(part) && part.type === "toolCall") calls++; if (typeof message.responseId === "string") { turns++; const usage = record(message.usage) ? message.usage : undefined; if (usage && typeof usage.output === "number") tokens += usage.output; } }
26
- if (message?.role === "toolResult" && message.isError !== true && typeof message.toolName === "string" && isTerminatingToolName(message.toolName) && record(message.details)) { let details; try { details = validateAcceptedDetails(message.toolName, message.details); } catch (error) { // Contract: README.md#Doctor — non-accepted terminating receipts are expected-negative evidence and are skipped; all other validation failures propagate with their cause.
24
+ if (row.type === "custom" && row.customType === "ak-role-submission-closure" && isRecord(row.data) && row.data.isError === false && typeof row.data.toolName === "string" && isTerminatingToolName(row.data.toolName)) accepted = row;
25
+ const message = isRecord(row.message) ? row.message : undefined;
26
+ if (message?.role === "assistant") { for (const part of Array.isArray(message.content) ? message.content : []) if (isRecord(part) && part.type === "toolCall") calls++; if (typeof message.responseId === "string") { turns++; const usage = isRecord(message.usage) ? message.usage : undefined; if (usage && typeof usage.output === "number") tokens += usage.output; } }
27
+ if (message?.role === "toolResult" && message.isError !== true && typeof message.toolName === "string" && isTerminatingToolName(message.toolName) && isRecord(message.details)) { let details; try { details = validateAcceptedDetails(message.toolName, message.details); } catch (error) { // Contract: README.md#Doctor — non-accepted terminating receipts are expected-negative evidence and are skipped; all other validation failures propagate with their cause.
27
28
  if (error instanceof AcceptedDetailsContractError) continue; throw error; } const facts = acceptedFacts(message.toolName, details); if (facts.commit && facts.commit !== observedCommit) { commits.push({ source: id, commit: facts.commit }); observedCommit = facts.commit; } /* #836 B10.3: do not wipe earlier statuses or invent a code status sentence */ if (facts.status !== undefined) { statuses.push({ source: id, status: facts.status }); } }
28
29
  }
29
30
  const acceptedAt = accepted && timestamp(accepted); const final = acceptedAt ? accepted! : rows.at(-1); const endedAt = final && timestamp(final); const wall = startedAt && endedAt ? Date.parse(endedAt) - Date.parse(startedAt) : undefined;
@@ -5,6 +5,8 @@ import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPU
5
5
 
6
6
  import { sitianReport } from "./sitian-facade.ts";
7
7
 
8
+ import { errorText } from "./unknown-value.ts";
9
+
8
10
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
9
11
  export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/<n>/runs directory", type: "string" as const } } as const;
10
12
  export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase> };
@@ -19,14 +21,14 @@ function appendCandidate(ctx: HostContext, data: unknown): void {
19
21
  source: "doctor-role",
20
22
  });
21
23
  } catch (error) {
22
- throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
24
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${errorText(error)}`, { cause: error });
23
25
  }
24
26
  const append = ctx.sessionManager.appendCustomEntry;
25
27
  if (typeof append === "function") {
26
28
  try {
27
29
  append.call(ctx.sessionManager, DOCTOR_CANDIDATE_ENTRY_TYPE, data);
28
30
  } catch (error) {
29
- throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
31
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${errorText(error)}`, { cause: error });
30
32
  }
31
33
  }
32
34
  }
@@ -9,6 +9,8 @@ import { resolve } from "node:path";
9
9
 
10
10
  import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
11
11
 
12
+ import { isRecord } from "./unknown-value.ts";
13
+
12
14
  export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR" as const;
13
15
  export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate" as const;
14
16
 
@@ -77,10 +79,6 @@ export function resolveAuditDossier(
77
79
  return { status: "ok", runDirectory };
78
80
  }
79
81
 
80
- function isRecord(value: unknown): value is Record<string, unknown> {
81
- return typeof value === "object" && value !== null && !Array.isArray(value);
82
- }
83
-
84
82
  type AuditSubjectContext = { sessionManager: { getEntries?(): Iterable<unknown> } };
85
83
 
86
84
  /**
@@ -22,8 +22,6 @@ import {
22
22
  } from "./engine-detour-usage.ts";
23
23
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
24
24
 
25
-
26
-
27
25
  // #836 r16 class 3: argv required/minItems/element-minLength stay — execute()
28
26
  // must obtain the first item as the executable and spawn it (below; #82-98).
29
27
  // Root additionalProperties:false is deleted — execute() reads only `argv`.
@@ -27,6 +27,8 @@ import {
27
27
  type RecordPointer,
28
28
  } from "./sitian-facade.ts";
29
29
 
30
+ import { isRecord } from "./unknown-value.ts";
31
+
30
32
  /** Sitian event kind for one detour-tool call (volume under session/). */
31
33
  export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call" as const;
32
34
 
@@ -155,10 +157,6 @@ export function reportEngineDetourCall(
155
157
  };
156
158
  }
157
159
 
158
- function isRecord(value: unknown): value is Record<string, unknown> {
159
- return typeof value === "object" && value !== null && !Array.isArray(value);
160
- }
161
-
162
160
  function callFactFromSitianPayload(
163
161
  payload: unknown,
164
162
  pointer: RecordPointer,
@@ -11,6 +11,8 @@ import type {
11
11
  } from "./host-contracts.ts";
12
12
  import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
13
13
 
14
+ import { errorText } from "./unknown-value.ts";
15
+
14
16
  export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
15
17
 
16
18
  export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
@@ -103,7 +105,7 @@ export function withExternalHostCleanupFailure(
103
105
  cleanupError: unknown,
104
106
  name: string,
105
107
  ): RoleTurnResult {
106
- const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
108
+ const message = errorText(cleanupError);
107
109
  if (outcome.knownFailure === undefined) {
108
110
  return externalHostFailure("session", name, "dispose-failed", { cleanupError: message }, message);
109
111
  }
@@ -41,6 +41,7 @@ import {
41
41
  } from "./ticket-trajectory.ts";
42
42
  import type { BoardSnapshot, SnapshotTicket, TicketIssueState } from "./ticket-snapshot.ts";
43
43
 
44
+ import { isRecord, isMissingPathError } from "./unknown-value.ts";
44
45
 
45
46
  /** Unaccepted latest-run mtime bands (page-visible thresholds). */
46
47
  export const UNACCEPTED_FLYING_MS = 2 * 60 * 1000;
@@ -308,14 +309,6 @@ function currentStateLabel(state: TicketCurrentState): string {
308
309
  }
309
310
  }
310
311
 
311
- function isRecord(value: unknown): value is Record<string, unknown> {
312
- return typeof value === "object" && value !== null && !Array.isArray(value);
313
- }
314
-
315
- function isMissingPathError(error: unknown): boolean {
316
- return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
317
- }
318
-
319
312
  function escapeHtml(text: string): string {
320
313
  return text
321
314
  .replaceAll("&", "&amp;")
@@ -14,6 +14,7 @@ import {
14
14
  import type { PublicSummonResult } from "./public-role-summons.ts";
15
15
  import type { TerminalResult } from "./public-cli/terminal.ts";
16
16
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
17
+ import { isRecord } from "./unknown-value.ts";
17
18
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
18
19
  export const NOTARY_OUTPUT_TOOL = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
19
20
 
@@ -174,20 +175,11 @@ function failureReason(error: unknown): string {
174
175
  return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
175
176
  }
176
177
 
177
- function isRecord(value: unknown): value is Record<string, unknown> {
178
- return typeof value === "object" && value !== null && !Array.isArray(value);
179
- }
180
-
181
178
  /** Original decision bytes — no sentinel replacement (#836). */
182
179
  function retainedReceipt(decision: unknown): unknown {
183
180
  return decision;
184
181
  }
185
182
 
186
- function readRecord(value: unknown): Record<string, unknown> | undefined {
187
- if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
188
- return value as Record<string, unknown>;
189
- }
190
-
191
183
  /**
192
184
  * Read only the conclusion field for queueing (#753).
193
185
  * converged | continue | escalate → queue signal + raw receipt.
@@ -206,7 +198,7 @@ function projectOfficerDecision(
206
198
  fallbackStatus?: string,
207
199
  ): GatekeeperResult {
208
200
  const receipt = retainedReceipt(decision);
209
- const record = readRecord(decision);
201
+ const record = isRecord(decision) ? decision : undefined;
210
202
  const status =
211
203
  (record !== undefined && typeof record.status === "string" ? record.status : undefined)
212
204
  ?? fallbackStatus;
@@ -4,6 +4,8 @@
4
4
  * No bounce / verdict channel (言不为狱). 原卷保真 (ADR 0055).
5
5
  */
6
6
 
7
+ import { isRecord } from "./unknown-value.ts";
8
+
7
9
  export const GLEANER_LEFT_OUTPUT_TOOL_NAME = "ak_gleaner_left_output";
8
10
 
9
11
  /** Internal transport: comparison-base revision for the unanchored merge-candidate diff. */
@@ -26,7 +28,7 @@ export type GleanerLeftOutput = {
26
28
  };
27
29
 
28
30
  export function validateRecordedGleanerLeftOutput(value: unknown): GleanerLeftOutput {
29
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
31
+ if (!isRecord(value)) {
30
32
  throw new Error("Gleaner-left output has no execution discriminator");
31
33
  }
32
34
  let status: unknown;
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import type { HostIdentityDescription } from "../host-descriptions.ts";
8
8
 
9
+ import { isRecord } from "../unknown-value.ts";
10
+
9
11
  /** Claude Code print-mode (#645). */
10
12
  export type ClaudePrintHostDescription = HostIdentityDescription & Readonly<{
11
13
  protocol: "claude-print";
@@ -167,7 +169,7 @@ export function closeJsonSchemaForCodex(
167
169
  schema: Readonly<Record<string, unknown>>,
168
170
  ): Record<string, unknown> {
169
171
  const closed = closeSchemaNode(schema) as Record<string, unknown>;
170
- const existingDefs = isPlainObject(closed.$defs)
172
+ const existingDefs = isRecord(closed.$defs)
171
173
  ? (closed.$defs as Record<string, unknown>)
172
174
  : {};
173
175
  return {
@@ -179,14 +181,9 @@ export function closeJsonSchemaForCodex(
179
181
  };
180
182
  }
181
183
 
182
- /** Shared plain-object guard for headless host schema/event reduction. */
183
- export function isPlainObject(value: unknown): value is Record<string, unknown> {
184
- return typeof value === "object" && value !== null && !Array.isArray(value);
185
- }
186
-
187
184
  /** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
188
185
  function isPureNullTypeSchema(value: unknown): boolean {
189
- if (!isPlainObject(value)) return false;
186
+ if (!isRecord(value)) return false;
190
187
  if (value.type === "null") return true;
191
188
  if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
192
189
  return true;
@@ -199,7 +196,7 @@ function isPureNullTypeSchema(value: unknown): boolean {
199
196
  * Required edges keep the non-null type(s); optional edges re-add null once.
200
197
  */
201
198
  function stripNullFromLeafType(leaf: unknown): unknown {
202
- if (!isPlainObject(leaf) || !Array.isArray(leaf.type)) return leaf;
199
+ if (!isRecord(leaf) || !Array.isArray(leaf.type)) return leaf;
203
200
  const nonNull = leaf.type.filter((t) => t !== "null");
204
201
  if (nonNull.length === leaf.type.length) return leaf;
205
202
  if (nonNull.length === 0) return { ...leaf, type: "null" };
@@ -213,7 +210,7 @@ function stripNullFromLeafType(leaf: unknown): unknown {
213
210
  * lack `type`; strict structured output rejects those intermediate nodes.
214
211
  */
215
212
  function flattenUnionLeaves(schema: unknown): unknown[] {
216
- if (!isPlainObject(schema)) return [schema];
213
+ if (!isRecord(schema)) return [schema];
217
214
  if (Array.isArray(schema.anyOf)) {
218
215
  return schema.anyOf.flatMap(flattenUnionLeaves);
219
216
  }
@@ -233,7 +230,7 @@ function nonNullLeaves(schema: unknown): unknown[] {
233
230
  */
234
231
  function closePropertySchema(schema: unknown, optional: boolean): unknown {
235
232
  const description =
236
- isPlainObject(schema) && typeof schema.description === "string"
233
+ isRecord(schema) && typeof schema.description === "string"
237
234
  ? { description: schema.description }
238
235
  : {};
239
236
  const leaves = nonNullLeaves(schema).map(closeSchemaNode);
@@ -242,10 +239,10 @@ function closePropertySchema(schema: unknown, optional: boolean): unknown {
242
239
  const closed = leaves.length === 1 ? leaves[0] : { anyOf: leaves };
243
240
  // Codex strict does not allow sibling keys on $ref; put property guidance
244
241
  // on an enclosing schema instead of losing it or weakening the free-JSON ref.
245
- if (isPlainObject(closed) && typeof closed.$ref === "string") {
242
+ if (isRecord(closed) && typeof closed.$ref === "string") {
246
243
  return { anyOf: [closed], ...description };
247
244
  }
248
- return isPlainObject(closed) ? { ...closed, ...description } : closed;
245
+ return isRecord(closed) ? { ...closed, ...description } : closed;
249
246
  }
250
247
  return { anyOf: [...leaves, { type: "null" }], ...description };
251
248
  }
@@ -258,7 +255,7 @@ function closePropertySchema(schema: unknown, optional: boolean): unknown {
258
255
  function ensureTypedLeaf(schema: Record<string, unknown>): Record<string, unknown> {
259
256
  if (schema.type !== undefined) return schema;
260
257
  if (typeof schema.$ref === "string") return schema;
261
- if (isPlainObject(schema.properties) || schema.additionalProperties !== undefined) {
258
+ if (isRecord(schema.properties) || schema.additionalProperties !== undefined) {
262
259
  return { ...schema, type: "object" };
263
260
  }
264
261
  if (schema.items !== undefined) {
@@ -287,7 +284,7 @@ function originalRequiredNames(node: Record<string, unknown>): ReadonlySet<strin
287
284
  }
288
285
 
289
286
  function closeSchemaNode(node: unknown): unknown {
290
- if (!isPlainObject(node)) return node;
287
+ if (!isRecord(node)) return node;
291
288
 
292
289
  // Already a ref (free-JSON leaf or pre-existing) — do not retype.
293
290
  if (typeof node.$ref === "string") return node;
@@ -305,13 +302,13 @@ function closeSchemaNode(node: unknown): unknown {
305
302
  if (node.items !== undefined) {
306
303
  out.items = closeSchemaNode(node.items);
307
304
  }
308
- if (isPlainObject(node.$defs)) {
305
+ if (isRecord(node.$defs)) {
309
306
  out.$defs = Object.fromEntries(
310
307
  Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]),
311
308
  );
312
309
  }
313
310
 
314
- const hasProperties = isPlainObject(node.properties);
311
+ const hasProperties = isRecord(node.properties);
315
312
  const isObjectType =
316
313
  node.type === "object"
317
314
  || (Array.isArray(node.type) && node.type.includes("object"))
@@ -351,10 +348,10 @@ function closeSchemaNode(node: unknown): unknown {
351
348
  function stringEnvironment(value: unknown): Record<string, string> | undefined {
352
349
  const entries = Array.isArray(value)
353
350
  ? value.flatMap((item) => {
354
- if (!isPlainObject(item) || typeof item.name !== "string" || typeof item.value !== "string") return [];
351
+ if (!isRecord(item) || typeof item.name !== "string" || typeof item.value !== "string") return [];
355
352
  return [[item.name, item.value] as const];
356
353
  })
357
- : isPlainObject(value)
354
+ : isRecord(value)
358
355
  ? Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string")
359
356
  : [];
360
357
  return entries.length === 0 ? undefined : Object.fromEntries(entries);
@@ -36,11 +36,12 @@ import {
36
36
  headlessTurnArgs,
37
37
  isClaudePrintDescription,
38
38
  isCodexExecDescription,
39
- isPlainObject,
40
39
  type HeadlessHostDescription,
41
40
  } from "./description.ts";
42
41
  import { NAVIGATOR_OUTPUT_TOOL_NAME } from "../package-contracts/navigator-output.ts";
43
42
 
43
+ import { errorText, isRecord } from "../unknown-value.ts";
44
+
44
45
  export type HeadlessRoleTurnHostConfig = Readonly<{
45
46
  description: HeadlessHostDescription;
46
47
  sessionIdentity: SessionIdentityAuthority;
@@ -68,7 +69,7 @@ export type HeadlessCliResult = Readonly<{
68
69
  * envelope without stream-json `type`). Intermediate stream-json events are not.
69
70
  */
70
71
  function isHeadlessResultCandidate(value: unknown): value is HeadlessCliResult {
71
- if (!isPlainObject(value)) return false;
72
+ if (!isRecord(value)) return false;
72
73
  const record = value as HeadlessCliResult & { type?: unknown };
73
74
  return record.type === undefined
74
75
  || record.type === "result"
@@ -131,11 +132,11 @@ function createCodexExecTurnObserver(): {
131
132
 
132
133
  return {
133
134
  observe(value) {
134
- if (!isPlainObject(value)) return;
135
+ if (!isRecord(value)) return;
135
136
  const type = typeof value.type === "string" ? value.type : undefined;
136
137
  if (type === "thread.started" && typeof value.thread_id === "string" && value.thread_id !== "") {
137
138
  threadId = value.thread_id;
138
- } else if (type === "item.completed" && isPlainObject(value.item)) {
139
+ } else if (type === "item.completed" && isRecord(value.item)) {
139
140
  if (value.item.type === "agent_message" && typeof value.item.text === "string") {
140
141
  finalMessage = value.item.text;
141
142
  }
@@ -161,7 +162,7 @@ function createCodexExecTurnObserver(): {
161
162
 
162
163
  function formatCodexFailurePayload(payload: unknown): string {
163
164
  if (typeof payload === "string" && payload.trim() !== "") return payload;
164
- if (isPlainObject(payload)) {
165
+ if (isRecord(payload)) {
165
166
  if (typeof payload.message === "string" && payload.message.trim() !== "") return payload.message;
166
167
  try {
167
168
  return JSON.stringify(payload);
@@ -198,7 +199,7 @@ function resolveGitCommonDir(cwd: string): string | undefined {
198
199
  encoding: "utf8",
199
200
  });
200
201
  } catch (error) {
201
- const message = error instanceof Error ? error.message : String(error);
202
+ const message = errorText(error);
202
203
  throw new Error(`git rev-parse --git-common-dir failed: ${message}`);
203
204
  }
204
205
  if (result.error !== undefined) {
@@ -487,7 +488,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
487
488
  ...(gitCommonDir === undefined ? {} : { writableRoots: [gitCommonDir] }),
488
489
  });
489
490
  } catch (error) {
490
- const message = error instanceof Error ? error.message : String(error);
491
+ const message = errorText(error);
491
492
  return {
492
493
  status: "terminal",
493
494
  result: failure("session", "HeadlessArgvFailure", "argv-failed", { diagnostic: message }, message),
@@ -559,7 +560,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
559
560
  } catch (error) {
560
561
  if (childExited) exitedSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
561
562
  if (isHostAbortedError(error)) throw error;
562
- const message = error instanceof Error ? error.message : String(error);
563
+ const message = errorText(error);
563
564
  const observedHostFailure = codexObserver?.result().failureDiagnostic;
564
565
  if (observedHostFailure !== undefined) {
565
566
  return {
@@ -604,7 +605,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
604
605
  } catch (error) {
605
606
  // The host failure remains primary; retain persistence failure as
606
607
  // secondary typed evidence instead of replacing the terminal.
607
- sessionBindingDiagnostic = error instanceof Error ? error.message : String(error);
608
+ sessionBindingDiagnostic = errorText(error);
608
609
  }
609
610
  }
610
611
 
@@ -26,6 +26,8 @@ import type { RoleTurnContinuation } from "./host-contracts.ts";
26
26
  import { sitianReport } from "./sitian-facade.ts";
27
27
  import type { SitianRecordInput } from "./sitian-contracts.ts";
28
28
 
29
+ import { errorText } from "./unknown-value.ts";
30
+
29
31
  /** Volume category under `<run>/session/<kind>/records.jsonl`. */
30
32
  export const HOST_SESSION_RECORD_KIND = "host-session" as const;
31
33
 
@@ -269,7 +271,7 @@ export function copyAndRecordHostDossier(options: {
269
271
  payload: {
270
272
  type: "native-session-warning",
271
273
  sessionId: options.sessionId,
272
- error: error instanceof Error ? error.message : String(error),
274
+ error: errorText(error),
273
275
  },
274
276
  });
275
277
  return;
@@ -334,7 +336,7 @@ export function copyAndRecordHostDossier(options: {
334
336
  landingPath,
335
337
  ordinal,
336
338
  sessionId: options.sessionId,
337
- error: lastError instanceof Error ? lastError.message : String(lastError),
339
+ error: errorText(lastError),
338
340
  },
339
341
  });
340
342
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
8
+ import { isRecord } from "./unknown-value.ts";
8
9
 
9
10
  export const INSPECTOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
10
11
  export const INSPECTOR_SOURCE_RUN_FLAG = {
@@ -21,7 +22,7 @@ export type InspectorOutput =
21
22
  | { readonly status: "escalate"; readonly reason?: unknown; readonly findings?: unknown };
22
23
 
23
24
  export function validateRecordedInspectorOutput(value: unknown): InspectorOutput {
24
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
25
+ if (!isRecord(value)) {
25
26
  throw new Error("Inspector output has no execution discriminator");
26
27
  }
27
28
  let status: unknown;
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { readFile } from "node:fs/promises";
6
6
 
7
+ import { isRecord } from "./unknown-value.ts";
8
+
7
9
  export type LedgerSessionRow = Record<string, unknown>;
8
10
 
9
11
  /**
@@ -32,10 +34,6 @@ export class LedgerSessionJsonlError extends Error {
32
34
  }
33
35
  }
34
36
 
35
- function isRecord(value: unknown): value is Record<string, unknown> {
36
- return typeof value === "object" && value !== null && !Array.isArray(value);
37
- }
38
-
39
37
  /**
40
38
  * One physical JSONL line as the single kernel saw it.
41
39
  * `row` present = syntactically complete session object at that 1-based line.
@@ -339,4 +337,3 @@ export function intervalRowsAroundAnchor(
339
337
  return { rows: rows.slice(start, end), closed: end < rows.length };
340
338
  }
341
339
 
342
-