@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2535

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.
@@ -93,6 +93,7 @@ import {
93
93
  } from "../navigator-invocation-identity.ts";
94
94
  import type { NavigatorPhase } from "../navigator-attendance.ts";
95
95
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, parseNoReceiptLifecycleFacts, type NoReceiptLifecycleFacts } from "../receipt-delivery-policy.ts";
96
+ import { INSPECTOR_OUTPUT_TOOL_NAME } from "../inspector-contracts.ts";
96
97
  import {
97
98
  ensureRunArtifactsDir,
98
99
  type AdmittedCoderInvocation,
@@ -100,6 +101,7 @@ import {
100
101
  type AdmittedDoctorInvocation,
101
102
  type AdmittedFixerInvocation,
102
103
  type AdmittedJudgeInvocation,
104
+ type AdmittedInspectorInvocation,
103
105
  type AdmittedMergerInvocation,
104
106
  type AdmittedNotaryInvocation,
105
107
  type AdmittedReviewerInvocation,
@@ -3057,6 +3059,83 @@ export async function trySettleDoctorTerminalResult(
3057
3059
  return settleLawfulDoctorTerminalResult(admitted);
3058
3060
  }
3059
3061
 
3062
+ export function extractInspectorRoleOutcome(entries: readonly SessionEntry[]): TerminalResult["roleOutcome"] | undefined {
3063
+ if (!isReceiptSettlementBindingClear(entries)) return undefined;
3064
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3065
+ const message = entries[index]?.message;
3066
+ if (message?.role !== "toolResult" || message.toolName !== INSPECTOR_OUTPUT_TOOL_NAME) continue;
3067
+ if (!isAcceptedPackagedRoleTerminalResult(message)) continue;
3068
+ const details = message.details;
3069
+ if (typeof details !== "object" || details === null) continue;
3070
+ const status = Reflect.get(details, "status");
3071
+ if (status !== "pass" && status !== "bounce") continue;
3072
+ const findings = Reflect.get(details, "findings");
3073
+ return { kind: "accepted", role: "inspector", status, decisiveFacts: { findings } };
3074
+ }
3075
+ return undefined;
3076
+ }
3077
+
3078
+ type UnusableOutputReceipt = Readonly<{
3079
+ candidate: unknown;
3080
+ diagnostic: string;
3081
+ }>;
3082
+
3083
+ function findUnusableOutputReceipt(
3084
+ entries: readonly SessionEntry[],
3085
+ toolName: string,
3086
+ isUsable: (candidate: unknown) => boolean,
3087
+ diagnostic: string,
3088
+ ): UnusableOutputReceipt | undefined {
3089
+ let acceptedNonUsable: unknown | undefined;
3090
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3091
+ const message = entries[index]?.message;
3092
+ if (message?.role !== "toolResult") continue;
3093
+ const residual = boundErroredToolCandidate(entries, index, message, toolName);
3094
+ if (residual !== undefined) {
3095
+ return { candidate: residual.candidate, diagnostic: residual.diagnostic };
3096
+ }
3097
+ if (
3098
+ acceptedNonUsable === undefined &&
3099
+ message.toolName === toolName &&
3100
+ isAcceptedPackagedRoleTerminalResult(message) &&
3101
+ !isUsable(message.details)
3102
+ ) {
3103
+ acceptedNonUsable = message.details;
3104
+ }
3105
+ }
3106
+ return acceptedNonUsable === undefined
3107
+ ? undefined
3108
+ : { candidate: acceptedNonUsable, diagnostic };
3109
+ }
3110
+
3111
+ export async function trySettleInspectorTerminalResult(
3112
+ admitted: AdmittedInspectorInvocation,
3113
+ ): Promise<TerminalResult | undefined> {
3114
+ const entries = await readLawfulSettlementEntries(admitted);
3115
+ if (entries === undefined) return undefined;
3116
+ const outcome = extractInspectorRoleOutcome(entries);
3117
+ if (outcome !== undefined) {
3118
+ return withOptionalGateProjection({
3119
+ roleOutcome: outcome,
3120
+ navigator: extractNavigatorFact(entries), artifacts: [], runId: admitted.runId,
3121
+ }, admitted.sessionDirectory);
3122
+ }
3123
+ const unusable = findUnusableOutputReceipt(
3124
+ entries,
3125
+ INSPECTOR_OUTPUT_TOOL_NAME,
3126
+ (candidate) => isRecord(candidate) &&
3127
+ (candidate.status === "pass" || candidate.status === "bounce"),
3128
+ "给事中回执无显式 pass/bounce",
3129
+ );
3130
+ return unusable === undefined
3131
+ ? undefined
3132
+ : settleFailureTerminalResult(admitted, {
3133
+ cause: "output",
3134
+ diagnostic: unusable.diagnostic,
3135
+ details: { candidate: unusable.candidate, acceptedReceipt: false },
3136
+ });
3137
+ }
3138
+
3060
3139
  /** Lawful Notary accepted outcome (pass/bounce). */
