@akagilnc/pi-workflow-roles 0.1.4980 → 0.1.4989

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 (65) hide show
  1. package/dist/acp-host/production-host.js +438 -456
  2. package/dist/analyst-gate-cycles-read.js +16 -11
  3. package/dist/analyst-metric-families/gate-cycles.js +2 -2
  4. package/dist/audit-escalation.js +4 -4
  5. package/dist/compliance-transport.js +2 -2
  6. package/dist/countersign-contracts.js +6 -5
  7. package/dist/doctor-evidence.js +2 -1
  8. package/dist/gatekeeper-role.js +20 -41
  9. package/dist/headless-host/production-host.js +438 -456
  10. package/dist/inspector-contracts.js +3 -2
  11. package/dist/migrate-book-topology.js +59 -54
  12. package/dist/navigator-invocation-identity.js +6 -4
  13. package/dist/notary-contracts.js +3 -19
  14. package/dist/package-contracts/auditor-output.js +4 -19
  15. package/dist/package-contracts/judge-output.js +6 -5
  16. package/dist/package-contracts/terminating-tools.js +9 -3
  17. package/dist/packaged-role-registry.js +10 -12
  18. package/dist/public-cli/countersign-run.js +1 -1
  19. package/dist/public-cli/main.js +90 -86
  20. package/dist/public-cli/option-definitions.js +2 -2
  21. package/dist/public-cli/settlement.js +11 -11
  22. package/dist/review-submission.js +20 -0
  23. package/dist/submission-errors.js +2 -2
  24. package/dist/submission-ledger.js +14 -2
  25. package/package.json +1 -1
  26. package/souls/doctor-auditor.md +1 -1
  27. package/souls/fixer-auditor.md +4 -4
  28. package/souls/gate-output-guide.md +2 -2
  29. package/souls/inspector.md +1 -1
  30. package/souls/judge-auditor.md +2 -2
  31. package/souls/notary.md +1 -1
  32. package/souls/quality-law.md +1 -1
  33. package/src/analyst-gate-cycles-read.ts +17 -9
  34. package/src/analyst-metric-families/gate-cycles.ts +2 -2
  35. package/src/audit-escalation.ts +8 -8
  36. package/src/auditor-role.ts +1 -1
  37. package/src/compliance-transport.ts +7 -7
  38. package/src/countersign-contracts.ts +10 -8
  39. package/src/countersign-role.ts +6 -35
  40. package/src/doctor-evidence.ts +2 -1
  41. package/src/doctor-role.ts +1 -2
  42. package/src/gatekeeper-role.ts +30 -55
  43. package/src/host-contracts.ts +4 -3
  44. package/src/inspector-contracts.ts +7 -6
  45. package/src/inspector-role.ts +3 -20
  46. package/src/judge-role.ts +29 -49
  47. package/src/navigator-invocation-identity.ts +16 -4
  48. package/src/notary-contracts.ts +8 -26
  49. package/src/notary-role.ts +1 -1
  50. package/src/package-contracts/auditor-output.ts +6 -27
  51. package/src/package-contracts/judge-output.ts +10 -8
  52. package/src/package-contracts/terminating-tools.ts +9 -3
  53. package/src/packaged-role-registry.ts +12 -13
  54. package/src/pi/adapter.ts +6 -5
  55. package/src/public-cli/countersign-run.ts +1 -1
  56. package/src/public-cli/option-definitions.ts +2 -2
  57. package/src/public-cli/settlement.ts +13 -14
  58. package/src/review-submission.ts +29 -0
  59. package/src/role-envelope.ts +17 -9
  60. package/src/role-runtime.ts +32 -15
  61. package/src/submission-errors.ts +6 -6
  62. package/src/{gatekeeper-pass-envelope.ts → submission-gate.ts} +29 -16
  63. package/src/submission-ledger.ts +13 -3
  64. package/src/ticket-trajectory.ts +9 -4
  65. package/src/worker-role.ts +16 -2
