@akagilnc/pi-workflow-roles 0.1.5375 → 0.1.5406

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 (79) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +6381 -5684
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/collector-tool-schemas.js +18 -0
  6. package/dist/compliance-transport.js +5 -0
  7. package/dist/diarist-contracts.js +26 -0
  8. package/dist/doctor-auditor.js +1 -0
  9. package/dist/engine-detour-usage.js +6 -6
  10. package/dist/filed-submission.js +28 -0
  11. package/dist/gatekeeper-role.js +6 -10
  12. package/dist/gleaner-left-contracts.js +16 -0
  13. package/dist/headless-host/production-host.js +1644 -947
  14. package/dist/judge-role.js +21 -16
  15. package/dist/migrate-book-topology.js +452 -81
  16. package/dist/navigator-attendance.js +10 -9
  17. package/dist/navigator-invocation-identity.js +18 -0
  18. package/dist/package-contracts/reviewer-output.js +16 -0
  19. package/dist/package-contracts/worker-output.js +18 -2
  20. package/dist/pi/role-turn-host.js +9 -0
  21. package/dist/public-cli/cli.js +1 -0
  22. package/dist/public-cli/instruction-seat-run.js +258 -55
  23. package/dist/public-cli/main.js +1209 -346
  24. package/dist/public-cli/post-admission.js +427 -244
  25. package/dist/public-cli/settlement.js +111 -48
  26. package/dist/public-cli/terminal.js +12 -1
  27. package/dist/public-cli/turn-request.js +3 -0
  28. package/dist/receipt-delivery-policy.js +152 -17
  29. package/dist/role-submission-declarations.js +131 -0
  30. package/dist/secretariat-contracts.js +23 -1
  31. package/dist/submission-errors.js +11 -1
  32. package/dist/submission-gate.js +32 -12
  33. package/package.json +1 -1
  34. package/src/archivist-record-pointer.ts +48 -0
  35. package/src/auditor-role.ts +2 -8
  36. package/src/collector-role.ts +5 -28
  37. package/src/compliance-transport.ts +10 -2
  38. package/src/countersign-role.ts +2 -7
  39. package/src/diarist-contracts.ts +37 -0
  40. package/src/diarist-role.ts +4 -44
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/doctor-role.ts +16 -11
  43. package/src/engine-detour-usage.ts +7 -6
  44. package/src/external-host-turn-loop.ts +24 -12
  45. package/src/filed-submission.ts +44 -0
  46. package/src/gatekeeper-role.ts +8 -16
  47. package/src/gleaner-left-contracts.ts +26 -1
  48. package/src/gleaner-left-role.ts +4 -32
  49. package/src/host-contracts.ts +7 -0
  50. package/src/inspector-role.ts +2 -7
  51. package/src/judge-role.ts +31 -24
  52. package/src/merger-role.ts +5 -8
  53. package/src/navigator-attendance.ts +17 -12
  54. package/src/navigator-invocation-identity.ts +31 -0
  55. package/src/navigator-role.ts +2 -8
  56. package/src/notary-role.ts +5 -20
  57. package/src/package-contracts/reviewer-output.ts +25 -0
  58. package/src/package-contracts/worker-output.ts +43 -5
  59. package/src/pi/adapter.ts +33 -8
  60. package/src/pi/role-turn-host.ts +7 -0
  61. package/src/public-cli/cli.ts +1 -0
  62. package/src/public-cli/instruction-seat-run.ts +292 -61
  63. package/src/public-cli/post-admission.ts +556 -324
  64. package/src/public-cli/settlement.ts +112 -50
  65. package/src/public-cli/terminal.ts +16 -4
  66. package/src/public-cli/turn-request.ts +10 -1
  67. package/src/receipt-delivery-policy.ts +209 -17
  68. package/src/reviewer-role.ts +7 -35
  69. package/src/role-envelope.ts +5 -1
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +72 -56
  72. package/src/role-submission-declarations.ts +143 -0
  73. package/src/secretariat-contracts.ts +35 -0
  74. package/src/secretariat-role.ts +4 -40
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +41 -12
  78. package/src/worker-role.ts +81 -86
  79. package/src/worker-submission-gates.ts +32 -7
@@ -8,7 +8,8 @@
8
8
  * converged → continue remaining review and settle
9
9
  * continue → raw officer receipt; caller resumes the submitted seat for revision
10
10
  * escalate → return the officer run so the caller can resume that officer directly