3061
3140
  export type LawfulNotaryRoleOutcome = {
3062
3141
  kind: "accepted";
@@ -3099,46 +3178,26 @@ async function settleLawfulNotaryTerminalResult(
3099
3178
  if (entries === undefined) return undefined;
3100
3179
  const extracted = extractNotaryRoleOutcome(entries);
3101
3180
  if (extracted === undefined) {
3102
- // No usable Notary release → existing non-zero failure channel with candidate (#475 / ADR 0055).
3103
- // One reverse pass: prefer errored residual; else latest accepted-once non-usable details.
3104
- let acceptedNonUsable: unknown | undefined;
3105
- for (let index = entries.length - 1; index >= 0; index -= 1) {
3106
- const message = entries[index]?.message;
3107
- if (message?.role !== "toolResult") continue;
3108
- const residual = boundErroredToolCandidate(
3109
- entries,
3110
- index,
3111
- message,
3112
- NOTARY_OUTPUT_TOOL_NAME,
3113
- );
3114
- if (residual !== undefined) {
3115
- return settleFailureTerminalResult(admitted, {
3116
- cause: "output",
3117
- diagnostic: residual.diagnostic,
3118
- details: { candidate: residual.candidate, acceptedReceipt: false },
3119
- });
3120
- }
3121
- if (
3122
- acceptedNonUsable === undefined &&
3123
- message.toolName === NOTARY_OUTPUT_TOOL_NAME &&
3124
- isAcceptedPackagedRoleTerminalResult(message)
3125
- ) {
3126
- // Accepted once but not a lawful pass/bounce — hold as fallback.
3181
+ const unusable = findUnusableOutputReceipt(
3182
+ entries,
3183
+ NOTARY_OUTPUT_TOOL_NAME,
3184
+ (candidate) => {
3127
3185
  try {
3128
- validateRecordedNotaryOutput(message.details);
3186
+ validateRecordedNotaryOutput(candidate);
3187
+ return true;
3129
3188
  } catch {
3130
- acceptedNonUsable = message.details;
3189
+ return false;
3131
3190
  }
3132
- }
3133
- }
3134
- if (acceptedNonUsable !== undefined) {
3135
- return settleFailureTerminalResult(admitted, {
3191
+ },
3192
+ "符宝郎回执无显式 pass/bounce",
3193
+ );
3194
+ return unusable === undefined
3195
+ ? undefined
3196
+ : settleFailureTerminalResult(admitted, {
3136
3197
  cause: "output",
3137
- diagnostic: "符宝郎回执无显式 pass/bounce",
3138
- details: { candidate: acceptedNonUsable, acceptedReceipt: false },
3198
+ diagnostic: unusable.diagnostic,
3199
+ details: { candidate: unusable.candidate, acceptedReceipt: false },
3139
3200
  });
3140
- }
3141
- return undefined;
3142
3201
  }
3143
3202
  const navigator = extractNavigatorFact(entries);
3144
3203
  return withOptionalGateProjection(
@@ -38,7 +38,8 @@ export type TerminalRoleName =
38
38
  | "doctor"
39
39
  | "reviewer"
40
40
  | "merger"
41
- | "notary";
41
+ | "notary"
42
+ | "inspector";
42
43
 
43
44
  /** Merger/Collector residual only — Notary/audit residual abolished (#475). */
44
45
  export type ResidualIncompleteTerminalOutcome = {
@@ -1,5 +1,5 @@
1
1
  import { writeSync } from "node:fs";
2
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Value } from "typebox/value";
4
4
 
5
5
  import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts";
@@ -39,6 +39,9 @@ import {
39
39
  import type { ComplianceDecision } from "./compliance-transport.ts";
40
40
  import { createDoctorRoleRuntime } from "./doctor-role.ts";
41
41
  import { createNotaryRoleRuntime } from "./notary-role.ts";
42
+ import { inspectorOutputSchema, projectInspectorReceipt } from "./inspector-role.ts";
43
+ import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
44
+ import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
42
45
  import { decorateSettlementWithNavigation, formatNavigatorReport, NAVIGATOR_EVENT_TYPE, navigatorSubjectKey, navigatorUnavailableError, subjectPath, type NavigatorAttendance, type NavigatorEvent, type NavigatorPhase, type NavigatorReport, type NavigatorSettlement, type NavigatorSubjectProvenance, type NavigatorWorkContext } from "./navigator-attendance.ts";
43
46
  import {
44
47
  buildNavigatorInfrastructureFailureFact,
@@ -367,6 +370,7 @@ type ActivationRuntime = {
367
370
  collector: { activate(context: ExtensionContext, event: { reason: string }): Promise<void> };
368
371
  doctor: { activate(): Promise<void> };
369
372
  notary: { activate(): Promise<void> };
373
+ inspector: { activate(): Promise<void> };
370
374
  merger(): Promise<void>;
371
375
  };
372
376
 
@@ -404,6 +408,7 @@ function activationStage(role: PackagedRole, runtime: ActivationRuntime): { id:
404
408
  case "collector": return { id: "load-and-install", run: async () => runtime.collector.activate(runtime.context, runtime.event) };
405
409
  case "doctor": return { id: "load-and-install", run: async () => runtime.doctor.activate() };
406
410
  case "notary": return { id: "load-and-install", run: async () => runtime.notary.activate() };
411
+ case "inspector": return { id: "load-and-install", run: async () => runtime.inspector.activate() };
407
412
  case "merger": return { id: "prepare-git-and-install", run: async () => runtime.merger() };
408
413
  }
409
414
  }
@@ -486,6 +491,7 @@ export type RoleRuntimeDependencies = {
486
491
  createCollectorTransport?(): CollectorGitHubTransport;
487
492
  loadDoctorSoul?(): Promise<string>;
488
493
  loadNotarySoul?(): Promise<string>;
494
+ loadInspectorSoul?(): Promise<string>;
489
495
  loadNotarySourceRun?(path: string): Promise<import("./notary-contracts.ts").NotarySourceRunLocator>;
490
496
  loadDoctorCase?(path: string): Promise<import("./doctor-contracts.ts").DoctorCase>;
491
497
  loadMergerSoul?(): Promise<string>;
@@ -989,6 +995,42 @@ export function createRoleRuntimeExtension(
989
995
  async loadCase(path) { if (!dependencies.loadDoctorCase) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorCase(path); },
990
996
  async auditCompliance(options) { if (!dependencies.auditDoctorCompliance) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.auditDoctorCompliance(options); },
991
997
  }, hostActions);
998
+ let inspectorSoul: string | undefined;
999
+ let inspectorRegistered = false;
1000
+ const inspector = {
1001
+ async activate(): Promise<void> {
1002
+ if (!dependencies.loadInspectorSoul) throw new Error("Inspector runtime dependencies are not configured");
1003
+ inspectorSoul = (await dependencies.loadInspectorSoul()).trim();
1004
+ if (inspectorSoul.length === 0) throw new Error("Inspector soul is empty");
1005
+ if (!inspectorRegistered) {
1006
+ inspectorRegistered = true;
1007
+ pi.registerTool({
1008
+ name: INSPECTOR_OUTPUT_TOOL_NAME,
1009
+ label: "给事中输出",
1010
+ description: "给事中终局回执,状态为 pass 或 bounce。",
1011
+ promptSnippet: "给事中终局回执",
1012
+ parameters: inspectorOutputSchema,
1013
+ async execute(toolCallId, parameters, _signal, _onUpdate, ctx): Promise<AgentToolResult<unknown>> {
1014
+ failOnInfrastructureFailureDeclaration(parameters, hostActions, ctx, toolCallId);
1015
+ const leaf = ctx.sessionManager.getLeafEntry();
1016
+ const calls = leaf?.type === "message" && leaf.message.role === "assistant"
1017
+ ? leaf.message.content.filter((part) => part.type === "toolCall")
1018
+ : [];
1019
+ if (calls.length !== 1 || calls[0]?.id !== toolCallId || calls[0]?.name !== INSPECTOR_OUTPUT_TOOL_NAME) {
1020
+ throw new Error("给事中回执非唯一终局工具调用");
1021
+ }
1022
+ return projectInspectorReceipt(parameters);
1023
+ },
1024
+ });
1025
+ pi.on("before_agent_start", (event) => {
1026
+ if (inspectorSoul === undefined) throw new Error("给事中未激活");
1027
+ return { systemPrompt: `${event.systemPrompt}\n\n<inspector_soul>\n${inspectorSoul}\n</inspector_soul>` };
1028
+ });
1029
+ }
1030
+ const tools = pi.getAllTools().filter((tool) => tool.name === INSPECTOR_OUTPUT_TOOL_NAME);
1031
+ if (tools.length !== 1) throw new Error(`Inspector required tool collision or missing: ${INSPECTOR_OUTPUT_TOOL_NAME}`);
1032
+ },
1033
+ };
992
1034
  const notary = createNotaryRoleRuntime(pi, {
993
1035
  async loadSoul() {
994
1036
  if (!dependencies.loadNotarySoul) throw new Error("Notary runtime dependencies are not configured");
@@ -1138,6 +1180,7 @@ export function createRoleRuntimeExtension(
1138
1180
  collector,
1139
1181
  doctor,
1140
1182
  notary,
1183
+ inspector,
1141
1184
  merger: async () => {
1142
1185
  if (dependencies.mergerGitState === undefined) {
1143
1186
  sessionMergerGitState = dependencies.createMergerGitState?.(ctx.cwd);