@akagilnc/pi-workflow-roles 0.1.4586 → 0.1.4631
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.md +2 -2
- package/README.zh-CN.md +3 -3
- package/dist/acp-host/production-host.js +1184 -717
- package/dist/activation-ledger-topology.js +0 -4
- package/dist/archivist-record-topology.js +27 -35
- package/dist/headless-host/description.js +7 -4
- package/dist/headless-host/production-host.js +998 -595
- package/dist/migrate-book-topology.js +3 -6
- package/dist/navigator-attendance.js +146 -241
- package/dist/navigator-public-session.js +81 -15
- package/dist/package-contracts/navigator-output.js +30 -12
- package/dist/pi/role-turn-host.js +0 -3
- package/dist/public-cli/auto-resume.js +26 -0
- package/dist/public-cli/judge-run.js +2 -2
- package/dist/public-cli/main.js +1119 -870
- package/dist/public-cli/option-definitions.js +1 -1
- package/dist/public-cli/post-admission.js +103 -9
- package/dist/public-cli/process-cancel.js +57 -0
- package/dist/public-cli/run-lifecycle.js +29 -7
- package/dist/public-cli/settlement.js +46 -32
- package/dist/public-cli/terminal.js +8 -27
- package/dist/public-role-summons.js +22 -4
- package/dist/submission-ledger.js +56 -23
- package/package.json +1 -1
- package/resources/836-deleted-machine-instruction-inventory.md +2 -0
- package/resources/navigator-route-playbook.md +9 -0
- package/scripts/build-package.mjs +6 -2
- package/src/acp-host/role-turn-host.ts +129 -13
- 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 +1 -1
- package/src/headless-host/description.ts +17 -7
- package/src/headless-host/role-turn-host.ts +42 -11
- package/src/navigator-attendance.ts +290 -425
- package/src/navigator-public-session.ts +126 -25
- package/src/navigator-role.ts +2 -2
- package/src/package-contracts/navigator-output.ts +41 -25
- 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/coder-run.ts +2 -2
- package/src/public-cli/fixer-run.ts +2 -2
- package/src/public-cli/judge-run.ts +2 -2
- package/src/public-cli/main.ts +25 -3
- package/src/public-cli/merger-run.ts +2 -2
- package/src/public-cli/option-definitions.ts +1 -1
- package/src/public-cli/post-admission.ts +175 -7
- package/src/public-cli/process-cancel.ts +75 -0
- package/src/public-cli/run-lifecycle.ts +37 -7
- package/src/public-cli/settlement.ts +42 -33
- package/src/public-cli/terminal.ts +12 -37
- package/src/public-role-summons.ts +34 -5
- package/src/role-envelope.ts +89 -4
- package/src/role-runtime.ts +78 -65
- package/src/submission-ledger.ts +63 -21
- package/dist/activation-reconciliation.js +0 -61
- package/dist/public-cli/command-renderer.js +0 -5
- package/dist/public-command-renderer.js +0 -29
- package/src/activation-reconciliation.ts +0 -163
- package/src/public-cli/command-renderer.ts +0 -9
- package/src/public-command-renderer.ts +0 -55
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* Navigator attendance session factory (#675 r3).
|
|
3
3
|
* Each prepare turn uses the public navigator activation path (summonPublicRole) —
|
|
4
4
|
* same seat table and shared envelope as `ak-role navigator`.
|
|
5
|
-
* Archivist createRecordSession
|
|
6
|
-
*
|
|
5
|
+
* Archivist createRecordSession books the attendance nest under the parent;
|
|
6
|
+
* host conversation continuity uses CLI resume (`resumeRunId`), not a package
|
|
7
|
+
* advice ledger (development-closure: do not invent package-level memory).
|
|
7
8
|
*/
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
|
|
8
11
|
import { sitianReport } from "./sitian-facade.ts";
|
|
9
12
|
import {
|
|
10
13
|
NavigatorUnavailableError,
|
|
@@ -17,6 +20,8 @@ import {
|
|
|
17
20
|
} from "./navigator-session-contracts.ts";
|
|
18
21
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
19
22
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
23
|
+
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
24
|
+
import { CliUsageError } from "./public-cli/cli-errors.ts";
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* Ledger process home for navigator attendance: admitted HostContext.runDirectory
|
|
@@ -40,13 +45,73 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
|
|
|
40
45
|
return undefined;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
/** Resume key only — points at a host run principal, never stores advice prose. */
|
|
49
|
+
export const NAVIGATOR_HOST_RUN_POINTER_ENTRY = "ak-navigator-host-run";
|
|
50
|
+
|
|
51
|
+
function exactRecord(value: unknown): value is Record<string, unknown> {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
|
|
56
|
+
const entry = basename(runDirectory);
|
|
57
|
+
const suffix = "@navigator";
|
|
58
|
+
if (!entry.endsWith(suffix)) return undefined;
|
|
59
|
+
const runId = entry.slice(0, entry.length - suffix.length);
|
|
60
|
+
return runId.length > 0 ? runId : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readNavigatorHostRunPointer(entries: readonly unknown[]): string | undefined {
|
|
64
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
65
|
+
const entry = entries[index];
|
|
66
|
+
if (!exactRecord(entry)) continue;
|
|
67
|
+
if (entry.type === "custom" && entry.customType === NAVIGATOR_HOST_RUN_POINTER_ENTRY) {
|
|
68
|
+
const data = entry.data;
|
|
69
|
+
if (exactRecord(data) && typeof data.runId === "string" && data.runId.trim() !== "") {
|
|
70
|
+
return data.runId.trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Typed preflight via public CLI load path: can this run reopen as navigator?
|
|
79
|
+
* CliUsageError (code AK_ROLE_USAGE) from loadResumableInstructionSeatRun means
|
|
80
|
+
* unknown id / principal unavailable / non-instruction seat — not resumable.
|
|
81
|
+
* Other throws propagate. Never reads stderr prose.
|
|
82
|
+
*/
|
|
83
|
+
export async function navigatorHostRunResumable(home: string, runId: string): Promise<boolean> {
|
|
84
|
+
const { piDurablePrincipalAuthority } = await import("./pi/durable-principal.ts");
|
|
85
|
+
const { loadResumableInstructionSeatRun } = await import("./public-cli/run-lifecycle.ts");
|
|
86
|
+
try {
|
|
87
|
+
const loaded = await loadResumableInstructionSeatRun(home, runId, piDurablePrincipalAuthority);
|
|
88
|
+
return loaded.admitted.role === "navigator";
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof CliUsageError) return false;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type NavigatorPublicSummon = (options: {
|
|
96
|
+
readonly role: "navigator";
|
|
97
|
+
readonly argv: readonly string[];
|
|
98
|
+
readonly cwd: string;
|
|
99
|
+
readonly home?: string;
|
|
100
|
+
readonly resumeRunId?: string;
|
|
101
|
+
}) => Promise<PublicSummonResult>;
|
|
102
|
+
|
|
103
|
+
export function createNativeNavigatorSessionFactory(deps?: {
|
|
104
|
+
/** Test/composition inject — production leaves unset and uses public-role-summons. */
|
|
105
|
+
readonly summonPublicRole?: NavigatorPublicSummon;
|
|
106
|
+
/** Test inject — production uses navigatorHostRunResumable (typed CLI load). */
|
|
107
|
+
readonly hostRunResumable?: (home: string, runId: string) => Promise<boolean>;
|
|
108
|
+
}): NavigatorSessionFactory {
|
|
44
109
|
return async ({ context, subject, tool }) => {
|
|
45
110
|
// Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
|
|
46
111
|
// Factory open only books the archivist nest — no package default, no eager seat read.
|
|
47
112
|
let thinkingLevel: string | undefined;
|
|
48
113
|
|
|
49
|
-
// Archivist nest for attendance
|
|
114
|
+
// Archivist nest for attendance bookkeeping (ADR 0018 / 0065) — not a host session open.
|
|
50
115
|
// #852: navigator/<work-subject> is the sole book-top exception; always pass subject so
|
|
51
116
|
// unmaterialized/missing parent still gets a durable nest instead of silent in-memory.
|
|
52
117
|
// Home comes from HostContext.runDirectory (or ledger parent path) — never context.home.
|
|
@@ -64,6 +129,14 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
|
|
|
64
129
|
let noReceipt: NoReceiptLifecycleFacts | undefined;
|
|
65
130
|
let disposed = false;
|
|
66
131
|
let inFlightPrompt: Promise<unknown> | undefined;
|
|
132
|
+
/** In-factory host run id for CLI resume; durable pointer also lives on the nest. */
|
|
133
|
+
let hostRunId = readNavigatorHostRunPointer(sessionManager.getEntries() as readonly unknown[]);
|
|
134
|
+
|
|
135
|
+
const summon: NavigatorPublicSummon = deps?.summonPublicRole
|
|
136
|
+
?? (async (options) => {
|
|
137
|
+
const { summonPublicRole } = await import("./public-role-summons.ts");
|
|
138
|
+
return summonPublicRole(options);
|
|
139
|
+
});
|
|
67
140
|
|
|
68
141
|
return {
|
|
69
142
|
prompt: async (text) => {
|
|
@@ -74,15 +147,28 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
|
|
|
74
147
|
noReceipt = undefined;
|
|
75
148
|
const run = (async () => {
|
|
76
149
|
try {
|
|
77
|
-
const { summonPublicRole } = await import("./public-role-summons.ts");
|
|
78
150
|
const summonHome = await resolveNavigatorLedgerHome(context);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
151
|
+
const resumeRunId = hostRunId;
|
|
152
|
+
|
|
153
|
+
const baseSummon = {
|
|
154
|
+
role: "navigator" as const,
|
|
155
|
+
argv: [text] as const,
|
|
83
156
|
cwd: context.cwd,
|
|
84
157
|
...(summonHome === undefined ? {} : { home: summonHome }),
|
|
85
|
-
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Prefer host CLI resume so prior advice stays on the host session.
|
|
161
|
+
// Fresh mint only when typed load says the principal cannot reopen.
|
|
162
|
+
const resumable = deps?.hostRunResumable ?? navigatorHostRunResumable;
|
|
163
|
+
let summoned: PublicSummonResult;
|
|
164
|
+
if (resumeRunId === undefined || summonHome === undefined) {
|
|
165
|
+
summoned = await summon(baseSummon);
|
|
166
|
+
} else if (await resumable(summonHome, resumeRunId)) {
|
|
167
|
+
summoned = await summon({ ...baseSummon, resumeRunId });
|
|
168
|
+
} else {
|
|
169
|
+
hostRunId = undefined;
|
|
170
|
+
summoned = await summon(baseSummon);
|
|
171
|
+
}
|
|
86
172
|
|
|
87
173
|
const outcome = summoned.terminal?.roleOutcome;
|
|
88
174
|
if (outcome === undefined) {
|
|
@@ -109,23 +195,40 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
|
|
|
109
195
|
noReceipt = outcome;
|
|
110
196
|
return;
|
|
111
197
|
}
|
|
198
|
+
|
|
199
|
+
// Pin host run for the next attendance prompt on this subject (CLI resume key).
|
|
200
|
+
const runDirectory = summoned.runDirectory
|
|
201
|
+
?? (typeof summoned.admitted?.runDirectory === "string"
|
|
202
|
+
? summoned.admitted.runDirectory
|
|
203
|
+
: undefined);
|
|
204
|
+
if (typeof runDirectory === "string" && runDirectory.trim() !== "") {
|
|
205
|
+
const nextRunId = runIdFromNavigatorDirectory(runDirectory);
|
|
206
|
+
if (nextRunId !== undefined) {
|
|
207
|
+
hostRunId = nextRunId;
|
|
208
|
+
sessionManager.appendCustomEntry(NAVIGATOR_HOST_RUN_POINTER_ENTRY, { runId: nextRunId });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
112
212
|
if (outcome.kind !== "accepted") {
|
|
113
|
-
// Non-accepted kinds (e.g. audit_escalation): no
|
|
213
|
+
// Non-accepted kinds (e.g. audit_escalation): no prose advice this turn.
|
|
114
214
|
// Not a shape-unusable judgment on the navigator reply (#757).
|
|
115
215
|
return;
|
|
116
216
|
}
|
|
217
|
+
// #959: present navigator words as prose. No candidates/next parsing.
|
|
218
|
+
const { navigatorProseFromUnknown } = await import("./package-contracts/navigator-output.ts");
|
|
219
|
+
const proseParts: string[] = [];
|
|
117
220
|
for (const payload of outcome.payloads ?? []) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!Array.isArray(candidates)) continue;
|
|
121
|
-
await tool.execute(
|
|
122
|
-
"navigator-public-prepare",
|
|
123
|
-
{ candidates },
|
|
124
|
-
undefined,
|
|
125
|
-
undefined,
|
|
126
|
-
context as never,
|
|
127
|
-
);
|
|
221
|
+
const prose = navigatorProseFromUnknown(payload);
|
|
222
|
+
if (prose !== undefined) proseParts.push(prose);
|
|
128
223
|
}
|
|
224
|
+
if (proseParts.length === 0) return;
|
|
225
|
+
await tool.execute(
|
|
226
|
+
"navigator-public-prepare",
|
|
227
|
+
{ prose: proseParts.join("\n\n") },
|
|
228
|
+
undefined,
|
|
229
|
+
undefined,
|
|
230
|
+
context as never,
|
|
231
|
+
);
|
|
129
232
|
} catch (error) {
|
|
130
233
|
if (error instanceof NavigatorUnavailableError) throw error;
|
|
131
234
|
const fact = navigatorProviderFailureFromError(error);
|
|
@@ -166,10 +269,8 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
|
|
|
166
269
|
} catch (error) {
|
|
167
270
|
throw navigatorUnavailableError("model", error);
|
|
168
271
|
}
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// / #617 DK-3). A seat edit between prepares therefore applies on the next
|
|
172
|
-
// summon — it is never an unavailable session.
|
|
272
|
+
// Resume and fresh mint both read the live seat table (#675 / #617 DK-3).
|
|
273
|
+
// A seat edit between prepares applies on the next host turn.
|
|
173
274
|
thinkingLevel = nextThinking ?? nextParsed.thinkingLevel;
|
|
174
275
|
},
|
|
175
276
|
getThinkingLevel: () => thinkingLevel,
|
package/src/navigator-role.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorOutputSchema } from "./package-con
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
|
|
5
|
-
*
|
|
5
|
+
* #959: navigator speaks free-form prose; tool is a lifecycle vehicle only.
|
|
6
6
|
*/
|
|
7
7
|
export const NAVIGATOR_TOOL_SPEC = {
|
|
8
8
|
name: NAVIGATOR_OUTPUT_TOOL_NAME,
|
|
9
9
|
label: "游奕使建议",
|
|
10
|
-
description: "
|
|
10
|
+
description: "游奕使终局回执:散文建议,原样呈现。",
|
|
11
11
|
promptSnippet: "游奕使建议",
|
|
12
12
|
parameters: navigatorOutputSchema,
|
|
13
13
|
} as const;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public Navigator (游奕使) terminating receipt contracts (#639).
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Public Navigator (游奕使) terminating receipt contracts (#639 / #959).
|
|
3
|
+
* Navigator speaks free-form prose. The tool is only a lifecycle vehicle so the
|
|
4
|
+
* host can seal a turn; the model-facing content is prose, not structured route
|
|
5
|
+
* candidates. Code does not parse, rank, or judge the advice.
|
|
5
6
|
* No usable result is infrastructure failure via public settlement, not a judgment status (#475).
|
|
6
7
|
*/
|
|
7
8
|
import { Type } from "typebox";
|
|
@@ -15,42 +16,57 @@ export const NAVIGATOR_ACCEPTED_TEXT = "游奕使建议已受理";
|
|
|
15
16
|
export const navigatorOutputSchema = withTerminatingOutputDeclarations(
|
|
16
17
|
openToolObject(
|
|
17
18
|
Type.Object({
|
|
18
|
-
|
|
19
|
-
description: "
|
|
20
|
-
}),
|
|
21
|
-
candidates: Type.Unknown({
|
|
22
|
-
description: "排好序的路线建议数组,元素含 next/phase/reason — 形状指引,非 schema 闸",
|
|
19
|
+
prose: Type.Unknown({
|
|
20
|
+
description: "游奕使散文建议,原样呈现。不要求 candidates/next 等结构化字段。形状指引,非 schema 闸",
|
|
23
21
|
}),
|
|
24
22
|
}),
|
|
25
23
|
),
|
|
26
24
|
);
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type NavigatorAdvice = { readonly status: "advice"; readonly candidates: readonly unknown[] };
|
|
26
|
+
export type NavigatorAdvice = { readonly prose: string };
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
|
-
* Project one
|
|
36
|
-
*
|
|
37
|
-
*
|
|
29
|
+
* Project one Navigator advice receipt as prose.
|
|
30
|
+
* Accepts { prose }, a bare string, or any object — objects stringify as the
|
|
31
|
+
* prose body so historical / free-form submissions still present. Shape is not
|
|
32
|
+
* an admission gate (ADR 0055 / 第 0 条 / #959).
|
|
38
33
|
*/
|
|
39
34
|
export function projectLawfulNavigatorOutput(value: unknown): NavigatorAdvice | undefined {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
if (value === undefined || value === null) return undefined;
|
|
36
|
+
if (typeof value === "string") return { prose: value };
|
|
37
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
38
|
+
const record = value as Record<string, unknown>;
|
|
39
|
+
if (typeof record.prose === "string") return { prose: record.prose };
|
|
40
|
+
if (record.prose !== undefined && record.prose !== null) {
|
|
41
|
+
return { prose: typeof record.prose === "string" ? record.prose : String(record.prose) };
|
|
42
|
+
}
|
|
43
|
+
// Free-form object submission: present the whole body as prose when it has content.
|
|
44
|
+
const keys = Object.keys(record);
|
|
45
|
+
if (keys.length === 0) return { prose: "" };
|
|
46
|
+
try {
|
|
47
|
+
return { prose: JSON.stringify(record) };
|
|
48
|
+
} catch {
|
|
49
|
+
return { prose: String(record) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { prose: String(value) };
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
|
-
* Settlement/recording path:
|
|
48
|
-
* Does not gate role admission — callers must not use this to reject a submission.
|
|
56
|
+
* Settlement/recording path: project prose. Does not gate role admission.
|
|
49
57
|
*/
|
|
50
58
|
export function validateRecordedNavigatorOutput(value: unknown): NavigatorAdvice {
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const projected = projectLawfulNavigatorOutput(value);
|
|
60
|
+
if (projected === undefined) {
|
|
61
|
+
throw new Error("Navigator output is empty");
|
|
53
62
|
}
|
|
54
|
-
return
|
|
63
|
+
return projected;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
/** Extract display prose from any navigator payload / attendance body. */
|
|
67
|
+
export function navigatorProseFromUnknown(value: unknown): string | undefined {
|
|
68
|
+
const projected = projectLawfulNavigatorOutput(value);
|
|
69
|
+
if (projected === undefined) return undefined;
|
|
70
|
+
const trimmed = projected.prose.trim();
|
|
71
|
+
return trimmed === "" ? undefined : projected.prose;
|
|
72
|
+
}
|
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,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type AdmittedCoderInvocation,
|
|
23
23
|
} from "./invocation.ts";
|
|
24
24
|
import {
|
|
25
|
-
|
|
25
|
+
buildAutoResumeContinuationPrompt,
|
|
26
26
|
loadResumableCoderRun,
|
|
27
27
|
markRunAdmitted,
|
|
28
28
|
type PublicResumeRequest,
|
|
@@ -198,7 +198,7 @@ export async function runPublicCoder(
|
|
|
198
198
|
: { correlationId: admitted.correlationId ?? env.correlationId }),
|
|
199
199
|
continuation: {
|
|
200
200
|
kind: "resume",
|
|
201
|
-
prompt:
|
|
201
|
+
prompt: buildAutoResumeContinuationPrompt({
|
|
202
202
|
packageRoot: env.packageRoot,
|
|
203
203
|
...pickEngineAxis(env),
|
|
204
204
|
}),
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type AdmittedFixerInvocation,
|
|
26
26
|
} from "./invocation.ts";
|
|
27
27
|
import {
|
|
28
|
-
|
|
28
|
+
buildAutoResumeContinuationPrompt,
|
|
29
29
|
loadResumableFixerRun,
|
|
30
30
|
markRunAdmitted,
|
|
31
31
|
type PublicResumeRequest,
|
|
@@ -213,7 +213,7 @@ export async function runPublicFixer(
|
|
|
213
213
|
: { correlationId: admitted.correlationId ?? env.correlationId }),
|
|
214
214
|
continuation: {
|
|
215
215
|
kind: "resume",
|
|
216
|
-
prompt:
|
|
216
|
+
prompt: buildAutoResumeContinuationPrompt({
|
|
217
217
|
packageRoot: env.packageRoot,
|
|
218
218
|
...pickEngineAxis(env),
|
|
219
219
|
}),
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
loadResumableJudgeRun,
|
|
20
20
|
markRunAdmitted,
|
|
21
|
-
|
|
21
|
+
buildAutoResumeContinuationPrompt,
|
|
22
22
|
type PublicResumeRequest,
|
|
23
23
|
} from "./run-lifecycle.ts";
|
|
24
24
|
import {
|
|
@@ -152,7 +152,7 @@ export async function runPublicJudge(
|
|
|
152
152
|
: { correlationId: admitted.correlationId ?? env.correlationId }),
|
|
153
153
|
continuation: {
|
|
154
154
|
kind: "resume",
|
|
155
|
-
prompt:
|
|
155
|
+
prompt: buildAutoResumeContinuationPrompt({
|
|
156
156
|
packageRoot: env.packageRoot,
|
|
157
157
|
...pickEngineAxis(env),
|
|
158
158
|
}),
|
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
|
+
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
loadResumableMergerRun,
|
|
26
26
|
markRunAdmitted,
|
|
27
|
-
|
|
27
|
+
buildAutoResumeContinuationPrompt,
|
|
28
28
|
type PublicResumeRequest,
|
|
29
29
|
} from "./run-lifecycle.ts";
|
|
30
30
|
import {
|
|
@@ -241,7 +241,7 @@ export async function runPublicMerger(
|
|
|
241
241
|
: { correlationId: admitted.correlationId ?? env.correlationId }),
|
|
242
242
|
continuation: {
|
|
243
243
|
kind: "resume",
|
|
244
|
-
prompt:
|
|
244
|
+
prompt: buildAutoResumeContinuationPrompt({
|
|
245
245
|
packageRoot: env.packageRoot,
|
|
246
246
|
...pickEngineAxis(env),
|
|
247
247
|
}),
|
|
@@ -1280,7 +1280,7 @@ const ROLE_COMMAND_HELP = {
|
|
|
1280
1280
|
},
|
|
1281
1281
|
navigator: {
|
|
1282
1282
|
command: "navigator",
|
|
1283
|
-
summary: "Direct Navigator (游奕使) route advice
|
|
1283
|
+
summary: "Direct Navigator (游奕使) free-form prose route advice.",
|
|
1284
1284
|
usage: ["ak-role navigator [options] [instruction]"],
|
|
1285
1285
|
examples: [
|
|
1286
1286
|
'ak-role navigator "刚完成 coder apply 收敛,下一步?"',
|