11
- * unrecognized status → resume officer with plain-language re-ask (no round cap)
11
+ * unrecognized status → resume officer with plain-language re-ask, counted
12
+ * against the configured ceiling; exhaustion keeps that receipt
12
13
  * transport / no_receipt → present honestly
13
14
  * Four pairs: countersign↔notary, judge↔auditor, worker↔inspector, secretariat↔countersign (#969).
14
15
  * Code does not judge content, map next-step for parent, or label unreadable/unusable.
@@ -28,21 +29,21 @@ import {
28
29
  } from "./gatekeeper-role.ts";
29
30
  import type { PublicSummonResult } from "./public-role-summons.ts";
30
31
  import type { TerminalResult } from "./public-cli/terminal.ts";
32
+ import { receivedDiscriminator } from "./submission-errors.ts";
33
+ import type { ReviewQueueWord } from "./review-submission.ts";
34
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
31
35
 
32
- /** Queue word of the latest this-terminal payload. History does not outrank it. */
33
- export function latestQueueStatus(terminal: TerminalResult | undefined): string | undefined {
36
+ /** Received discriminator of the latest payload, retained verbatim for re-ask. */
37
+ export function latestQueueStatus(terminal: TerminalResult | undefined): unknown {
34
38
  const outcome = terminal?.roleOutcome;
35
39
  if (outcome === undefined) return undefined;
36
40
  if (outcome.kind === "audit_escalation") return "escalate";
37
41
  if (outcome.kind !== "accepted") return undefined;
38
42
  const latest = outcome.payloads?.[outcome.payloads.length - 1];
39
- if (isRecord(latest)) {
40
- const status = (latest as { status?: unknown }).status;
41
- if (typeof status === "string" && status.trim() !== "") return status;
43
+ if (isRecord(latest) && Object.hasOwn(latest, "status")) {
44
+ return receivedDiscriminator(latest, "status");
42
45
  }
43
- return typeof outcome.status === "string" && outcome.status.trim() !== ""
44
- ? outcome.status
45
- : undefined;
46
+ return outcome.status;
46
47
  }
47
48
 
48
49
  export function latestQueuePayload(terminal: TerminalResult | undefined): unknown {
@@ -98,6 +99,7 @@ function bookDirectOfficerPointer(
98
99
  officer: GateOfficer,
99
100
  result: GatekeeperResult,
100
101
  summoned: PublicSummonResult,
102
+ toolCallId: string,
101
103
  ): void {
102
104
  if (
103
105
  result.status !== "converged"
@@ -122,6 +124,7 @@ function bookDirectOfficerPointer(
122
124
  ...(typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== ""
123
125
  ? { runDirectory: summoned.runDirectory }
124
126
  : {}),
127
+ ...(toolCallId.trim() === "" ? {} : { submissionToolCallId: toolCallId }),
125
128
  });
126
129
  }
127
130
 
@@ -131,16 +134,20 @@ function bookDirectOfficerPointer(
131
134
  * public terminal without a second authority.
132
135
  */
133
136
  export type SubmissionGateOutcome = {
134
- readonly status: "converged" | "continue" | "escalate";
137
+ readonly status: ReviewQueueWord | "needs_reask";
135
138
  readonly officer: GateOfficer;
136
139
  readonly receipt: unknown;
137
140
  readonly runId?: string;
138
141
  readonly runDirectory?: string;
142
+ /** Officer terminal already returned by the summon. Present when the reply was not a queue word. */
143
+ readonly terminal?: TerminalResult;
139
144
  };
140
145
 
141
146
  /**
142
147
  * Shared envelope: project gate, book officer pointer, map onto host actions.
143
- * Review loop has no round cap (#753 no-round-cap).
148
+ * A real converged / continue / escalate returns immediately. An unreadable
149
+ * conclusion reasks that officer against the configured ceiling; exhaustion
150
+ * returns the receipt actually received.
144
151
  * On converged, returns the officer snapshot (receipt + nested runId) for seat projection.
145
152
  */
146
153
  export async function requireSubmissionGate(options: {
@@ -156,9 +163,12 @@ export async function requireSubmissionGate(options: {
156
163
  readonly submission?: unknown;
157
164
  /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
158
165
  readonly summonOfficer?: GateOfficerSummon;
166
+ /** This loop's ceiling. Absent uses the package default. */
167
+ readonly autoResumeLimit?: number;
159
168
  }): Promise<SubmissionGateOutcome | void> {
160
169
  let reask: string | undefined;
161
- // No round cap — end only on converged, continue/escalate, or real failure.
170
+ let reasksSpent = 0;
171
+ const reaskLimit = deliveryLimitFromConfig(options.autoResumeLimit);
162
172
  const summonOfficer =
163
173
  options.summonOfficer ??
164
174
  createDefaultGateOfficerSummon({
@@ -182,6 +192,7 @@ export async function requireSubmissionGate(options: {
182
192
  projected.officer,
183
193
  gatekeeper,
184
194
  projected.summoned,
195
+ options.toolCallId,
185
196
  );
186
197
  } catch (error) {
187
198
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
@@ -208,6 +219,24 @@ export async function requireSubmissionGate(options: {
208
219
  };
209
220
  }
210
221
  if (gatekeeper.status === "needs_reask") {
222
+ if (reasksSpent >= reaskLimit) {
223
+ return {
224
+ status: "needs_reask",
225
+ officer: projected.officer,
226
+ receipt: gatekeeper.receipt,
227
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
228
+ ? { runId: gatekeeper.runId }
229
+ : {}),
230
+ ...(typeof projected.summoned?.runDirectory === "string"
231
+ && projected.summoned.runDirectory.trim() !== ""
232
+ ? { runDirectory: projected.summoned.runDirectory }
233
+ : {}),
234
+ ...(projected.summoned?.terminal === undefined
235
+ ? {}
236
+ : { terminal: projected.summoned.terminal }),
237
+ };
238
+ }
239
+ reasksSpent += 1;
211
240
  reask = officerConclusionReask(gatekeeper.receivedStatus);
212
241
  continue;
213
242
  }
@@ -1,18 +1,15 @@
1
- import type { RoleHost, HostContext, HostToolResult, HostGatekeeperActions } from "./host-contracts.ts";
2
- import { Type, type Static } from "typebox";
3
- import { openToolObject } from "./open-tool-schema.ts";
4
- import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
1
+ import type { RoleHost, HostContext, HostGatekeeperActions } from "./host-contracts.ts";
5
2
 
6
3
  import {
7
- CODER_OUTPUT_TOOL_NAME,
8
- FIXER_OUTPUT_TOOL_NAME,
9
4
  validateAcceptedWorkerDetails,
10
5
  type CoderOutput,
11
6
  type FixerOutput,
12
7
  type WorkerOutput,
13
8
  type WorkerRoleLabel,
14
9
  } from "./package-contracts/worker-output.ts";
15
- import { PARTIALLY_COMPLETED_DEFINITION, fixerOutputSchema, validateFixerOutput, type FixerPhase } from "./package-contracts/fixer-output.ts";
10
+ import { validateFixerOutput, type FixerPhase } from "./package-contracts/fixer-output.ts";
11
+ import { registerFiledSubmissionTool } from "./filed-submission.ts";
12
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
16
13
  import {
17
14
  FixerPacketValidationError,
18
15
  parseFixerPrerequisites,
@@ -23,7 +20,9 @@ import {
23
20
  WorkerCommitReminderError,
24
21
  WorkerPrefixReminderError,
25
22
  WorkerUnfinishedReasonReminderError,
23
+ type WorkerSubmissionGate,
26
24
  } from "./worker-submission-gates.ts";
25
+ import { deliveryLimitFromEnv } from "./receipt-delivery-policy.ts";
27
26
  import {
28
27
  fixerBashSeatbeltDenyReason,
29
28
  matchFixerBashForbiddenLiteral,
@@ -34,28 +33,10 @@ import { isRecord } from "./unknown-value.ts";
34
33
  export {
35
34
  CODER_OUTPUT_TOOL_NAME,
36
35
  FIXER_OUTPUT_TOOL_NAME,
36
+ coderOutputSchema,
37
37
  validateAcceptedWorkerDetails,
38
- };
39
- export type { WorkerOutput };
40
-
41
- // status 的合法词写在 description。WORKER_DONE_STATUSES 只让 completed /
42
- // partially_completed 进入提交闸。unfinished 且未见理由说明时,运行时同 run 催全
43
- // (ADR 0050);理由在不在不按 JSON 类型判,也不在派发前用长度拒收。
44
- // report / remainingScope / reason 的声明只留字段名和语义。
45
- const CODER_STATUS_DESCRIPTION =
46
- `planned | completed | refused | partially_completed | unfinished。unfinished:缺前置或违宪约束致本局未完成。${PARTIALLY_COMPLETED_DEFINITION}` as const;
47
- const coderOutputObject = Type.Object({
48
- status: Type.Unknown({ description: CODER_STATUS_DESCRIPTION }),
49
- report: Type.Unknown({ description: "如实结果报告" }),
50
- remainingScope: Type.Unknown({ description: "本局后剩余工作" }),
51
- reason: Type.Unknown({
52
- description: "阻断原因:缺前置或违宪约束。缺待决 owner 决定或答复属缺前置。",
53
- }),
54
- });
55
- export const coderOutputSchema = withTerminatingOutputDeclarations(
56
- openToolObject(coderOutputObject),
57
- );
58
- export type { FixerOutput, CoderOutput };
38
+ } from "./package-contracts/worker-output.ts";
39
+ export type { WorkerOutput, FixerOutput, CoderOutput };
59
40
  export const FIXER_FLAG_DEFINITIONS = {
60
41
  packet: {
61
42
  name: "ak-fix-packet",
@@ -103,7 +84,7 @@ export type CoderRoleDependencies = {
103
84
  export type WorkerRoleRuntime = {
104
85
  activate(ctx?: HostContext): Promise<void>;
105
86
  /** Arm gate ① baseline after envelope places the worktree (coder/fixer). Durable parent required (#857). */
106
- armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }): void;
87
+ armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }, invocationScopeId?: string): void;
107
88
  };
108
89
 
109
90
  /** Read only the field that selects the worker's next package-owned path. */
@@ -128,9 +109,48 @@ export function validateWorkerOutput(
128
109
  return validateAcceptedWorkerDetails(output, "Coder") as CoderOutput;
129
110
  }
130
111
 
112
+ /**
113
+ * Coder and fixer share one submission sequence: record via the shared tool,
114
+ * then read status and bounce through the same reminder gate.
115
+ * Seat differences stay in the ready check and the projection.
116
+ */
117
+ function registerWorkerSubmission(
118
+ pi: RoleHost,
119
+ role: "coder" | "fixer",
120
+ options: {
121
+ readonly notReady: string;
122
+ readonly ready: () => boolean;
123
+ readonly phase: () => WorkerPhase | undefined;
124
+ readonly project: (parameters: unknown, phase: WorkerPhase) => WorkerOutput;
125
+ readonly submissionGate: WorkerSubmissionGate;
126
+ readonly hostActions: WorkerRoleHostActions;
127
+ },
128
+ ): void {
129
+ registerFiledSubmissionTool(pi, roleSubmissionDeclaration(role), {
130
+ readyError: () => (options.ready() ? undefined : options.notReady),
131
+ beforeAccept: async ({ toolCallId, parameters, ctx }) => {
132
+ const phase = options.phase();
133
+ if (phase === undefined) throw new Error(options.notReady);
134
+ const output = options.project(parameters, phase);
135
+ const status = workerStatusOf(output);
136
+ if (status !== undefined) {
137
+ assertAcceptableThroughHost(
138
+ options.submissionGate,
139
+ status,
140
+ output,
141
+ options.hostActions,
142
+ ctx,
143
+ toolCallId,
144
+ );
145
+ }
146
+ return output;
147
+ },
148
+ });
149
+ }
150
+
131
151
  /** Reminder bounces stay typed rejects; IO/infrastructure keep identity via host failInfrastructure. */
132
152
  function assertAcceptableThroughHost(
133
- submissionGate: { assertAcceptable(status: string, details?: unknown): void },
153
+ submissionGate: WorkerSubmissionGate,
134
154
  status: string,
135
155
  details: unknown,
136
156
  hostActions: WorkerRoleHostActions,
@@ -155,6 +175,7 @@ export function createFixerRoleRuntime(
155
175
  pi: RoleHost,
156
176
  dependencies: FixerRoleDependencies,
157
177
  hostActions: WorkerRoleHostActions,
178
+ options?: { readonly unfinishedReasonBounceLimit?: number },
158
179
  ): WorkerRoleRuntime {
159
180
  let soul: string | undefined;
160
181
  let packet: FixerInvocationInput | undefined;
@@ -162,7 +183,12 @@ export function createFixerRoleRuntime(
162
183
  let prerequisitesPath: string | undefined;
163
184
  let phase: WorkerPhase | undefined;
164
185
  let lifecycleRegistered = false;
165
- const submissionGate = createWorkerSubmissionGate();
186
+ // #1132: ADR 0050 缺理由催全次数. The in-process seam passes the value it
187
+ // already resolved; the Pi child has only the env that seam projected.
188
+ const submissionGate = createWorkerSubmissionGate({
189
+ unfinishedReasonBounceLimit: options?.unfinishedReasonBounceLimit
190
+ ?? deliveryLimitFromEnv(process.env),
191
+ });
166
192
 
167
193
  pi.registerFlag(
168
194
  FIXER_FLAG_DEFINITIONS.packet.name,
@@ -212,30 +238,13 @@ export function createFixerRoleRuntime(
212
238
 
213
239
  if (!lifecycleRegistered) {
214
240
  lifecycleRegistered = true;
215
- pi.registerTool({
216
- name: FIXER_OUTPUT_TOOL_NAME,
217
- label: "修内司输出",
218
- description: "提交修内司终局回执。",
219
- promptSnippet: "提交修内司终局回执",
220
- parameters: fixerOutputSchema,
221
- async execute(toolCallId: string, parameters: unknown, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
222
- if (packet === undefined || phase === undefined) {
223
- throw new Error("修内司修理包与阶段未装载");
224
- }
225
- const output = deepFreeze(validateFixerOutput(parameters, phase));
226
- const status = workerStatusOf(output);
227
- if (status !== undefined) {
228
- assertAcceptableThroughHost(
229
- submissionGate,
230
- status,
231
- output,
232
- hostActions,
233
- ctx,
234
- toolCallId,
235
- );
236
- }
237
- return { content: [], details: output, terminate: true as const };
238
- },
241
+ registerWorkerSubmission(pi, "fixer", {
242
+ notReady: "修内司修理包与阶段未装载",
243
+ ready: () => packet !== undefined && phase !== undefined,
244
+ phase: () => phase,
245
+ project: (parameters, current) => deepFreeze(validateFixerOutput(parameters, current)),
246
+ submissionGate,
247
+ hostActions,
239
248
  });
240
249
  pi.on("tool_call", (event) => {
241
250
  if (event.toolName !== "bash") return;
@@ -263,8 +272,8 @@ export function createFixerRoleRuntime(
263
272
  });
264
273
  }
265
274
  },
266
- armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }) {
267
- submissionGate.arm(cwd, parent);
275
+ armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }, invocationScopeId?: string) {
276
+ submissionGate.arm(cwd, parent, invocationScopeId);
268
277
  },
269
278
  };
270
279
  }
@@ -273,12 +282,17 @@ export function createCoderRoleRuntime(
273
282
  pi: RoleHost,
274
283
  dependencies: CoderRoleDependencies,
275
284
  hostActions: WorkerRoleHostActions,
285
+ options?: { readonly unfinishedReasonBounceLimit?: number },
276
286
  ): WorkerRoleRuntime {
277
287
  let soul: string | undefined;
278
288
  let task: string | undefined;
279
289
  let phase: WorkerPhase | undefined;
280
290
  let lifecycleRegistered = false;
281
- const submissionGate = createWorkerSubmissionGate();
291
+ // #1132: same resolved ceiling as the fixer gate. Absent = Pi child env.
292
+ const submissionGate = createWorkerSubmissionGate({
293
+ unfinishedReasonBounceLimit: options?.unfinishedReasonBounceLimit
294
+ ?? deliveryLimitFromEnv(process.env),
295
+ });
282
296
 
283
297
  pi.registerFlag("ak-coder-task", {
284
298
  description: "Markdown task assigned to the coder role",
@@ -310,32 +324,13 @@ export function createCoderRoleRuntime(
310
324
 
311
325
  if (!lifecycleRegistered) {
312
326
  lifecycleRegistered = true;
313
- pi.registerTool({
314
- name: CODER_OUTPUT_TOOL_NAME,
315
- label: "将作监输出",
316
- description: "提交将作监终局回执。",
317
- promptSnippet: "提交将作监终局回执",
318
- parameters: coderOutputSchema,
319
- async execute(toolCallId: string, parameters: unknown, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
320
- if (task === undefined || phase === undefined) {
321
- throw new Error("将作监任务与阶段未装载");
322
- }
323
- const output = validateWorkerOutput(parameters, phase, "Coder");
324
- const status = workerStatusOf(output);
325
- // #836: skill-expansion evidence rejection deleted (陛下「2.4/5 删」).
326
- // Skill still ships with the package (ADR 0052); code no longer refuses on it.
327
- if (status !== undefined) {
328
- assertAcceptableThroughHost(
329
- submissionGate,
330
- status,
331
- output,
332
- hostActions,
333
- ctx,
334
- toolCallId,
335
- );
336
- }
337
- return { content: [], details: output, terminate: true as const };
338
- },
327
+ registerWorkerSubmission(pi, "coder", {
328
+ notReady: "将作监任务与阶段未装载",
329
+ ready: () => task !== undefined && phase !== undefined,
330
+ phase: () => phase,
331
+ project: (parameters, current) => validateWorkerOutput(parameters, current, "Coder"),
332
+ submissionGate,
333
+ hostActions,
339
334
  });
340
335
  pi.on("before_agent_start", (event, ctx) => {
341
336
  if (soul === undefined) throw new Error("将作监职分未装载");
@@ -346,8 +341,8 @@ export function createCoderRoleRuntime(
346
341
  });
347
342
  }
348
343
  },
349
- armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }) {
350
- submissionGate.arm(cwd, parent);
344
+ armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }, invocationScopeId?: string) {
345
+ submissionGate.arm(cwd, parent, invocationScopeId);
351
346
  },
352
347
  };
353
348
  }
@@ -16,6 +16,7 @@ import {
16
16
  WorkerUnfinishedReasonReminderError,
17
17
  } from "./submission-errors.ts";
18
18
  import { WORKER_DONE_STATUSES } from "./worker-submission-contracts.ts";
19
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
19
20
 
20
21
  import { isRecord } from "./unknown-value.ts";
21
22
 
@@ -26,6 +27,7 @@ export const WORKER_SUBMISSION_GATE_RECORD_KIND = WORKER_SUBMISSION_GATE_KIND;
26
27
  export const WORKER_COMMIT_BASELINE_ENTRY_TYPE = "commit-baseline";
27
28
  export const WORKER_COMMIT_REMINDER_BOUNCE_ENTRY_TYPE = "commit-reminder-bounce";
28
29
  export const WORKER_PREFIX_REMINDER_BOUNCE_ENTRY_TYPE = "prefix-reminder-bounce";
30
+ export const WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE = "unfinished-reason-bounce";
29
31
 
30
32
  /** Historical package hook ownership marker — uninstall criterion only. */
31
33
  const HOOK_MARKER = "ak-roles: worker-submission-gates reference-transaction";
@@ -33,7 +35,6 @@ const HOOKS_DIR = "ak-roles-hooks";
33
35
  const HOOK_FILE = "reference-transaction";
34
36
  /** Open platform-prefix domain (constitution #10) — not a closed singleton. */
35
37
  const PLATFORM_PREFIX = /^[A-Za-z][A-Za-z0-9_-]*:/;
36
- const UNFINISHED_REASON_BOUNCE_LIMIT = 2;
37
38
 
38
39
  export type WorkerSubmissionGateParent = RecordSessionParent;
39
40
 
@@ -171,14 +172,16 @@ function unfinishedReasonPresent(details?: unknown): boolean {
171
172
  return containsWrittenReason(details.reason);
172
173
  }
173
174
 
174
- function readGateState(session: HostRecordSession): {
175
+ function readGateState(session: HostRecordSession, invocationScopeId?: string): {
175
176
  baseline: string | null | undefined;
176
177
  reminded: boolean;
177
178
  prefixReminded: boolean;
179
+ unfinishedReasonBounces: number;
178
180
  } {
179
181
  let baseline: string | null | undefined;
180
182
  let reminded = false;
181
183
  let prefixReminded = false;
184
+ let unfinishedReasonBounces = 0;
182
185
  for (const entry of session.getEntries()) {
183
186
  if (entry.type !== "custom") continue;
184
187
  if (entry.customType === WORKER_COMMIT_BASELINE_ENTRY_TYPE) {
@@ -190,9 +193,13 @@ function readGateState(session: HostRecordSession): {
190
193
  reminded = true;
191
194
  } else if (entry.customType === WORKER_PREFIX_REMINDER_BOUNCE_ENTRY_TYPE) {
192
195
  prefixReminded = true;
196
+ } else if (entry.customType === WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE
197
+ && invocationScopeId !== undefined && isRecord(entry.data)
198
+ && entry.data.invocationScopeId === invocationScopeId) {
199
+ unfinishedReasonBounces += 1;
193
200
  }
194
201
  }
195
- return { baseline, reminded, prefixReminded };
202
+ return { baseline, reminded, prefixReminded, unfinishedReasonBounces };
196
203
  }
197
204
 
198
205
  function isAncestor(cwd: string, ancestor: string, descendant: string): boolean {
@@ -234,13 +241,20 @@ export type CreateWorkerSubmissionGateOptions = {
234
241
  * Path still derives from required parent session; home only pins ledger root. #604 Scope 2.
235
242
  */
236
243
  readonly home?: string;
244
+ /**
245
+ * #1132: ADR 0050 缺理由催全次数 read from the single configured
246
+ * `autoResumeLimit` value. Absent = package default. Resolved once here; the
247
+ * gate never re-reads it per submission. Exhaustion still accepts (照收) —
248
+ * only the count changes.
249
+ */
250
+ readonly unfinishedReasonBounceLimit?: number;
237
251
  };
238
252
 
239
253
  export function createWorkerSubmissionGate(
240
254
  options: CreateWorkerSubmissionGateOptions = {},
241
255
  ): {
242
256
  /** Durable parent is required — ownership must be known at arm (#857 loud failure). */
243
- arm(cwd: string, parent: WorkerSubmissionGateParent): void;
257
+ arm(cwd: string, parent: WorkerSubmissionGateParent, invocationScopeId?: string): void;
244
258
  assertAcceptable(status: string, details?: unknown): void;
245
259
  } {
246
260
  let baseline: string | null | undefined;
@@ -248,11 +262,14 @@ export function createWorkerSubmissionGate(
248
262
  let reminded = false;
249
263
  let prefixReminded = false;
250
264
  let unfinishedReasonBounces = 0;
265
+ let invocationScopeId: string | undefined;
251
266
  let record: HostRecordSession | undefined;
252
267
  /** Parent session file retained so every gate sitian write path-derives the same ledger home. */
253
268
  let sessionParent: string | undefined;
254
269
  const explicitHome =
255
270
  typeof options.home === "string" && options.home.length > 0 ? options.home : undefined;
271
+ // #1132: one configured number, resolved once at gate construction.
272
+ const unfinishedReasonBounceLimit = deliveryLimitFromConfig(options.unfinishedReasonBounceLimit);
256
273
  const head = (cwd: string): string | null => {
257
274
  try {
258
275
  return git(cwd, ["rev-parse", "HEAD"]);
@@ -273,7 +290,8 @@ export function createWorkerSubmissionGate(
273
290
  });
274
291
  };
275
292
  return {
276
- arm(cwd, parent) {
293
+ arm(cwd, parent, scope) {
294
+ invocationScopeId = scope;
277
295
  uninstallPackageWorkerHooks(cwd);
278
296
  root = cwd;
279
297
  sessionParent = parent.getSessionFile();
@@ -282,7 +300,8 @@ export function createWorkerSubmissionGate(
282
300
  kind: WORKER_SUBMISSION_GATE_RECORD_KIND,
283
301
  parent,
284
302
  });
285
- const prior = readGateState(record);
303
+ const prior = readGateState(record, invocationScopeId);
304
+ unfinishedReasonBounces = prior.unfinishedReasonBounces;
286
305
  if (prior.baseline !== undefined) {
287
306
  baseline = prior.baseline;
288
307
  reminded = prior.reminded;
@@ -304,7 +323,10 @@ export function createWorkerSubmissionGate(
304
323
  },
305
324
  assertAcceptable(status, details) {
306
325
  if (status === "unfinished" && !unfinishedReasonPresent(details)) {
307
- if (unfinishedReasonBounces < UNFINISHED_REASON_BOUNCE_LIMIT) {
326
+ if (unfinishedReasonBounces < unfinishedReasonBounceLimit) {
327
+ record?.appendCustomEntry(WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE, {
328
+ version: 1, invocationScopeId,
329
+ });
308
330
  unfinishedReasonBounces += 1;
309
331
  throw new WorkerUnfinishedReasonReminderError();
310
332
  }
@@ -345,3 +367,6 @@ export function createWorkerSubmissionGate(
345
367
  },
346
368
  };
347
369
  }
370
+
371
+ /** The gate object returned above. Callers reference this instead of restating assertAcceptable. */
372
+ export type WorkerSubmissionGate = ReturnType<typeof createWorkerSubmissionGate>;