@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca
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 +129 -39
- package/README.zh-CN.md +128 -39
- package/dist/activation-ledger-topology.js +1 -1
- package/dist/archivist-record-entry.js +47 -26
- package/dist/auditor-dossier-tool.js +3 -1
- package/dist/compliance-transport.js +17 -26
- package/dist/doctor-contracts.js +1 -2
- package/dist/dossier-resolution.js +4 -8
- package/dist/engine-detour-tool.js +33 -8
- package/dist/engine-detour.js +1 -0
- package/dist/evidence-child-executor.js +277 -424
- package/dist/host-contracts.js +22 -0
- package/dist/institutional-resolution.js +88 -0
- package/dist/merger-contracts.js +1 -2
- package/dist/navigator-attendance.js +16 -0
- package/dist/notary-contracts.js +2 -3
- package/dist/package-contracts/fixer-output.js +1 -2
- package/dist/packaged-role-registry.js +102 -9
- package/dist/pi/in-process-session.js +671 -0
- package/dist/public-cli/config.js +7 -5
- package/dist/public-cli/main.js +3767 -4113
- package/dist/reviewer-child-executor.js +6 -1
- package/dist/session-opening-materials.js +10 -38
- package/dist/sitian-appender.js +146 -0
- package/dist/sitian-contracts.js +21 -0
- package/dist/sitian-facade.js +15 -0
- package/dist/sitian-reader.js +52 -0
- package/extensions/role-runtime.ts +9 -7
- package/package.json +5 -5
- package/resources/engines/cursor.md +12 -26
- package/resources/engines/hermes.md +2 -23
- package/resources/engines/opus.md +1 -16
- package/resources/navigator-route-playbook.md +2 -2
- package/scripts/build-package.mjs +0 -1
- package/souls/quality-law.md +1 -1
- package/src/activation-ledger-topology.ts +1 -1
- package/src/archivist-record-entry.ts +57 -45
- package/src/auditor-dossier-tool.ts +4 -2
- package/src/canonical-skill-binding.ts +37 -27
- package/src/collector-role.ts +11 -20
- package/src/collector-tool-schemas.ts +1 -4
- package/src/compliance-transport.ts +19 -44
- package/src/doctor-auditor.ts +1 -5
- package/src/doctor-contracts.ts +1 -4
- package/src/doctor-role.ts +31 -8
- package/src/dossier-resolution.ts +8 -8
- package/src/engine-detour-tool.ts +48 -19
- package/src/engine-detour.ts +3 -0
- package/src/evidence-child-executor.ts +277 -485
- package/src/factory-board.ts +3 -3
- package/src/gatekeeper-role.ts +21 -10
- package/src/host-contracts.ts +386 -0
- package/src/in-process-session.ts +1 -115
- package/src/institutional-resolution.ts +135 -0
- package/src/judge-auditor.ts +1 -5
- package/src/judge-role.ts +37 -51
- package/src/merger-contracts.ts +1 -4
- package/src/merger-role.ts +7 -11
- package/src/navigator-attendance.ts +20 -1
- package/src/notary-contracts.ts +8 -11
- package/src/notary-role.ts +7 -23
- package/src/notary-source-run.ts +4 -3
- package/src/package-contracts/fixer-output.ts +1 -4
- package/src/package-resources/method-skill-binding.ts +10 -34
- package/src/packaged-role-registry.ts +122 -12
- package/src/pi/adapter.ts +192 -0
- package/src/pi/durable-principal.ts +98 -0
- package/src/pi/in-process-session.ts +819 -0
- package/src/pi/known-failure.ts +77 -0
- package/src/pi/pi-normalization.ts +284 -0
- package/src/pi/role-turn-host.ts +459 -0
- package/src/public-cli/auto-resume.ts +42 -44
- package/src/public-cli/cli.ts +171 -257
- package/src/public-cli/coder-run.ts +115 -433
- package/src/public-cli/collector-run.ts +62 -314
- package/src/public-cli/config.ts +7 -5
- package/src/public-cli/doctor-run.ts +41 -52
- package/src/public-cli/fixer-run.ts +122 -427
- package/src/public-cli/invocation.ts +287 -96
- package/src/public-cli/judge-run.ts +92 -422
- package/src/public-cli/merger-run.ts +167 -429
- package/src/public-cli/notary-run.ts +41 -47
- package/src/public-cli/option-definitions.ts +136 -3
- package/src/public-cli/post-admission.ts +497 -0
- package/src/public-cli/public-run-credentials.ts +4 -4
- package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
- package/src/public-cli/reviewer-run.ts +132 -449
- package/src/public-cli/run-lifecycle.ts +339 -327
- package/src/public-cli/settlement.ts +251 -134
- package/src/public-cli/terminal.ts +3 -9
- package/src/public-cli/turn-request.ts +87 -0
- package/src/reviewer-child-executor.ts +9 -1
- package/src/reviewer-role.ts +11 -20
- package/src/role-runtime.ts +129 -78
- package/src/session-opening-materials.ts +25 -39
- package/src/sitian-appender.ts +171 -0
- package/src/sitian-contracts.ts +106 -0
- package/src/sitian-facade.ts +18 -0
- package/src/sitian-reader.ts +62 -0
- package/src/worker-role.ts +42 -40
- package/src/worker-submission-gates.ts +33 -0
- package/dist/archivist-role-run-coordinates.js +0 -28
- package/dist/package-contracts/terminating-infrastructure.js +0 -90
- package/resources/engines/sonnet.md +0 -27
- package/resources/engines/zcode.md +0 -63
- package/src/archivist-role-run-coordinates.ts +0 -45
- package/src/package-contracts/terminating-infrastructure.ts +0 -132
- package/src/public-cli/explicit-internal.ts +0 -427
- package/src/public-cli/one-shot-dispatch.ts +0 -300
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi adapter for the host-neutral main-session execution seam (#526 / S1b-2).
|
|
3
|
+
* Owns argv construction, spawn/SIGTERM/close, and session codec helpers.
|
|
4
|
+
* public-cli runners project RoleTurnRequest; this module is the sole argv owner.
|
|
5
|
+
*/
|
|
6
|
+
import { execFile, spawn } from "node:child_process";
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { access, realpath } from "node:fs/promises";
|
|
9
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { platform } from "node:process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { appendFile, readFile } from "node:fs/promises";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
DurablePrincipal,
|
|
17
|
+
DurablePrincipalAuthority,
|
|
18
|
+
MethodBinding,
|
|
19
|
+
RoleTurnActivation,
|
|
20
|
+
RoleTurnHost,
|
|
21
|
+
RoleTurnKnownFailure,
|
|
22
|
+
RoleTurnModelConfig,
|
|
23
|
+
RoleTurnRequest,
|
|
24
|
+
RoleTurnResult,
|
|
25
|
+
} from "../host-contracts.ts";
|
|
26
|
+
import { ExplicitInternalActivationError } from "../host-contracts.ts";
|
|
27
|
+
import { applyEngineChildEnv } from "../engine-detour.ts";
|
|
28
|
+
import { decodePiDurablePrincipal } from "./durable-principal.ts";
|
|
29
|
+
|
|
30
|
+
/** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
|
|
31
|
+
const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
|
|
32
|
+
|
|
33
|
+
export function resolveInternalRoleEntrypoint(packageRoot: string): string {
|
|
34
|
+
return join(packageRoot, INTERNAL_ROLE_ENTRYPOINT_RELATIVE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Non-dispatch args: load Internal once and exit via Pi help (no model turn). */
|
|
38
|
+
export const EXPLICIT_INTERNAL_LOAD_PROBE_ARGS = [
|
|
39
|
+
"--no-skills",
|
|
40
|
+
"--no-prompt-templates",
|
|
41
|
+
"--no-themes",
|
|
42
|
+
"--no-context-files",
|
|
43
|
+
"--no-session",
|
|
44
|
+
"--help",
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export function buildExplicitInternalActivationArgs(
|
|
48
|
+
selectedRoleEntry: string,
|
|
49
|
+
extraArgs: readonly string[] = [],
|
|
50
|
+
): string[] {
|
|
51
|
+
return ["--no-extensions", "-e", selectedRoleEntry, ...extraArgs];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildSeatModelCliArgs(model: RoleTurnModelConfig | undefined): string[] {
|
|
55
|
+
if (model === undefined) return [];
|
|
56
|
+
return [
|
|
57
|
+
"--provider",
|
|
58
|
+
model.provider,
|
|
59
|
+
"--model",
|
|
60
|
+
model.model,
|
|
61
|
+
...(model.thinking === undefined ? [] : ["--thinking", model.thinking]),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
|
|
66
|
+
switch (activation.role) {
|
|
67
|
+
case "judge":
|
|
68
|
+
return ["--ak-role", "judge"];
|
|
69
|
+
case "coder":
|
|
70
|
+
return [
|
|
71
|
+
"--ak-role",
|
|
72
|
+
"coder",
|
|
73
|
+
"--ak-coder-phase",
|
|
74
|
+
activation.phase,
|
|
75
|
+
"--ak-coder-task",
|
|
76
|
+
activation.taskPath,
|
|
77
|
+
];
|
|
78
|
+
case "fixer":
|
|
79
|
+
return [
|
|
80
|
+
"--ak-role",
|
|
81
|
+
"fixer",
|
|
82
|
+
"--ak-fixer-phase",
|
|
83
|
+
activation.phase,
|
|
84
|
+
"--ak-fix-packet",
|
|
85
|
+
activation.packetPath,
|
|
86
|
+
...(activation.prerequisitesPath === undefined
|
|
87
|
+
? []
|
|
88
|
+
: ["--ak-fixer-prerequisites", activation.prerequisitesPath]),
|
|
89
|
+
];
|
|
90
|
+
case "reviewer":
|
|
91
|
+
return [
|
|
92
|
+
"--ak-role",
|
|
93
|
+
"reviewer",
|
|
94
|
+
"--ak-review-base",
|
|
95
|
+
activation.baseRevision,
|
|
96
|
+
...(activation.authorityRefs.length === 0
|
|
97
|
+
? []
|
|
98
|
+
: ["--ak-review-authority-refs", JSON.stringify([...activation.authorityRefs])]),
|
|
99
|
+
...(activation.ticketNumber === undefined
|
|
100
|
+
? []
|
|
101
|
+
: ["--ak-review-ticket-number", String(activation.ticketNumber)]),
|
|
102
|
+
];
|
|
103
|
+
case "merger":
|
|
104
|
+
return ["--ak-role", "merger", "--ak-merger-input", activation.inputPath];
|
|
105
|
+
case "collector":
|
|
106
|
+
return [
|
|
107
|
+
"--ak-role",
|
|
108
|
+
"collector",
|
|
109
|
+
"--ak-collector-repo",
|
|
110
|
+
activation.repo,
|
|
111
|
+
"--ak-collector-pr",
|
|
112
|
+
activation.pr,
|
|
113
|
+
...(activation.requestManifestPath === undefined
|
|
114
|
+
? []
|
|
115
|
+
: ["--ak-collector-request-manifest", activation.requestManifestPath]),
|
|
116
|
+
];
|
|
117
|
+
case "doctor":
|
|
118
|
+
return ["--ak-role", "doctor", "--ak-doctor-case", activation.casePath];
|
|
119
|
+
case "notary":
|
|
120
|
+
return ["--ak-role", "notary", "--ak-notary-source-run", activation.sourceRun];
|
|
121
|
+
default: {
|
|
122
|
+
const _exhaustive: never = activation;
|
|
123
|
+
return _exhaustive;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildMethodArgs(methods: readonly MethodBinding[]): string[] {
|
|
129
|
+
const skillArgs: string[] = [];
|
|
130
|
+
for (const method of methods) {
|
|
131
|
+
if (method.kind === "skill") {
|
|
132
|
+
skillArgs.push("--skill", method.path);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return skillArgs;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Translate a closed RoleTurnRequest into Pi argv after `--no-extensions -e entry`.
|
|
140
|
+
* Controlled session constants and session coordinates are adapter-internal.
|
|
141
|
+
*/
|
|
142
|
+
export function buildPiTurnExtraArgs(
|
|
143
|
+
request: RoleTurnRequest,
|
|
144
|
+
authority: DurablePrincipalAuthority,
|
|
145
|
+
extraPiArgs: readonly string[] = [],
|
|
146
|
+
): string[] {
|
|
147
|
+
const { sessionFile, sessionDirectory } = decodePiDurablePrincipal(
|
|
148
|
+
authority,
|
|
149
|
+
request.principal,
|
|
150
|
+
);
|
|
151
|
+
const prompt =
|
|
152
|
+
request.continuation.kind === "initial" || request.continuation.kind === "resume"
|
|
153
|
+
? request.continuation.prompt
|
|
154
|
+
: (() => {
|
|
155
|
+
const _exhaustive: never = request.continuation;
|
|
156
|
+
return _exhaustive;
|
|
157
|
+
})();
|
|
158
|
+
return [
|
|
159
|
+
"--no-skills",
|
|
160
|
+
...buildMethodArgs(request.methods),
|
|
161
|
+
"--no-prompt-templates",
|
|
162
|
+
"--no-themes",
|
|
163
|
+
"--no-context-files",
|
|
164
|
+
"--session",
|
|
165
|
+
sessionFile,
|
|
166
|
+
"--session-dir",
|
|
167
|
+
sessionDirectory,
|
|
168
|
+
...extraPiArgs,
|
|
169
|
+
...buildActivationFlagArgs(request.activation),
|
|
170
|
+
"--mode",
|
|
171
|
+
"json",
|
|
172
|
+
...buildSeatModelCliArgs(request.model),
|
|
173
|
+
prompt,
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type PiSpawnRunner = (
|
|
178
|
+
args: readonly string[],
|
|
179
|
+
options: {
|
|
180
|
+
cwd: string;
|
|
181
|
+
env: NodeJS.ProcessEnv;
|
|
182
|
+
timeoutMs?: number;
|
|
183
|
+
},
|
|
184
|
+
) => Promise<{
|
|
185
|
+
code: number | null;
|
|
186
|
+
stderr: string;
|
|
187
|
+
timedOut: boolean;
|
|
188
|
+
knownFailure?: RoleTurnKnownFailure;
|
|
189
|
+
}>;
|
|
190
|
+
|
|
191
|
+
export type LaunchedPiIdentity = {
|
|
192
|
+
readonly executable: string;
|
|
193
|
+
readonly version: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type LaunchedRolePackageIdentity = {
|
|
197
|
+
readonly roleEntry: string;
|
|
198
|
+
readonly rolePackageRoot: string;
|
|
199
|
+
readonly rolePackageVersion: string;
|
|
200
|
+
readonly entryMode: "public-cli";
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export type PiRoleTurnHostConfig = {
|
|
204
|
+
readonly packageRoot: string;
|
|
205
|
+
readonly principalAuthority: DurablePrincipalAuthority;
|
|
206
|
+
/** Test / seat-specific extra Pi args (faux provider etc.). */
|
|
207
|
+
readonly extraPiArgs?: readonly string[];
|
|
208
|
+
readonly timeoutMs?: number;
|
|
209
|
+
/** Low-level spawn seam (tests inject faux children). */
|
|
210
|
+
readonly spawnRunner?: PiSpawnRunner;
|
|
211
|
+
readonly recordLaunchedPiIdentity?: (
|
|
212
|
+
runDirectory: string,
|
|
213
|
+
identity: LaunchedPiIdentity,
|
|
214
|
+
) => Promise<void>;
|
|
215
|
+
readonly recordLaunchedRolePackageIdentity?: (
|
|
216
|
+
runDirectory: string,
|
|
217
|
+
identity: LaunchedRolePackageIdentity,
|
|
218
|
+
) => Promise<void>;
|
|
219
|
+
readonly observeLaunchedRolePackageIdentity?: (
|
|
220
|
+
packageRoot: string,
|
|
221
|
+
roleEntrypoint: string,
|
|
222
|
+
) => Promise<LaunchedRolePackageIdentity>;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const execFileAsync = promisify(execFile);
|
|
226
|
+
|
|
227
|
+
async function resolveSelectedPi(
|
|
228
|
+
command: string,
|
|
229
|
+
cwd: string,
|
|
230
|
+
env: NodeJS.ProcessEnv,
|
|
231
|
+
): Promise<string> {
|
|
232
|
+
const searchPath =
|
|
233
|
+
env.PATH ?? (platform === "win32" ? (process.env.PATH ?? "") : "/usr/bin:/bin");
|
|
234
|
+
const candidates =
|
|
235
|
+
isAbsolute(command) || command.includes("/")
|
|
236
|
+
? [resolve(cwd, command)]
|
|
237
|
+
: searchPath.split(delimiter).map((dir) => resolve(cwd, dir, command));
|
|
238
|
+
for (const candidate of candidates) {
|
|
239
|
+
try {
|
|
240
|
+
await access(candidate, constants.X_OK);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
243
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES") continue;
|
|
244
|
+
throw new ExplicitInternalActivationError(
|
|
245
|
+
`Pi executable resolution failed: ${String((error as Error).message)}`,
|
|
246
|
+
{ knownCause: "activation", cause: error },
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return await realpath(candidate);
|
|
250
|
+
}
|
|
251
|
+
throw new ExplicitInternalActivationError(`Pi executable not found: ${command}`, {
|
|
252
|
+
knownCause: "activation",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function selectedPiIdentity(
|
|
257
|
+
command: string,
|
|
258
|
+
cwd: string,
|
|
259
|
+
env: NodeJS.ProcessEnv,
|
|
260
|
+
): Promise<LaunchedPiIdentity> {
|
|
261
|
+
const executable = await resolveSelectedPi(command, cwd, env);
|
|
262
|
+
const { stdout } = await execFileAsync(executable, ["--version"], {
|
|
263
|
+
cwd,
|
|
264
|
+
env,
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
});
|
|
267
|
+
const version = stdout.trim();
|
|
268
|
+
if (version === "") throw new Error(`Pi executable returned an empty version: ${executable}`);
|
|
269
|
+
return { executable, version };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Default child runner: canonically select `pi` on PATH (or PI_BINARY) and launch
|
|
274
|
+
* that exact file. Close settles exactly once for natural return / error / SIGTERM.
|
|
275
|
+
*/
|
|
276
|
+
type SpawnedPiChild = ReturnType<typeof spawn> & {
|
|
277
|
+
stderr: NonNullable<ReturnType<typeof spawn>["stderr"]>;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export function createDefaultPiSpawnRunner(options: {
|
|
281
|
+
recordLaunchedPiIdentity?: (
|
|
282
|
+
runDirectory: string,
|
|
283
|
+
identity: LaunchedPiIdentity,
|
|
284
|
+
) => Promise<void>;
|
|
285
|
+
}): PiSpawnRunner {
|
|
286
|
+
return async (args, spawnOptions) => {
|
|
287
|
+
const command = spawnOptions.env.PI_BINARY ?? "pi";
|
|
288
|
+
const piIdentity = await selectedPiIdentity(command, spawnOptions.cwd, spawnOptions.env);
|
|
289
|
+
return await new Promise((resolveResult, reject) => {
|
|
290
|
+
// Child stdout is discarded at the stdio seam (CLAUDE.md Role invocation
|
|
291
|
+
// evidence). Do not pipe or accumulate it. stderr stays piped for diagnostics.
|
|
292
|
+
const child: SpawnedPiChild = spawn(piIdentity.executable, [...args], {
|
|
293
|
+
cwd: spawnOptions.cwd,
|
|
294
|
+
env: spawnOptions.env,
|
|
295
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
296
|
+
});
|
|
297
|
+
let stderr = "";
|
|
298
|
+
let timedOut = false;
|
|
299
|
+
// No default wall clock. Only an explicit caller budget arms a timer (ADR 0010).
|
|
300
|
+
// SIGKILL is unconditionally forbidden — graceful SIGTERM only.
|
|
301
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
302
|
+
let settled = false;
|
|
303
|
+
// `error` fires for pre-spawn failures (ENOENT after identity check is a
|
|
304
|
+
// true activation failure) or kill/dispatch errors. Retain it so `close`
|
|
305
|
+
// remains the SOLE settlement point (spec-B: child close once). Only
|
|
306
|
+
// fall back to rejecting on `error` if `close` never fires (e.g. spawn
|
|
307
|
+
// never succeeded so no `close` event will arrive).
|
|
308
|
+
let hasSpawned = false;
|
|
309
|
+
let executionError: Error | undefined;
|
|
310
|
+
const armTimeoutAfterChildReady = (): void => {
|
|
311
|
+
if (spawnOptions.timeoutMs === undefined) return;
|
|
312
|
+
timer = setTimeout(() => {
|
|
313
|
+
timedOut = true;
|
|
314
|
+
child.kill("SIGTERM");
|
|
315
|
+
}, spawnOptions.timeoutMs);
|
|
316
|
+
};
|
|
317
|
+
let identityRecorded: Promise<void> = Promise.resolve();
|
|
318
|
+
child.once("spawn", () => {
|
|
319
|
+
hasSpawned = true;
|
|
320
|
+
armTimeoutAfterChildReady();
|
|
321
|
+
const runDirectory = spawnOptions.env.AK_ROLE_RUN_DIR;
|
|
322
|
+
if (
|
|
323
|
+
typeof runDirectory === "string" &&
|
|
324
|
+
runDirectory !== "" &&
|
|
325
|
+
options.recordLaunchedPiIdentity !== undefined
|
|
326
|
+
) {
|
|
327
|
+
identityRecorded = options.recordLaunchedPiIdentity(runDirectory, piIdentity);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
331
|
+
stderr += chunk;
|
|
332
|
+
});
|
|
333
|
+
child.on("error", (error) => {
|
|
334
|
+
// A pre-spawn error has no child lifecycle to close. After spawn,
|
|
335
|
+
// retain the execution error and let the mandatory close event own
|
|
336
|
+
// cleanup and the single settlement.
|
|
337
|
+
if (settled || hasSpawned) {
|
|
338
|
+
executionError = error;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
342
|
+
settled = true;
|
|
343
|
+
reject(error);
|
|
344
|
+
});
|
|
345
|
+
child.on("close", (code) => {
|
|
346
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
347
|
+
void identityRecorded.then(
|
|
348
|
+
() => {
|
|
349
|
+
if (settled) return;
|
|
350
|
+
settled = true;
|
|
351
|
+
if (executionError !== undefined) {
|
|
352
|
+
reject(executionError);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
resolveResult({
|
|
356
|
+
code,
|
|
357
|
+
stderr,
|
|
358
|
+
timedOut,
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
(error) => {
|
|
362
|
+
if (settled) return;
|
|
363
|
+
settled = true;
|
|
364
|
+
reject(error);
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Create the production Pi RoleTurnHost (composition-root assembly). */
|
|
373
|
+
export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost {
|
|
374
|
+
const spawnRunner =
|
|
375
|
+
config.spawnRunner ??
|
|
376
|
+
createDefaultPiSpawnRunner({
|
|
377
|
+
...(config.recordLaunchedPiIdentity === undefined
|
|
378
|
+
? {}
|
|
379
|
+
: { recordLaunchedPiIdentity: config.recordLaunchedPiIdentity }),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
async executeTurn(request: RoleTurnRequest): Promise<RoleTurnResult> {
|
|
384
|
+
const roleEntry = await realpath(resolveInternalRoleEntrypoint(config.packageRoot));
|
|
385
|
+
const extraArgs = buildPiTurnExtraArgs(
|
|
386
|
+
request,
|
|
387
|
+
config.principalAuthority,
|
|
388
|
+
config.extraPiArgs ?? [],
|
|
389
|
+
);
|
|
390
|
+
const args = buildExplicitInternalActivationArgs(roleEntry, extraArgs);
|
|
391
|
+
const env: NodeJS.ProcessEnv = {
|
|
392
|
+
...process.env,
|
|
393
|
+
HOME: request.home,
|
|
394
|
+
PI_CODING_AGENT_DIR: request.agentDir,
|
|
395
|
+
AK_ROLE_RUN_DIR: request.runDirectory,
|
|
396
|
+
};
|
|
397
|
+
applyEngineChildEnv(env, request.engine);
|
|
398
|
+
if (request.correlationId !== undefined && request.correlationId.trim() !== "") {
|
|
399
|
+
env.AK_CORRELATION_ID = request.correlationId;
|
|
400
|
+
}
|
|
401
|
+
// Package provenance is known before Pi starts.
|
|
402
|
+
if (
|
|
403
|
+
config.recordLaunchedRolePackageIdentity !== undefined &&
|
|
404
|
+
config.observeLaunchedRolePackageIdentity !== undefined
|
|
405
|
+
) {
|
|
406
|
+
await config.recordLaunchedRolePackageIdentity(
|
|
407
|
+
request.runDirectory,
|
|
408
|
+
await config.observeLaunchedRolePackageIdentity(config.packageRoot, roleEntry),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const timeoutMs = request.timeoutMs ?? config.timeoutMs;
|
|
412
|
+
return await spawnRunner(args, {
|
|
413
|
+
cwd: request.cwd,
|
|
414
|
+
env,
|
|
415
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
import { sitianReport } from "../sitian-facade.ts";
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Append one custom JSONL entry to the durable principal's session file.
|
|
425
|
+
* Pi session codec only — AK artifact O_EXCL retention stays in public-cli.
|
|
426
|
+
*/
|
|
427
|
+
export async function appendPiSessionCustomEntry(
|
|
428
|
+
authority: DurablePrincipalAuthority,
|
|
429
|
+
principal: DurablePrincipal,
|
|
430
|
+
customType: string,
|
|
431
|
+
data: unknown,
|
|
432
|
+
): Promise<void> {
|
|
433
|
+
const { sessionFile } = decodePiDurablePrincipal(authority, principal);
|
|
434
|
+
const text = await readFile(sessionFile, "utf8");
|
|
435
|
+
let parentId: string | null = null;
|
|
436
|
+
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
437
|
+
const entry = JSON.parse(line) as { id?: unknown; type?: unknown };
|
|
438
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
439
|
+
}
|
|
440
|
+
const timestamp = new Date().toISOString();
|
|
441
|
+
const pointerLine = `${JSON.stringify({
|
|
442
|
+
type: "custom",
|
|
443
|
+
customType,
|
|
444
|
+
data,
|
|
445
|
+
id: randomUUID(),
|
|
446
|
+
parentId,
|
|
447
|
+
timestamp,
|
|
448
|
+
})}\n`;
|
|
449
|
+
await appendFile(sessionFile, pointerLine, "utf8");
|
|
450
|
+
try {
|
|
451
|
+
sitianReport({
|
|
452
|
+
level: "event",
|
|
453
|
+
kind: "dispatch-error",
|
|
454
|
+
sessionParent: sessionFile,
|
|
455
|
+
payload: { customType, data },
|
|
456
|
+
source: "pi-role-turn-host",
|
|
457
|
+
});
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { constants as fsConstants } from "node:fs";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import {
|
|
16
|
+
import { lstat, mkdir, open } from "node:fs/promises";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
|
+
import type {
|
|
20
|
+
DurablePrincipal,
|
|
21
|
+
DurablePrincipalAuthority,
|
|
22
|
+
SessionCustomEntryAppender,
|
|
23
|
+
} from "../host-contracts.ts";
|
|
19
24
|
import {
|
|
20
25
|
AUTO_RESUME_LIMIT,
|
|
21
26
|
describeErrorIdentity,
|
|
22
|
-
isSessionPrincipalAvailable,
|
|
23
27
|
acquireRunWriterLease,
|
|
24
28
|
markRunTerminal,
|
|
25
29
|
RunWriterLeaseHeldError,
|
|
@@ -192,7 +196,9 @@ function jsonSafeReplacer(): (key: string, value: unknown) => unknown {
|
|
|
192
196
|
* attempt can never overwrite an earlier attempt's file.
|
|
193
197
|
*/
|
|
194
198
|
async function retainDispatchError(
|
|
195
|
-
admitted: {
|
|
199
|
+
admitted: { runDirectory: string; principal: DurablePrincipal },
|
|
200
|
+
principalAuthority: DurablePrincipalAuthority,
|
|
201
|
+
sessionAppender: SessionCustomEntryAppender,
|
|
196
202
|
attempt: number,
|
|
197
203
|
error: unknown,
|
|
198
204
|
): Promise<{ file: string; pointerError?: unknown }> {
|
|
@@ -216,6 +222,7 @@ async function retainDispatchError(
|
|
|
216
222
|
// construction; a colliding name fails loudly instead of overwriting.
|
|
217
223
|
// O_NOFOLLOW when the platform provides it keeps a planted symlink from
|
|
218
224
|
// being followed; on platforms without it, exclusivity still holds.
|
|
225
|
+
// AK artifact owner stays here; only the session JSONL pointer uses Pi codec.
|
|
219
226
|
const noFollowFlag =
|
|
220
227
|
typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
221
228
|
const handle = await open(
|
|
@@ -228,13 +235,8 @@ async function retainDispatchError(
|
|
|
228
235
|
} finally {
|
|
229
236
|
await handle.close();
|
|
230
237
|
}
|
|
231
|
-
// Addressable pointer in the dossier (卷宗):
|
|
232
|
-
//
|
|
233
|
-
// same mechanism as #419 attempt history — no second ledger introduced).
|
|
234
|
-
// The one-writer lease is held for the append (#426 review: production
|
|
235
|
-
// dispatchers release the lease in their finally before the rejection reaches
|
|
236
|
-
// this point); if a concurrent writer already holds it, the append is skipped
|
|
237
|
-
// gracefully — the error file itself is already durably retained.
|
|
238
|
+
// Addressable pointer in the dossier (卷宗): Pi session custom-entry codec
|
|
239
|
+
// (appendPiSessionCustomEntry). Lease still owned here with run-writer.
|
|
238
240
|
let pointerLease: RunWriterLease;
|
|
239
241
|
try {
|
|
240
242
|
pointerLease = await acquireRunWriterLease(admitted.runDirectory);
|
|
@@ -243,32 +245,17 @@ async function retainDispatchError(
|
|
|
243
245
|
throw error;
|
|
244
246
|
}
|
|
245
247
|
// Pointer-stage failure (#426 fix_now #5) is separated from the file write:
|
|
246
|
-
// once the error file is durably on disk, a failed
|
|
247
|
-
//
|
|
248
|
-
// still handed back to the caller (retainedErrorFiles → terminal
|
|
249
|
-
// dispatchErrorFiles/artifacts), and the pointer failure is reported
|
|
250
|
-
// separately by the caller.
|
|
248
|
+
// once the error file is durably on disk, a failed session append must not
|
|
249
|
+
// reject through here and orphan it — the file path is still handed back.
|
|
251
250
|
let pointerError: unknown;
|
|
252
251
|
try {
|
|
253
|
-
const text = await readFile(admitted.sessionFile, "utf8");
|
|
254
|
-
// Session headers are not branch entries (#419 precedent in
|
|
255
|
-
// appendRunAttemptHistory excludes type === "session"); leave parentId null
|
|
256
|
-
// until a non-header entry exists so the pointer stays addressable.
|
|
257
|
-
let parentId: string | null = null;
|
|
258
|
-
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
259
|
-
const entry = JSON.parse(line) as { id?: unknown; type?: unknown };
|
|
260
|
-
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
261
|
-
}
|
|
262
252
|
const timestamp = new Date().toISOString();
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
timestamp,
|
|
270
|
-
})}\n`;
|
|
271
|
-
await appendFile(admitted.sessionFile, pointerLine, "utf8");
|
|
253
|
+
await sessionAppender(
|
|
254
|
+
principalAuthority,
|
|
255
|
+
admitted.principal,
|
|
256
|
+
DISPATCH_ERROR_RETENTION_ENTRY_TYPE,
|
|
257
|
+
{ version: 1, attempt, file: filePath, recordedAt: timestamp },
|
|
258
|
+
);
|
|
272
259
|
} catch (error) {
|
|
273
260
|
pointerError = error;
|
|
274
261
|
} finally {
|
|
@@ -331,15 +318,20 @@ function dispatchExceptionFailureTerminal(input: {
|
|
|
331
318
|
};
|
|
332
319
|
}
|
|
333
320
|
|
|
334
|
-
export async function runWithAutoResumeLoop<
|
|
321
|
+
export async function runWithAutoResumeLoop<
|
|
322
|
+
T extends AutoResumeDispatchResult,
|
|
323
|
+
TPayload = unknown,
|
|
324
|
+
>(options: {
|
|
335
325
|
admitted: {
|
|
336
|
-
sessionFile: string;
|
|
337
326
|
runDirectory: string;
|
|
338
327
|
/** Identity for the loop-owned typed failure terminal (dispatch-exception exhaustion). */
|
|
339
328
|
role: TerminalRoleName;
|
|
340
329
|
runId: string;
|
|
330
|
+
principal: DurablePrincipal;
|
|
341
331
|
};
|
|
332
|
+
principalAuthority: DurablePrincipalAuthority;
|
|
342
333
|
io: CliIo;
|
|
334
|
+
sessionAppender: SessionCustomEntryAppender;
|
|
343
335
|
/**
|
|
344
336
|
* Effective ceiling (#422), resolved by the caller before the loop; never re-read
|
|
345
337
|
* per round. undefined = package default (AUTO_RESUME_LIMIT). Domain-validated at
|
|
@@ -347,9 +339,9 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
347
339
|
* before the first dispatch instead of silently bypassing the ceiling comparison.
|
|
348
340
|
*/
|
|
349
341
|
autoResumeLimit?: number | undefined;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
dispatch: (
|
|
342
|
+
buildInitialPayload: () => TPayload;
|
|
343
|
+
buildResumePayload: () => TPayload;
|
|
344
|
+
dispatch: (payload: TPayload, lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
|
|
353
345
|
}): Promise<T> {
|
|
354
346
|
// #422 single-point resolution + domain validation. NaN would bypass every
|
|
355
347
|
// `attempts >= limit` comparison (always false) — reject here, before any dispatch.
|
|
@@ -357,7 +349,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
357
349
|
parseAutoResumeLimit(limit);
|
|
358
350
|
let autoResumeAttempts = 0;
|
|
359
351
|
let isFirst = true;
|
|
360
|
-
let
|
|
352
|
+
let currentPayload = options.buildInitialPayload();
|
|
361
353
|
let dispatchOrdinal = 0;
|
|
362
354
|
let lastThrownError: unknown;
|
|
363
355
|
let everyAttemptThrew = true;
|
|
@@ -379,7 +371,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
379
371
|
|
|
380
372
|
let result: T | undefined;
|
|
381
373
|
try {
|
|
382
|
-
result = await options.dispatch(
|
|
374
|
+
result = await options.dispatch(currentPayload, lease, isFirst, dummyIo);
|
|
383
375
|
} catch (error) {
|
|
384
376
|
// Owner 2026-08-23: 「出了异常,就原地记录错误信息,然后重试。」
|
|
385
377
|
// Retain the whole exception in place (per-attempt full file + dossier
|
|
@@ -392,7 +384,13 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
392
384
|
// Track the file as soon as it is durably written (#426 review):
|
|
393
385
|
// a pointer-stage failure comes back separately (pointerError) and must
|
|
394
386
|
// never orphan the retained file (#426 fix_now #5).
|
|
395
|
-
const { file, pointerError } = await retainDispatchError(
|
|
387
|
+
const { file, pointerError } = await retainDispatchError(
|
|
388
|
+
options.admitted,
|
|
389
|
+
options.principalAuthority,
|
|
390
|
+
options.sessionAppender,
|
|
391
|
+
attempt,
|
|
392
|
+
error,
|
|
393
|
+
);
|
|
396
394
|
retainedErrorFiles.push(file);
|
|
397
395
|
options.io.stderr(
|
|
398
396
|
`dispatch attempt ${attempt} threw (${describeErrorIdentity(error)}); full error retained at ${file}\n`,
|
|
@@ -430,7 +428,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
430
428
|
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
431
429
|
return result;
|
|
432
430
|
}
|
|
433
|
-
if (!(await
|
|
431
|
+
if (!(await options.principalAuthority.isAvailable(options.admitted.principal))) {
|
|
434
432
|
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
435
433
|
return result;
|
|
436
434
|
}
|
|
@@ -453,7 +451,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
453
451
|
terminal,
|
|
454
452
|
} as T;
|
|
455
453
|
}
|
|
456
|
-
if (!(await
|
|
454
|
+
if (!(await options.principalAuthority.isAvailable(options.admitted.principal))) {
|
|
457
455
|
const terminal = dispatchExceptionFailureTerminal({
|
|
458
456
|
role: options.admitted.role,
|
|
459
457
|
runId: options.admitted.runId,
|
|
@@ -473,7 +471,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
473
471
|
}
|
|
474
472
|
|
|
475
473
|
autoResumeAttempts++;
|
|
476
|
-
|
|
474
|
+
currentPayload = options.buildResumePayload();
|
|
477
475
|
isFirst = false;
|
|
478
476
|
}
|
|
479
477
|
}
|