@@ -0,0 +1,29 @@
1
+ /** One shared terminating receipt contract for review officers (#1028). */
2
+ import { Type } from "typebox";
3
+
4
+ import { openToolObject } from "./open-tool-schema.ts";
5
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
6
+
7
+ export const REVIEW_SUBMISSION_OUTPUT_TOOL_NAME = "ak_submission_output" as const;
8
+
9
+ /** Open receipt: the queue reads only status; all other submitted fields ride unchanged. */
10
+ export const reviewSubmissionSchema = withTerminatingOutputDeclarations(
11
+ openToolObject(
12
+ Type.Object({
13
+ status: Type.Optional(Type.Unknown({
14
+ description: "converged | continue | escalate — 非三态时请重读后重交,勿改标。",
15
+ })),
16
+ findings: Type.Optional(Type.Unknown({ description: "审核发现,原样留存" })),
17
+ reason: Type.Optional(Type.Unknown({ description: "上呈理由" })),
18
+ violations: Type.Optional(Type.Unknown({ description: "违规条目" })),
19
+ conflicts: Type.Optional(Type.Unknown({ description: "待决冲突" })),
20
+ fix: Type.Optional(Type.Unknown({ description: "continue 时的补救说明;可写 summary 摘要,其余内容原样留存" })),
21
+ classes: Type.Optional(Type.Unknown({ description: "已裁决 finding 类;可说明 name、owner、boundary、disposition,原样留存" })),
22
+ note: Type.Optional(Type.Unknown({ description: "裁决附注" })),
23
+ evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
24
+ decisionGate: Type.Optional(Type.Unknown({ description: "需人权威处置的问题与选项;可说明 question、options,原样留存" })),
25
+ }),
26
+ ),
27
+ );
28
+
29
+ export type ReviewSubmission = { readonly status?: unknown; readonly [key: string]: unknown };
@@ -11,8 +11,8 @@ import {
11
11
  } from "./engine-detour.ts";
