@akagilnc/pi-workflow-roles 0.1.4620 → 0.1.4641
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.
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +962 -547
- package/dist/activation-ledger-topology.js +0 -4
- package/dist/gatekeeper-role.js +76 -6
- package/dist/headless-host/production-host.js +923 -508
- package/dist/migrate-book-topology.js +9 -0
- package/dist/pi/role-turn-host.js +0 -3
- package/dist/public-cli/auto-resume.js +26 -0
- package/dist/public-cli/countersign-run.js +31 -10
- package/dist/public-cli/main.js +1456 -1068
- package/dist/public-cli/post-admission.js +101 -7
- package/dist/public-cli/process-cancel.js +57 -0
- package/dist/public-cli/settlement.js +95 -2
- package/dist/public-role-summons.js +19 -0
- package/dist/secretariat-contracts.js +4 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -1
- package/src/activation-ledger-topology.ts +1 -6
- package/src/activation-ledger.ts +5 -236
- package/src/analyst-entry.ts +19 -2
- package/src/analyst-ledger.ts +111 -6
- package/src/archivist-record-entry.ts +3 -3
- package/src/audit-escalation.ts +1 -1
- package/src/gatekeeper-pass-envelope.ts +37 -5
- package/src/gatekeeper-role.ts +125 -20
- package/src/host-contracts.ts +22 -2
- package/src/pi/adapter.ts +5 -1
- package/src/pi/role-turn-host.ts +0 -3
- package/src/public-cli/auto-resume.ts +34 -0
- package/src/public-cli/cli.ts +30 -0
- package/src/public-cli/countersign-run.ts +50 -13
- package/src/public-cli/main.ts +25 -3
- package/src/public-cli/post-admission.ts +173 -6
- package/src/public-cli/process-cancel.ts +75 -0
- package/src/public-cli/settlement.ts +117 -1
- package/src/public-role-summons.ts +30 -4
- package/src/role-envelope.ts +27 -10
- package/src/role-runtime.ts +146 -55
- package/src/secretariat-contracts.ts +18 -0
- package/dist/activation-reconciliation.js +0 -61
- package/src/activation-reconciliation.ts +0 -163
package/src/gatekeeper-role.ts
CHANGED
|
@@ -12,30 +12,49 @@ import {
|
|
|
12
12
|
} from "./package-contracts/gatekeeper-output.ts";
|
|
13
13
|
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
14
14
|
import type { TerminalResult } from "./public-cli/terminal.ts";
|
|
15
|
+
import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
|
|
15
16
|
export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
|
|
16
17
|
export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
|
|
17
18
|
|
|
18
|
-
/** Gate review officers — 台院 / 符宝郎 / 审刑院 (#753 / #756). */
|
|
19
|
-
export type GateOfficer = "inspector" | "notary" | "auditor";
|
|
19
|
+
/** Gate review officers — 台院 / 符宝郎 / 审刑院 / 给事中 (#753 / #756 / #969). */
|
|
20
|
+
export type GateOfficer = "inspector" | "notary" | "auditor" | "countersign";
|
|
20
21
|
|
|
21
22
|
/** Officer routing only — content is self-fetched via the shared run-dossier tool (#632). */
|
|
22
23
|
export type GatekeeperSubject =
|
|
23
24
|
| { readonly kind: "worker_completion" }
|
|
24
25
|
| { readonly kind: "judge_draft" }
|
|
25
26
|
| { readonly kind: "judge_compliance" }
|
|
26
|
-
| { readonly kind: "countersign_verdict" }
|
|
27
|
+
| { readonly kind: "countersign_verdict" }
|
|
28
|
+
| { readonly kind: "secretariat_verdict" };
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Gate projection for the review queue (#753 / #750).
|
|
30
32
|
* Code only reads the conclusion field for queueing. Officer words ride as
|
|
31
33
|
* `receipt` unchanged — no findings rewrite, no unreadable/unusable label,
|
|
32
34
|
* no next-step selection for the parent.
|
|
35
|
+
* `runId` is the nested officer run id when known (summoned.runDirectory /
|
|
36
|
+
* terminal.runId) — public terminal projection consumes it (#969).
|
|
33
37
|
*/
|
|
34
38
|
export type GatekeeperResult =
|
|
35
|
-
| {
|
|
39
|
+
| {
|
|
40
|
+
readonly status: "pass";
|
|
41
|
+
readonly officer: GateOfficer;
|
|
42
|
+
readonly receipt: unknown;
|
|
43
|
+
readonly runId?: string;
|
|
44
|
+
}
|
|
36
45
|
/** bounce | escalate: both return the officer receipt to the parent (#753 / #756). */
|
|
37
|
-
| {
|
|
38
|
-
|
|
46
|
+
| {
|
|
47
|
+
readonly status: "bounce";
|
|
48
|
+
readonly officer: GateOfficer;
|
|
49
|
+
readonly receipt: unknown;
|
|
50
|
+
readonly runId?: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
readonly status: "escalate";
|
|
54
|
+
readonly officer: GateOfficer;
|
|
55
|
+
readonly receipt: unknown;
|
|
56
|
+
readonly runId?: string;
|
|
57
|
+
}
|
|
39
58
|
| {
|
|
40
59
|
/**
|
|
41
60
|
* Accepted reply whose conclusion is not pass|bounce|escalate.
|
|
@@ -45,6 +64,7 @@ export type GatekeeperResult =
|
|
|
45
64
|
readonly status: "needs_reask";
|
|
46
65
|
readonly officer: GateOfficer;
|
|
47
66
|
readonly receipt: unknown;
|
|
67
|
+
readonly runId?: string;
|
|
48
68
|
}
|
|
49
69
|
| { readonly status: "no_receipt"; readonly stage: GateOfficer; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
|
|
50
70
|
| {
|
|
@@ -64,13 +84,15 @@ export type GatekeeperNonPassResult = Extract<
|
|
|
64
84
|
function gateSeatLabel(stage: GateOfficer): string {
|
|
65
85
|
if (stage === "inspector") return "台院";
|
|
66
86
|
if (stage === "auditor") return "审刑院";
|
|
87
|
+
if (stage === "countersign") return "给事中";
|
|
67
88
|
return "符宝郎";
|
|
68
89
|
}
|
|
69
90
|
|
|
70
|
-
/** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector). */
|
|
91
|
+
/** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector, #969 secretariat/countersign). */
|
|
71
92
|
export function gateOfficerForSubject(subject: GatekeeperSubject): GateOfficer {
|
|
72
93
|
if (subject.kind === "worker_completion") return "inspector";
|
|
73
94
|
if (subject.kind === "judge_compliance") return "auditor";
|
|
95
|
+
if (subject.kind === "secretariat_verdict") return "countersign";
|
|
74
96
|
return "notary";
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -183,6 +205,14 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
183
205
|
* `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
|
|
184
206
|
* (keeps missing-args sentinel intact as the receipt).
|
|
185
207
|
*/
|
|
208
|
+
/** Map 给事中 countersignStatus onto the shared gate queue words (#969). */
|
|
209
|
+
function gateStatusFromCountersign(status: string): string | undefined {
|
|
210
|
+
if (status === "converged") return "pass";
|
|
211
|
+
if (status === "continue") return "bounce";
|
|
212
|
+
if (status === "escalate") return "escalate";
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
function projectOfficerDecision(
|
|
187
217
|
officer: GateOfficer,
|
|
188
218
|
decision: unknown,
|
|
@@ -190,6 +220,26 @@ function projectOfficerDecision(
|
|
|
190
220
|
): GatekeeperResult {
|
|
191
221
|
const receipt = retainedReceipt(decision);
|
|
192
222
|
const record = readRecord(decision);
|
|
223
|
+
// 给事中: only countersignStatus, strict three-word map; never generic status,
|
|
224
|
+
// never unmapped raw word, never fallbackStatus (#969 / ADR 0040/0055).
|
|
225
|
+
if (officer === "countersign") {
|
|
226
|
+
const countersignStatus =
|
|
227
|
+
record !== undefined && typeof record.countersignStatus === "string"
|
|
228
|
+
? record.countersignStatus
|
|
229
|
+
: undefined;
|
|
230
|
+
const status =
|
|
231
|
+
typeof countersignStatus === "string"
|
|
232
|
+
? gateStatusFromCountersign(countersignStatus)
|
|
233
|
+
: undefined;
|
|
234
|
+
if (status === "pass") {
|
|
235
|
+
return { status: "pass", officer, receipt };
|
|
236
|
+
}
|
|
237
|
+
if (status === "bounce" || status === "escalate") {
|
|
238
|
+
return { status, officer, receipt };
|
|
239
|
+
}
|
|
240
|
+
return { status: "needs_reask", officer, receipt };
|
|
241
|
+
}
|
|
242
|
+
// inspector / notary / auditor: shared gate words on generic status.
|
|
193
243
|
const status =
|
|
194
244
|
(record !== undefined && typeof record.status === "string" ? record.status : undefined)
|
|
195
245
|
?? fallbackStatus;
|
|
@@ -240,6 +290,41 @@ function projectOfficerPayloads(
|
|
|
240
290
|
return projectOfficerDecision(officer, payloads[payloads.length - 1], fallbackStatus);
|
|
241
291
|
}
|
|
242
292
|
|
|
293
|
+
/** Nested officer runId from summoned.runDirectory, else terminal.runId (#969). */
|
|
294
|
+
export function officerRunIdFromSummoned(summoned: PublicSummonResult): string | undefined {
|
|
295
|
+
if (typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== "") {
|
|
296
|
+
const fromDir = runIdFromRunDirectory(summoned.runDirectory);
|
|
297
|
+
if (fromDir !== undefined) return fromDir;
|
|
298
|
+
}
|
|
299
|
+
const terminal = summoned.terminal;
|
|
300
|
+
if (
|
|
301
|
+
terminal !== undefined
|
|
302
|
+
&& typeof terminal.runId === "string"
|
|
303
|
+
&& terminal.runId.trim() !== ""
|
|
304
|
+
) {
|
|
305
|
+
return terminal.runId;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function withOfficerRunId(
|
|
311
|
+
result: GatekeeperResult,
|
|
312
|
+
summoned: PublicSummonResult,
|
|
313
|
+
): GatekeeperResult {
|
|
314
|
+
if (
|
|
315
|
+
result.status !== "pass"
|
|
316
|
+
&& result.status !== "bounce"
|
|
317
|
+
&& result.status !== "escalate"
|
|
318
|
+
&& result.status !== "needs_reask"
|
|
319
|
+
) {
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
if (typeof result.runId === "string" && result.runId.trim() !== "") return result;
|
|
323
|
+
const runId = officerRunIdFromSummoned(summoned);
|
|
324
|
+
if (runId === undefined) return result;
|
|
325
|
+
return { ...result, runId };
|
|
326
|
+
}
|
|
327
|
+
|
|
243
328
|
function projectOfficerTerminal(
|
|
244
329
|
officer: GateOfficer,
|
|
245
330
|
summoned: PublicSummonResult,
|
|
@@ -279,25 +364,34 @@ function projectOfficerTerminal(
|
|
|
279
364
|
};
|
|
280
365
|
}
|
|
281
366
|
if (outcome.kind === "audit_escalation") {
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
367
|
+
return withOfficerRunId(
|
|
368
|
+
{
|
|
369
|
+
status: "escalate",
|
|
370
|
+
officer,
|
|
371
|
+
// This-court receipt only (#879) — historical rows remain on terminal.submissions.
|
|
372
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
373
|
+
},
|
|
374
|
+
summoned,
|
|
375
|
+
);
|
|
288
376
|
}
|
|
289
377
|
if (outcome.kind === "accepted") {
|
|
290
378
|
// outcome.status is the fixture/compat leaf: production settlement leaves
|
|
291
379
|
// it undefined once payloads are recorded, so this only matters when a
|
|
292
380
|
// caller still supplies status without any recorded payload (#836 hang).
|
|
293
|
-
return
|
|
381
|
+
return withOfficerRunId(
|
|
382
|
+
projectOfficerPayloads(officer, thisCourt, outcome.status),
|
|
383
|
+
summoned,
|
|
384
|
+
);
|
|
294
385
|
}
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
386
|
+
return withOfficerRunId(
|
|
387
|
+
{
|
|
388
|
+
status: "needs_reask",
|
|
389
|
+
officer,
|
|
390
|
+
// This-court receipt only (#879).
|
|
391
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
392
|
+
},
|
|
393
|
+
summoned,
|
|
394
|
+
);
|
|
301
395
|
}
|
|
302
396
|
|
|
303
397
|
/** Projection carrier for the shared submit envelope (ADR 0018). No lifecycle book here. */
|
|
@@ -311,10 +405,21 @@ export type GatekeeperProjection = {
|
|
|
311
405
|
/**
|
|
312
406
|
* Plain-language re-ask when the officer conclusion is not pass|bounce|escalate.
|
|
313
407
|
* Not a packaged engine handbook line (#755 exception for 读不出三态).
|
|
408
|
+
* inspector / notary / auditor keep shared gate words; 给事中 uses own three-state
|
|
409
|
+
* field and words so reask does not invite non-contract values (#969 / ADR 0055).
|
|
314
410
|
*/
|
|
315
411
|
export const OFFICER_CONCLUSION_REASK =
|
|
316
412
|
"上次交卷的结论不是 pass、bounce、escalate 三态之一。请重新输出,结论字段写明其一;打回或上呈的话就是给对方看的原文。" as const;
|
|
317
413
|
|
|
414
|
+
/** 给事中 re-ask: countersignStatus converged|continue|escalate only (#969). */
|
|
415
|
+
export const COUNTERSIGN_CONCLUSION_REASK =
|
|
416
|
+
"上次交卷的 countersignStatus 不是 converged、continue、escalate 三态之一。请重新输出,countersignStatus 写明其一;封驳或上呈的话就是给对方看的原文。" as const;
|
|
417
|
+
|
|
418
|
+
/** Pick the re-ask line for the officer under review. */
|
|
419
|
+
export function officerConclusionReask(officer: GateOfficer): string {
|
|
420
|
+
return officer === "countersign" ? COUNTERSIGN_CONCLUSION_REASK : OFFICER_CONCLUSION_REASK;
|
|
421
|
+
}
|
|
422
|
+
|
|
318
423
|
/**
|
|
319
424
|
* Summon (via injected seam) + project. Default summonGateOfficer drive lives
|
|
320
425
|
* on the shared envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675) —
|
package/src/host-contracts.ts
CHANGED
|
@@ -337,7 +337,12 @@ type HostEventHandler<K extends keyof HostEventMap> = (event: HostEventMap[K], c
|
|
|
337
337
|
export type HostEventRegistration = { [K in keyof HostEventMap]: [event: K, handler: HostEventHandler<K>] }[keyof HostEventMap];
|
|
338
338
|
|
|
339
339
|
type HostGatekeeperSubject = {
|
|
340
|
-
readonly kind:
|
|
340
|
+
readonly kind:
|
|
341
|
+
| "worker_completion"
|
|
342
|
+
| "judge_draft"
|
|
343
|
+
| "judge_compliance"
|
|
344
|
+
| "countersign_verdict"
|
|
345
|
+
| "secretariat_verdict";
|
|
341
346
|
};
|
|
342
347
|
/** Gatekeeper non-pass faces returned to parent (#836 includes transport_failure; never kill leg). */
|
|
343
348
|
type HostGatekeeperNonPass = { readonly status: "bounce" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
|
|
@@ -388,7 +393,22 @@ export interface RoleHost {
|
|
|
388
393
|
getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
|
|
389
394
|
setActiveTools(names: string[]): void;
|
|
390
395
|
getActiveTools(): string[];
|
|
391
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Shared gate envelope. On pass may return officer snapshot (receipt + nested
|
|
398
|
+
* runId) for seat public-terminal projection (#969); callers may ignore it.
|
|
399
|
+
*/
|
|
400
|
+
requireGatekeeperPass?(options: {
|
|
401
|
+
context: HostContext;
|
|
402
|
+
subject: HostGatekeeperSubject;
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
hostActions: HostGatekeeperActions;
|
|
405
|
+
toolCallId: string;
|
|
406
|
+
submission?: unknown;
|
|
407
|
+
}): Promise<void | {
|
|
408
|
+
readonly officer: string;
|
|
409
|
+
readonly receipt: unknown;
|
|
410
|
+
readonly runId?: string;
|
|
411
|
+
}>;
|
|
392
412
|
on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
|
|
393
413
|
on(event: "input", handler: HostEventHandler<"input">): void;
|
|
394
414
|
on(event: "tool_call", handler: HostEventHandler<"tool_call">): void;
|
package/src/pi/adapter.ts
CHANGED
|
@@ -92,7 +92,11 @@ function requirePiGatekeeperPass(options: {
|
|
|
92
92
|
hostActions: HostGatekeeperActions;
|
|
93
93
|
toolCallId: string;
|
|
94
94
|
submission?: unknown;
|
|
95
|
-
}): Promise<void
|
|
95
|
+
}): Promise<void | {
|
|
96
|
+
readonly officer: string;
|
|
97
|
+
readonly receipt: unknown;
|
|
98
|
+
readonly runId?: string;
|
|
99
|
+
}> {
|
|
96
100
|
return requireGatekeeperPass({
|
|
97
101
|
context: options.context,
|
|
98
102
|
subject: options.subject,
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -470,9 +470,6 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
470
470
|
) {
|
|
471
471
|
env.AK_ROLE_AUDITOR_SUBJECT = process.env.AK_ROLE_AUDITOR_SUBJECT;
|
|
472
472
|
}
|
|
473
|
-
if (request.correlationId !== undefined && request.correlationId.trim() !== "") {
|
|
474
|
-
env.AK_CORRELATION_ID = request.correlationId;
|
|
475
|
-
}
|
|
476
473
|
if (
|
|
477
474
|
config.recordLaunchedRolePackageIdentity !== undefined &&
|
|
478
475
|
config.observeLaunchedRolePackageIdentity !== undefined
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type RunWriterLease,
|
|
33
33
|
} from "./run-lifecycle.ts";
|
|
34
34
|
import { parseAutoResumeLimit } from "./config.ts";
|
|
35
|
+
import { processCancelSignalName } from "./process-cancel.ts";
|
|
35
36
|
import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifactRef, type TerminalResult, type TerminalRoleName } from "./terminal.ts";
|
|
36
37
|
import {
|
|
37
38
|
attachRecordedSubmissions,
|
|
@@ -473,6 +474,11 @@ export async function runWithAutoResumeLoop<
|
|
|
473
474
|
* before the first dispatch instead of silently bypassing the ceiling comparison.
|
|
474
475
|
*/
|
|
475
476
|
autoResumeLimit?: number | undefined;
|
|
477
|
+
/**
|
|
478
|
+
* #855: process-cancel signal. Once aborted by a catchable process signal,
|
|
479
|
+
* the loop must not dispatch another turn or spawn another child.
|
|
480
|
+
*/
|
|
481
|
+
signal?: AbortSignal;
|
|
476
482
|
buildInitialPayload: () => TPayload;
|
|
477
483
|
buildResumePayload: () => TPayload;
|
|
478
484
|
dispatch: (payload: TPayload, lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
|
|
@@ -578,6 +584,11 @@ export async function runWithAutoResumeLoop<
|
|
|
578
584
|
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
579
585
|
return result;
|
|
580
586
|
}
|
|
587
|
+
// #855: process cancel — stop even if dispatch forgot skipAutoResume.
|
|
588
|
+
if (processCancelSignalName(options.signal) !== undefined) {
|
|
589
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
581
592
|
}
|
|
582
593
|
|
|
583
594
|
if (result !== undefined) {
|
|
@@ -616,6 +627,29 @@ export async function runWithAutoResumeLoop<
|
|
|
616
627
|
terminal,
|
|
617
628
|
} as T;
|
|
618
629
|
}
|
|
630
|
+
// #855: process cancel on the throw path — do not re-dispatch; name the signal.
|
|
631
|
+
const cancelName = processCancelSignalName(options.signal);
|
|
632
|
+
if (cancelName !== undefined) {
|
|
633
|
+
const terminal = await attachDispatchExceptionTerminal(
|
|
634
|
+
options.admitted,
|
|
635
|
+
dispatchExceptionFailureTerminal({
|
|
636
|
+
role: options.admitted.role,
|
|
637
|
+
runId: options.admitted.runId,
|
|
638
|
+
causeError: lastThrownError,
|
|
639
|
+
errorFiles: retainedErrorFiles,
|
|
640
|
+
autoResumeAttempts,
|
|
641
|
+
endReason: `ak-role terminated by ${cancelName}`,
|
|
642
|
+
everyAttemptThrew,
|
|
643
|
+
}),
|
|
644
|
+
options.io,
|
|
645
|
+
);
|
|
646
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
647
|
+
presentTerminal(terminal, options.io);
|
|
648
|
+
return {
|
|
649
|
+
exitCode: 1,
|
|
650
|
+
terminal,
|
|
651
|
+
} as T;
|
|
652
|
+
}
|
|
619
653
|
if (!(await isPrincipalAvailable(options.admitted.principal))) {
|
|
620
654
|
const terminal = await attachDispatchExceptionTerminal(
|
|
621
655
|
options.admitted,
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -305,6 +305,11 @@ export type CliEnv = {
|
|
|
305
305
|
* skip same-ticket auto-resume. Not a public flag — the verb is the choice.
|
|
306
306
|
*/
|
|
307
307
|
freshSummons?: true;
|
|
308
|
+
/**
|
|
309
|
+
* #855: process-level cancel (SIGTERM/SIGINT/SIGHUP). Nested turns receive
|
|
310
|
+
* the same signal so hosts can gracefully stop children.
|
|
311
|
+
*/
|
|
312
|
+
signal?: AbortSignal;
|
|
308
313
|
};
|
|
309
314
|
|
|
310
315
|
|
|
@@ -413,6 +418,7 @@ function createRoleEnvironment(
|
|
|
413
418
|
? {}
|
|
414
419
|
: { autoResumeLimit: options.config.autoResumeLimit }),
|
|
415
420
|
...(env.freshSummons === true ? { freshSummons: true as const } : {}),
|
|
421
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
416
422
|
};
|
|
417
423
|
}
|
|
418
424
|
|
|
@@ -1168,6 +1174,30 @@ async function runConfigCommand(
|
|
|
1168
1174
|
throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
|
|
1169
1175
|
}
|
|
1170
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* #855: process-cancel handlers only for commands that run a role turn
|
|
1179
|
+
* (callable roles / resume / new). analyst/roles/config/help keep Node's
|
|
1180
|
+
* default SIGINT/SIGTERM termination — installing handlers would swallow Ctrl+C.
|
|
1181
|
+
*/
|
|
1182
|
+
export function commandNeedsProcessCancel(argv: readonly string[]): boolean {
|
|
1183
|
+
// Decision-time parse must not escape the CLI usage boundary. Malformed
|
|
1184
|
+
// global flags throw CliUsageError here; return false and let runAkRole
|
|
1185
|
+
// present the same structured usage reject (exit 2). Handlers are moot
|
|
1186
|
+
// when parse fails — install-or-not has no subsequent effect.
|
|
1187
|
+
let parsed: ParsedGlobal;
|
|
1188
|
+
try {
|
|
1189
|
+
parsed = parseArgv(argv);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
if (error instanceof CliUsageError) return false;
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
if (parsed.help || parsed.command === undefined || parsed.command === "help") {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
if (parsed.command === "resume" || parsed.command === "new") return true;
|
|
1198
|
+
return isPublicCallableRole(parsed.command);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1171
1201
|
export async function runAkRole(
|
|
1172
1202
|
argv: readonly string[],
|
|
1173
1203
|
env: CliEnv,
|
|
@@ -88,6 +88,22 @@ export type CountersignRunEnv = PostAdmissionEnv & {
|
|
|
88
88
|
* replace — never union. Production leaves unset outside that handoff.
|
|
89
89
|
*/
|
|
90
90
|
pendingCourtTicketNumbers?: readonly number[];
|
|
91
|
+
/**
|
|
92
|
+
* #969 gate path: plain-language re-ask when prior 给事中 reply was not three-state.
|
|
93
|
+
* Wins over gateReviewInstruction when both present (notary/inspector precedent).
|
|
94
|
+
*/
|
|
95
|
+
reviewReask?: string;
|
|
96
|
+
/**
|
|
97
|
+
* #969/#879 Secretariat submission-gate body: verbatim parent payload as dialogue
|
|
98
|
+
* content when reviewReask is absent. Binding stays ticket / argv instruction.
|
|
99
|
+
*/
|
|
100
|
+
gateReviewInstruction?: string;
|
|
101
|
+
/**
|
|
102
|
+
* #969 gate path: parent run durable board ticket handoff. Same-ticket resume
|
|
103
|
+
* key and identity 起居郎 bind under this key — never re-derived from receipt
|
|
104
|
+
* ticketNumber (optional / omit-able payload field).
|
|
105
|
+
*/
|
|
106
|
+
boundTicketNumber?: number;
|
|
91
107
|
};
|
|
92
108
|
|
|
93
109
|
/** Project admitted invocation onto the host-neutral turn request. */
|
|
@@ -470,6 +486,10 @@ export async function runPublicCountersign(
|
|
|
470
486
|
instruction: parsed.instruction,
|
|
471
487
|
projectRoot: admitted.projectRoot,
|
|
472
488
|
failureLabel: "unbound summons",
|
|
489
|
+
// #969: parent durable ticket is the resume/bind key (ADR 0079).
|
|
490
|
+
...(env.boundTicketNumber === undefined
|
|
491
|
+
? {}
|
|
492
|
+
: { boundTicketNumber: env.boundTicketNumber }),
|
|
473
493
|
},
|
|
474
494
|
env,
|
|
475
495
|
io,
|
|
@@ -528,16 +548,30 @@ export async function runPublicCountersign(
|
|
|
528
548
|
}
|
|
529
549
|
|
|
530
550
|
// Missing lawful 起居郎 terminal is not countersign body failure: leave
|
|
531
|
-
// unbound and continue (true-unbound face)
|
|
532
|
-
//
|
|
551
|
+
// unbound and continue (true-unbound face) — unless parent gate handoff
|
|
552
|
+
// already carries the durable board ticket (#969). Escalate / typed
|
|
553
|
+
// failure above; bound refresh still fails honest via station.
|
|
533
554
|
if (outcome.identity.kind === "ticket") {
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
// #969: parent durable handoff wins over 起居郎 re-assert when both
|
|
556
|
+
// present (receipt/assert mismatch → parent resume key).
|
|
557
|
+
typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
|
|
558
|
+
? env.boundTicketNumber
|
|
559
|
+
: outcome.identity.ticketNumber;
|
|
536
560
|
// #871: identity may hand a co-review set; absent field defaults to [main].
|
|
537
561
|
typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
|
|
562
|
+
} else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
|
|
563
|
+
// Parent board handoff present; 起居郎 returned true-unbound — still
|
|
564
|
+
// resume/bind under the parent key (omitted receipt ticketNumber path).
|
|
565
|
+
typedTicket = env.boundTicketNumber;
|
|
566
|
+
}
|
|
567
|
+
if (typedTicket !== undefined) {
|
|
568
|
+
// #969/#879: gate re-ask / parent submission body share summons.instruction
|
|
569
|
+
// (reask wins; argv instruction remains the 起居郎 identity face above).
|
|
570
|
+
const resumeInstruction =
|
|
571
|
+
env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction;
|
|
538
572
|
const summons: SameTicketSummonsMaterials = {
|
|
539
|
-
instruction:
|
|
540
|
-
instructionEmpty:
|
|
573
|
+
instruction: resumeInstruction,
|
|
574
|
+
instructionEmpty: resumeInstruction.trim() === "",
|
|
541
575
|
};
|
|
542
576
|
const resumed = await tryResumeSameTicketSeatRun({
|
|
543
577
|
home: env.home,
|
|
@@ -641,13 +675,16 @@ export async function runPublicCountersign(
|
|
|
641
675
|
: { correlationId: env.correlationId }),
|
|
642
676
|
continuation: {
|
|
643
677
|
kind: "initial",
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
678
|
+
// #969/#879: gate path first mint carries parent payload as dialogue content;
|
|
679
|
+
// ordinary public entry keeps package transport prompt.
|
|
680
|
+
prompt: (env.reviewReask ?? env.gateReviewInstruction)
|
|
681
|
+
?? buildCountersignTransportPrompt(
|
|
682
|
+
admitted,
|
|
683
|
+
engineSessionMaterialFromOptions({
|
|
684
|
+
...pickEngineAxis(env),
|
|
685
|
+
packageRoot: env.packageRoot,
|
|
686
|
+
}),
|
|
687
|
+
),
|
|
651
688
|
},
|
|
652
689
|
};
|
|
653
690
|
// Mutable shell: ticket bind re-projects activation before executeTurn.
|
package/src/public-cli/main.ts
CHANGED
|
@@ -34,6 +34,28 @@ process.env.AK_ROLE_PACKAGE_ROOT = packageRoot;
|
|
|
34
34
|
// The host-provided runtime must be resolvable before the CLI module graph loads it.
|
|
35
35
|
ensureHostPiRuntimeResolvable(packageRoot);
|
|
36
36
|
|
|
37
|
-
const { runAkRole } = await import("./cli.ts");
|
|
38
|
-
const
|
|
39
|
-
|
|
37
|
+
const { runAkRole, commandNeedsProcessCancel } = await import("./cli.ts");
|
|
38
|
+
const { installProcessCancelHandlers } = await import("./process-cancel.ts");
|
|
39
|
+
|
|
40
|
+
// #855: catchable signals only for role-turn commands. analyst/roles/config/help
|
|
41
|
+
// keep Node default termination — global handlers would swallow Ctrl+C.
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
const processCancel = commandNeedsProcessCancel(argv)
|
|
44
|
+
? installProcessCancelHandlers()
|
|
45
|
+
: undefined;
|
|
46
|
+
try {
|
|
47
|
+
const result = await runAkRole(argv, {
|
|
48
|
+
packageRoot,
|
|
49
|
+
...(processCancel === undefined ? {} : { signal: processCancel.signal }),
|
|
50
|
+
});
|
|
51
|
+
// Signal termination is never a successful public exit, even if a seat
|
|
52
|
+
// somehow returned zero before settlement folded the abort.
|
|
53
|
+
process.exitCode =
|
|
54
|
+
processCancel !== undefined &&
|
|
55
|
+
processCancel.receivedSignal() !== undefined &&
|
|
56
|
+
result.exitCode === 0
|
|
57
|
+
? 1
|
|
58
|
+
: result.exitCode;
|
|
59
|
+
} finally {
|
|
60
|
+
processCancel?.dispose();
|
|
61
|
+
}
|