12
12
  import {
13
13
  createDefaultGateOfficerSummon,
14
- requireGatekeeperPass,
15
- } from "./gatekeeper-pass-envelope.ts";
14
+ requireSubmissionGate,
15
+ } from "./submission-gate.ts";
16
16
  import type {
17
17
  HostContext,
18
18
  HostEventRegistration,
@@ -233,12 +233,12 @@ export async function prepareRoleEnvelope(options: {
233
233
  // seat remains reachable through MCP.
234
234
  setActiveTools(names) { preferredTools = [...names]; },
235
235
  getActiveTools() { return [...preferredTools]; },
236
- async requireGatekeeperPass(gateOptions) {
236
+ async requireSubmissionGate(gateOptions) {
237
237
  const packageRoot =
238
238
  typeof options.dependencies.packageRoot === "string"
239
239
  ? options.dependencies.packageRoot
240
240
  : undefined;
241
- return requireGatekeeperPass({
241
+ return requireSubmissionGate({
242
242
  context: gateOptions.context,
243
243
  subject: gateOptions.subject,
244
244
  ...(gateOptions.signal === undefined ? {} : { signal: gateOptions.signal }),
@@ -301,7 +301,7 @@ export async function prepareRoleEnvelope(options: {
301
301
  if (record.kind === "role_infrastructure_failure") return;
302
302
  const code = typeof record.code === "string" && record.code.length > 0
303
303
  ? record.code
304
- : record.status === "bounce" || record.status === "escalate" || record.status === "no_receipt"
304
+ : record.status === "continue" || record.status === "escalate" || record.status === "no_receipt"
305
305
  ? record.status
306
306
  : undefined;
307
307
  if (code === undefined) return;
@@ -314,11 +314,12 @@ export async function prepareRoleEnvelope(options: {
314
314
  };
315
315
  }
316
316
  function textDiagnostic(content: ContentPart[]): string | undefined {
317
- // Keep concatenated text bytes intact for resume relay (#813 online P2):
318
- // only emptiness uses the trimmed form; do not alter Markdown/spacing.
317
+ // Keep each text item's bytes intact and its boundary readable on the
318
+ // string-only host continuation channel.
319
319
  const text = content
320
- .map((part) => (part.type === "text" ? part.text : ""))
321
- .join("");
320
+ .filter((part) => part.type === "text")
321
+ .map((part) => part.text)
322
+ .join("\n\n");
322
323
  return text.trim().length > 0 ? text : undefined;
323
324
  }
324
325
  /** Arm closeRound + abort path with the durable infrastructure failure for this round. */
@@ -483,6 +484,13 @@ export async function prepareRoleEnvelope(options: {
483
484
  details: result.details,
484
485
  isError: false,
485
486
  });
487
+ if (result.terminate === false) {
488
+ rejection = {
489
+ code: "continue",
490
+ toolCallIds: [toolCallId],
491
+ message: textDiagnostic(projected.content) ?? "continue",
492
+ };
493
+ }
486
494
  // Candidate only: seal waits for closeRound after the host round boundary.
487
495
  return { content: projected.content, isError: projected.isError };
488
496
  } catch (error) {
@@ -119,7 +119,7 @@ import {
119
119
  type ReviewerActivation,
120
120
  type ReviewerAdmittedInputs,
121
121
  } from "./reviewer-role.ts";
122
- import type { GatekeeperNonPassResult } from "./gatekeeper-role.ts";
122
+ import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
123
123
 
124
124
  /**
125
125
  * Private transport flag names/definitions for Reviewer admitted inputs.
@@ -256,7 +256,7 @@ import {
256
256
 
257
257
  /** One envelope map for Gatekeeper non-pass and other correct submission rejects (#525). */
258
258
  type SubmissionNonPassResult =
259
- | GatekeeperNonPassResult
259
+ | SubmissionGateNonPassResult
260
260
  | CoderSkillExpansionEvidenceMissingResult;
261
261
  import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.ts";
262
262
  import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.ts";
@@ -319,7 +319,7 @@ export {
319
319
  runGatekeeper,
320
320
  gateOfficerForSubject,
321
321
  };
322
- export type { GatekeeperResult, GatekeeperSubject, GatekeeperNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts";
322
+ export type { GatekeeperResult, GatekeeperSubject, SubmissionGateNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts";
323
323
  import { ParentQueueReaskError } from "./submission-errors.ts";
324
324
 
325
325
  export {
@@ -490,7 +490,7 @@ export type RoleRuntimeDependencies = {
490
490
  /**
491
491
  * Composition-root adapter table for nested gate officer summons (#969).
492
492
  * Production leaves unset (default pi + packaged externals). Tests inject
493
- * faux nested hosts so prepareRoleEnvelope.requireGatekeeperPass is bitten
493
+ * faux nested hosts so prepareRoleEnvelope.requireSubmissionGate is bitten
494
494
  * without reimplementing the envelope summon closure.
495
495
  */
496
496
  hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[];
@@ -667,6 +667,14 @@ function createFiledOfficerRuntime(
667
667
  if (isAuditEscalationProjection(projected)) {
668
668
  return { content: [], details: projected, terminate: true as const };
669
669
  }
670
+ if (projected !== null && typeof projected === "object" && "status" in projected
671
+ && projected.status === "continue" && "receipt" in projected) {
672
+ return {
673
+ content: [{ type: "text" as const, text: readableGateItem(projected.receipt) }],
674
+ details: projected,
675
+ terminate: false,
676
+ };
677
+ }
670
678
  const pass = spec.relayGatePass === true && projected !== null && typeof projected === "object" && "officer" in projected && "receipt" in projected
671
679
  ? projected
672
680
  : undefined;
@@ -937,15 +945,15 @@ export function createDiaristRoleRuntime(
937
945
  );
938
946
  }
939
947
 
940
- /** Countersign status words the queue reads (#753). */
948
+ /** Shared review-submission status words the queue reads (#1028). */
941
949
  const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
942
950
 
943
951
  /**
944
- * Plain-language re-ask when countersignStatus is not a known queue word.
952
+ * Plain-language re-ask when status is not a known queue word.
945
953
  * Back to countersign itself — notary is not summoned (#753).
946
954
  */
947
955
  const COUNTERSIGN_STATUS_REASK =
948
- "countersignStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
956
+ "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
949
957
 
950
958
  /** Secretariat status words the submission gate reads. */
951
959
  const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]);
@@ -969,7 +977,7 @@ export function createSecretariatRoleRuntime(
969
977
  const beforeAccept: FiledOfficerBeforeAccept | undefined =
970
978
  hostActions !== undefined
971
979
  ? async ({ toolCallId, parameters, signal, ctx }) => {
972
- if (roleHost.requireGatekeeperPass === undefined) {
980
+ if (roleHost.requireSubmissionGate === undefined) {
973
981
  const error = new Error("host cannot mount the secretariat submission gate");
974
982
  error.name = "InfrastructureFailure";
975
983
  return hostActions.failInfrastructure(error, ctx, toolCallId);
@@ -990,7 +998,7 @@ export function createSecretariatRoleRuntime(
990
998
  return undefined;
991
999
  }
992
1000
  try {
993
- const pass = await roleHost.requireGatekeeperPass!({
1001
+ const pass = await roleHost.requireSubmissionGate!({
994
1002
  context: ctx,
995
1003
  subject: { kind: "secretariat_verdict" },
996
1004
  ...(signal === undefined ? {} : { signal }),
@@ -1005,6 +1013,7 @@ export function createSecretariatRoleRuntime(
1005
1013
  pass !== undefined
1006
1014
  && pass !== null
1007
1015
  && typeof pass === "object"
1016
+ && pass.status === "converged"
1008
1017
  && "receipt" in pass
1009
1018
  ) {
1010
1019
  const {
@@ -1112,7 +1121,7 @@ export function createCountersignRoleRuntime(
1112
1121
  ) {
1113
1122
  // Notary inner gate difference only — lifecycle stays on the shared envelope.
1114
1123
  // Pointer-only summons: officer self-fetches from run dossier (#632 / ADR 0079).
1115
- // #753 queue: read countersignStatus only — escalate skips gate (thrown to caller);
1124
+ // #1028 queue: read shared status — escalate skips gate (thrown to caller);
1116
1125
  // unreadable status returns to countersign; else notary inner gate.
1117
1126
  const beforeAccept: FiledOfficerBeforeAccept | undefined =
1118
1127
  hostActions !== undefined
@@ -1122,8 +1131,8 @@ export function createCountersignRoleRuntime(
1122
1131
  ? (parameters as Record<string, unknown>)
1123
1132
  : undefined;
1124
1133
  const status =
1125
- record !== undefined && typeof record.countersignStatus === "string"
1126
- ? record.countersignStatus
1134
+ record !== undefined && typeof record.status === "string"
1135
+ ? record.status
1127
1136
  : undefined;
1128
1137
  if (status === undefined || !COUNTERSIGN_QUEUE_STATUSES.has(status)) {
1129
1138
  // Parent status unreadable → back to countersign itself; do not summon notary,
@@ -1134,13 +1143,13 @@ export function createCountersignRoleRuntime(
1134
1143
  // Parent escalate → throw to caller as-is; notary does not attend (#753).
1135
1144
  return undefined;
1136
1145
  }
1137
- if (roleHost.requireGatekeeperPass === undefined) {
1146
+ if (roleHost.requireSubmissionGate === undefined) {
1138
1147
  const error = new Error("host cannot mount the countersign gate");
1139
1148
  error.name = "InfrastructureFailure";
1140
1149
  return hostActions.failInfrastructure(error, ctx, toolCallId);
1141
1150
  }
1142
1151
  try {
1143
- return await roleHost.requireGatekeeperPass({
1152
+ return await roleHost.requireSubmissionGate({
1144
1153
  context: ctx,
1145
1154
  subject: { kind: "countersign_verdict" },
1146
1155
  ...(signal === undefined ? {} : { signal }),
@@ -1202,7 +1211,15 @@ export function createRoleRuntimeExtension(
1202
1211
  };
1203
1212
  const roleHost = createSubmissionLedgerHost(
1204
1213
  envelopeHost.host,
1205
- new Map(PACKAGED_ROLE_REGISTRY.map(({ role, outputTool }) => [outputTool, role])),
1214
+ new Map(PACKAGED_ROLE_REGISTRY.reduce<(readonly [string, import("./public-cli/terminal.ts").TerminalRoleName | readonly import("./public-cli/terminal.ts").TerminalRoleName[]])[]>((entries, { role, outputTool }) => {
1215
+ const existingIndex = entries.findIndex(([name]) => name === outputTool);
1216
+ if (existingIndex < 0) entries.push([outputTool, role]);
1217
+ else {
1218
+ const previous = entries[existingIndex]![1];
1219
+ entries[existingIndex] = [outputTool, [...(Array.isArray(previous) ? previous : [previous]), role]];
1220
+ }
1221
+ return entries;
1222
+ }, [])),
1206
1223
  failInfrastructure,
1207
1224
  async (closed, context) => projectClosedSubmission(closed, context),
1208
1225
  );
@@ -1,9 +1,9 @@
1
- import type { GatekeeperNonPassResult } from "./gatekeeper-role.ts";
1
+ import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
2
2
  import { readableGateItem } from "./readable-gate-item.ts";
3
3
 
4
4
  /**
5
5
  * Tool-result text the parent model sees (#753 / #750 evidence).
6
- * bounce | escalate → officer receipt verbatim (JSON when structured).
6
+ * continue | escalate → officer receipt verbatim (JSON when structured).
7
7
  * no_receipt → honest lifecycle fact. No findings rewrite, no「(无 findings)」.
8
8
  * #775: structured field content via readableGateItem (DRY with other gate seams).
9
9
  */
@@ -11,8 +11,8 @@ function serializeReceipt(receipt: unknown): string {
11
11
  return readableGateItem(receipt);
12
12
  }
13
13
 
14
- function gatekeeperNonPassMessage(result: GatekeeperNonPassResult): string {
15
- if (result.status === "bounce" || result.status === "escalate") {
14
+ function gatekeeperNonPassMessage(result: SubmissionGateNonPassResult): string {
15
+ if (result.status === "continue" || result.status === "escalate") {
16
16
  return serializeReceipt(result.receipt);
17
17
  }
18
18
  if (result.status === "transport_failure") {
@@ -26,8 +26,8 @@ function gatekeeperNonPassMessage(result: GatekeeperNonPassResult): string {
26
26
 
27
27
  /** Structured non-pass; `.result` is session-projected via tool_result, message feeds the model. */
28
28
  export class GatekeeperDecisionError extends Error {
29
- readonly result: GatekeeperNonPassResult;
30
- constructor(result: GatekeeperNonPassResult, message?: string) {
29
+ readonly result: SubmissionGateNonPassResult;
30
+ constructor(result: SubmissionGateNonPassResult, message?: string) {
31
31
  super(message ?? gatekeeperNonPassMessage(result));
32
32
  this.name = "GatekeeperDecisionError";
33
33
  this.result = result;
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Shared submit-path envelope for 门下省 gates (ADR 0018 / #675 / #753).
2
+ * Shared submissionGate path for review officers (ADR 0018 / #675 / #753).
3
3
  * Owns officer-pointer book + host abort/non-pass faces + review queue loop.
4
4
  * Role modules only project via projectGatekeeperRun / runGatekeeper — no book, no catch.
5
5
  *
6
6
  * Queue guarantee only (#753 / #756 / #750):
7
7
  * parent submit → summon officer → read conclusion field
8
- * pass → accept end
9
- * bounce → raw officer receipt back to parent; escalate → audit_escalation terminal,
10
- * without binding a correctable parent submission
11
- * not three-state → resume officer with plain-language re-ask (no round cap)
8
+ * converged → accept end
9
+ * continue → raw officer receipt as a nonterminal result; parent may resubmit
10
+ * escalate → audit_escalation terminal, without binding a correctable parent submission
11
+ * unrecognized status → resume officer with plain-language re-ask (no round cap)
12
12
  * transport / no_receipt → present honestly
13
13
  * Four pairs: countersign↔notary, judge↔auditor, worker↔inspector, secretariat↔countersign (#969).
14
14
  * Code does not judge content, map next-step for parent, or label unreadable/unusable.
@@ -20,7 +20,7 @@ import {
20
20
  GatekeeperDecisionError,
21
21
  officerConclusionReask,
22
22
  projectGatekeeperRun,
23
- type GatekeeperPassHostActions,
23
+ type SubmissionGateHostActions,
24
24
  type GatekeeperResult,
25
25
  type GatekeeperSubject,
26
26
  type GateOfficer,
@@ -71,8 +71,8 @@ function bookDirectOfficerPointer(
71
71
  summoned: PublicSummonResult,
72
72
  ): void {
73
73
  if (
74
- result.status !== "pass"
75
- && result.status !== "bounce"
74
+ result.status !== "converged"
75
+ && result.status !== "continue"
76
76
  && result.status !== "escalate"
77
77
  && result.status !== "needs_reask"
78
78
  && result.status !== "transport_failure"
@@ -101,7 +101,8 @@ function bookDirectOfficerPointer(
101
101
  * Carries officer receipt + nested runId so seat settlement can project the
102
102
  * public terminal without a second authority.
103
103
  */
104
- export type GatekeeperPassOutcome = {
104
+ export type SubmissionGateOutcome = {
105
+ readonly status: "converged" | "continue";
105
106
  readonly officer: GateOfficer;
106
107
  readonly receipt: unknown;
107
108
  readonly runId?: string;
@@ -110,13 +111,13 @@ export type GatekeeperPassOutcome = {
110
111
  /**
111
112
  * Shared envelope: project gate, book officer pointer, map onto host actions.
112
113
  * Review loop has no round cap (#753 no-round-cap).
113
- * On pass, returns the officer snapshot (receipt + nested runId) for seat projection.
114
+ * On converged, returns the officer snapshot (receipt + nested runId) for seat projection.
114
115
  */
115
- export async function requireGatekeeperPass(options: {
116
+ export async function requireSubmissionGate(options: {
116
117
  readonly context: ExtensionContext | HostContext;
117
118
  readonly subject: GatekeeperSubject;
118
119
  readonly signal?: AbortSignal;
119
- readonly hostActions: GatekeeperPassHostActions;
120
+ readonly hostActions: SubmissionGateHostActions;
120
121
  readonly toolCallId: string;
121
122
  /**
122
123
  * In-flight parent typed payload for this gate turn (#879). Relayed verbatim
@@ -125,9 +126,9 @@ export async function requireGatekeeperPass(options: {
125
126
  readonly submission?: unknown;
126
127
  /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
127
128
  readonly summonOfficer?: GateOfficerSummon;
128
- }): Promise<GatekeeperPassOutcome | void> {
129
+ }): Promise<SubmissionGateOutcome | void> {
129
130
  let reask: string | undefined;
130
- // No round cap — end only on pass, bounce/escalate, or real failure.
131
+ // No round cap — end only on converged, continue/escalate, or real failure.
131
132
  const summonOfficer =
132
133
  options.summonOfficer ??
133
134
  createDefaultGateOfficerSummon({
@@ -156,8 +157,19 @@ export async function requireGatekeeperPass(options: {
156
157
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
157
158
  }
158
159
  }
159
- if (gatekeeper.status === "pass") {
160
+ if (gatekeeper.status === "converged") {
160
161
  return {
162
+ status: "converged",
163
+ officer: projected.officer,
164
+ receipt: gatekeeper.receipt,
165
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
166
+ ? { runId: gatekeeper.runId }
167
+ : {}),
168
+ };
169
+ }
170
+ if (gatekeeper.status === "continue") {
171
+ return {
172
+ status: "continue",
161
173
  officer: projected.officer,
162
174
  receipt: gatekeeper.receipt,
163
175
  ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
@@ -181,7 +193,8 @@ export async function requireGatekeeperPass(options: {
181
193
  if (gatekeeper.status === "escalate") {
182
194
  throw new GatekeeperDecisionError(gatekeeper);
183
195
  }
184
- // bounce | no_receipt: parent stands and may resubmit. Not a run abort.
196
+ // no_receipt: keep the lifecycle failure channel; continue is an ordinary
197
+ // nonterminal tool result above, not an exception or correctable rejection.
185
198
  options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
186
199
  throw new GatekeeperDecisionError(gatekeeper);
187
200
  }
@@ -593,7 +593,7 @@ export async function sealAcceptedSubmission(options: {
593
593
  */
594
594
  export function createSubmissionLedgerHost(
595
595
  host: RoleHost,
596
- outputTools: ReadonlyMap<string, TerminalRoleName>,
596
+ outputTools: ReadonlyMap<string, TerminalRoleName | readonly TerminalRoleName[]>,
597
597
  failInfrastructure: (error: unknown, context: HostContext) => never = (error) => { throw error; },
598
598
  projectClosure: (closed: ClosedSubmission, context: HostContext) => void | Promise<void> = () => undefined,
599
599
  options?: { home?: string },
@@ -646,7 +646,16 @@ export function createSubmissionLedgerHost(
646
646
  return {
647
647
  ...host,
648
648
  registerTool(tool) {
649
- const role = outputTools.get(tool.name);
649
+ const registeredRole = outputTools.get(tool.name);
650
+ let role: TerminalRoleName | undefined;
651
+ if (typeof registeredRole === "string") {
652
+ role = registeredRole;
653
+ } else if (registeredRole !== undefined) {
654
+ const activeRole = host.getFlag("ak-role");
655
+ role = typeof activeRole === "string" && registeredRole.includes(activeRole as TerminalRoleName)
656
+ ? activeRole as TerminalRoleName
657
+ : undefined;
658
+ }
650
659
  if (role === undefined) return host.registerTool(tool);
651
660
  host.registerTool({
652
661
  ...tool,
@@ -735,7 +744,8 @@ export function createSubmissionLedgerHost(
735
744
  await projectClosure(closed, context);
736
745
  return result;
737
746
  }
738
- // Terminating-tool wrap records every call; terminate flag does not withhold params.
747
+ // A continuing gate leaves the candidate recorded but has not accepted it.
748
+ if (result.terminate === false) return result;
739
749
  const closed: ClosedSubmission = {
740
750
  role,
741
751
  kind: "accepted",
@@ -44,7 +44,7 @@ import {
44
44
  validateAcceptedDetails,
45
45
  type TerminatingToolName,
46
46
  } from "./package-contracts/terminating-tools.ts";
47
- import { PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
47
+ import { LEGACY_REVIEW_OUTPUT_ROLES, PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
48
48
 
49
49
 
50
50
  /** Declared refresh bound for the same viewing surface (seconds). */
@@ -140,9 +140,11 @@ async function realpathOrLexicalIfMissing(path: string): Promise<string> {
140
140
  }
141
141
  }
142
142
 
143
- const TOOL_TO_ROLE: ReadonlyMap<string, string> = new Map(
144
- PACKAGED_ROLE_REGISTRY.map((entry) => [entry.outputTool, entry.role]),
145
- );
143
+ const TOOL_TO_ROLE: ReadonlyMap<string, string> = new Map([
144
+ ...PACKAGED_ROLE_REGISTRY.filter((entry) => entry.outputTool !== "ak_submission_output")
145
+ .map((entry) => [entry.outputTool, entry.role] as const),
146
+ ...LEGACY_REVIEW_OUTPUT_ROLES,
147
+ ]);
146
148
 
147
149
  const NAME_PREFIX_ROLES: readonly string[] = PACKAGED_ROLE_REGISTRY.map((entry) => entry.role);
148
150
 
@@ -324,6 +326,9 @@ function resolveStation(input: {
324
326
  invocationRole?: string;
325
327
  runId: string;
326
328
  }): { station: string; stationSource: StationSource } {
329
+ if (input.invocationRole && input.toolNames.includes("ak_submission_output")) {
330
+ return { station: input.invocationRole, stationSource: "invocation" };
331
+ }
327
332
  for (const toolName of input.toolNames) {
328
333
  const role = roleFromToolName(toolName);
329
334
  if (role) return { station: role, stationSource: "tool" };
@@ -287,7 +287,7 @@ export function createFixerRoleRuntime(
287
287
  let pass;
288
288
  try {
289
289
  pass = WORKER_DONE_STATUSES.has(workerStatusOf(output))
290
- ? await pi.requireGatekeeperPass!({
290
+ ? await pi.requireSubmissionGate!({
291
291
  context: ctx,
292
292
  subject: { kind: "worker_completion" },
293
293
  ...(_signal === undefined ? {} : { signal: _signal }),
@@ -303,6 +303,13 @@ export function createFixerRoleRuntime(
303
303
  }
304
304
  throw error;
305
305
  }
306
+ if (pass?.status === "continue") {
307
+ return {
308
+ content: [{ type: "text" as const, text: readableGateItem(pass.receipt) }],
309
+ details: output,
310
+ terminate: false,
311
+ };
312
+ }
306
313
  const acceptedDetails = output;
307
314
  return {
308
315
  content: pass === undefined ? [] : [{ type: "text" as const, text: readableGateItem(pass.receipt) }],
@@ -435,7 +442,7 @@ export function createCoderRoleRuntime(
435
442
  let pass;
436
443
  try {
437
444
  pass = WORKER_DONE_STATUSES.has(workerStatusOf(output))
438
- ? await pi.requireGatekeeperPass!({
445
+ ? await pi.requireSubmissionGate!({
439
446
  context: ctx,
440
447
  subject: { kind: "worker_completion" },
441
448
  ...(_signal === undefined ? {} : { signal: _signal }),
@@ -451,6 +458,13 @@ export function createCoderRoleRuntime(
451
458
  }
452
459
  throw error;
453
460
  }
461
+ if (pass?.status === "continue") {
462
+ return {
463
+ content: [{ type: "text" as const, text: readableGateItem(pass.receipt) }],
464
+ details: output,
465
+ terminate: false,
466
+ };
467
+ }
454
468
  const acceptedDetails = output;
455
469
  return {
456
470
  content: pass === undefined ? [] : [{ type: "text" as const, text: readableGateItem(pass.receipt) }],