@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193
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/dist/acp-host/production-host.js +2368 -1155
- package/dist/archivist-record-entry.js +204 -20
- package/dist/archivist-record-topology.js +36 -16
- package/dist/auditor-dossier-tool.js +2 -1
- package/dist/diarist-contracts.js +96 -0
- package/dist/doctor-auditor.js +1 -1
- package/dist/doctor-evidence.js +3 -2
- package/dist/dossier-resolution.js +21 -8
- package/dist/gatekeeper-role.js +34 -27
- package/dist/headless-host/description.js +7 -8
- package/dist/headless-host/production-host.js +2375 -1149
- package/dist/host-contracts.js +16 -0
- package/dist/inspector-contracts.js +8 -0
- package/dist/migrate-book-topology.js +278917 -0
- package/dist/navigator-public-session.js +22 -25
- package/dist/notary-source-run.js +7 -4
- package/dist/packaged-role-registry.js +1 -1
- package/dist/pi/durable-principal.js +9 -6
- package/dist/pi/role-turn-host.js +43 -13
- package/dist/public-cli/auto-resume.js +2 -5
- package/dist/public-cli/case-dossier-delivery.js +60 -1
- package/dist/public-cli/diarist-run.js +27 -25
- package/dist/public-cli/inspector-run.js +17 -3
- package/dist/public-cli/instruction-seat-run.js +17 -17
- package/dist/public-cli/invocation.js +127 -63
- package/dist/public-cli/main.js +1685 -696
- package/dist/public-cli/notary-run.js +7 -4
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-cli/post-admission.js +113 -71
- package/dist/public-cli/run-lifecycle.js +209 -58
- package/dist/public-cli/seat-ticket-binding.js +3 -6
- package/dist/public-cli/settlement.js +29 -10
- package/dist/public-role-summons.js +8 -0
- package/dist/role-run-placement.js +84 -0
- package/dist/role-run-relocation.js +358 -0
- package/dist/run-terminal-artifacts.js +2 -1
- package/dist/run-ticket-number.js +51 -32
- package/dist/sitian-appender.js +11 -26
- package/dist/submission-ledger.js +106 -25
- package/dist/ticket-provenance.js +3 -1
- package/dist/user-dialogue-stdin.js +33 -0
- package/extensions/role-runtime.ts +1 -0
- package/package.json +3 -2
- package/resources/836-deleted-machine-instruction-inventory.md +1 -1
- package/scripts/build-package.mjs +34 -2
- package/souls/fixer.md +3 -2
- package/src/analyst-ledger.ts +14 -15
- package/src/archivist-record-entry.ts +285 -38
- package/src/archivist-record-topology.ts +43 -18
- package/src/auditor-dossier-tool.ts +2 -1
- package/src/book-topology-migration-placement.ts +493 -0
- package/src/book-topology-migration.ts +220 -0
- package/src/book-topology-migrators-t11.ts +856 -0
- package/src/book-topology-mixed-volume-migrators.ts +470 -0
- package/src/book-topology-partition-migrators.ts +25 -0
- package/src/book-topology-record-class-migrators.ts +779 -0
- package/src/book-topology-runs-migrator.ts +244 -0
- package/src/diarist-contracts.ts +116 -0
- package/src/diarist-role.ts +6 -0
- package/src/diarist.ts +2 -0
- package/src/doctor-auditor.ts +1 -1
- package/src/doctor-evidence.ts +4 -1
- package/src/doctor-role.ts +1 -1
- package/src/dossier-resolution.ts +26 -8
- package/src/external-host-turn-loop.ts +9 -0
- package/src/gatekeeper-pass-envelope.ts +6 -0
- package/src/gatekeeper-role.ts +47 -28
- package/src/headless-host/description.ts +8 -11
- package/src/headless-host/role-turn-host.ts +20 -5
- package/src/host-contracts.ts +22 -3
- package/src/inspector-contracts.ts +7 -0
- package/src/judge-role.ts +4 -0
- package/src/navigator-public-session.ts +33 -30
- package/src/navigator-work-context.ts +3 -4
- package/src/notary-role.ts +1 -0
- package/src/notary-source-run.ts +9 -4
- package/src/packaged-role-registry.ts +1 -1
- package/src/pi/adapter.ts +19 -4
- package/src/pi/durable-principal.ts +9 -13
- package/src/pi/role-turn-host.ts +48 -14
- package/src/public-cli/auto-resume.ts +2 -6
- package/src/public-cli/case-dossier-delivery.ts +86 -1
- package/src/public-cli/countersign-run.ts +182 -32
- package/src/public-cli/diarist-run.ts +34 -26
- package/src/public-cli/inspector-run.ts +26 -2
- package/src/public-cli/instruction-seat-run.ts +24 -20
- package/src/public-cli/invocation.ts +194 -58
- package/src/public-cli/notary-run.ts +13 -4
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-cli/post-admission.ts +147 -78
- package/src/public-cli/run-lifecycle.ts +258 -65
- package/src/public-cli/seat-ticket-binding.ts +3 -6
- package/src/public-cli/settlement.ts +38 -10
- package/src/public-role-summons.ts +31 -7
- package/src/role-envelope.ts +9 -30
- package/src/role-run-placement.ts +128 -0
- package/src/role-run-relocation.ts +491 -0
- package/src/role-runtime-dependencies.ts +1 -0
- package/src/role-runtime.ts +106 -24
- package/src/run-terminal-artifacts.ts +3 -1
- package/src/run-ticket-number.ts +80 -20
- package/src/sitian-appender.ts +25 -28
- package/src/submission-ledger.ts +148 -30
- package/src/ticket-provenance.ts +4 -1
- package/src/ticket-trajectory.ts +22 -35
- package/src/user-dialogue-stdin.ts +37 -0
- package/src/worker-role.ts +8 -4
- package/src/worker-submission-gates.ts +6 -6
|
@@ -8,10 +8,8 @@ import type {
|
|
|
8
8
|
NewDurablePrincipalRequest,
|
|
9
9
|
} from "../host-contracts.ts";
|
|
10
10
|
import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
resolveActivationLedgerHome,
|
|
14
|
-
} from "../activation-ledger-topology.ts";
|
|
11
|
+
import { resolveActivationLedgerHome } from "../activation-ledger-topology.ts";
|
|
12
|
+
import { roleRunPlacement } from "../role-run-placement.ts";
|
|
15
13
|
|
|
16
14
|
type PiDurablePrincipal = DurablePrincipal & {
|
|
17
15
|
readonly sessionDirectory: string;
|
|
@@ -31,18 +29,16 @@ export function issuePiDurablePrincipalCoordinates(
|
|
|
31
29
|
} {
|
|
32
30
|
const ledgerHome = resolveActivationLedgerHome(request.home);
|
|
33
31
|
const bookKey = resolveBookKeyFromGit(request.cwd);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
const placement = roleRunPlacement(ledgerHome, {
|
|
33
|
+
bookKey,
|
|
34
|
+
subject: { unbound: true },
|
|
35
|
+
runId: request.runId,
|
|
36
|
+
role: request.role,
|
|
37
|
+
});
|
|
40
38
|
return {
|
|
41
39
|
ledgerHome,
|
|
42
40
|
bookKey,
|
|
43
|
-
|
|
44
|
-
sessionDirectory,
|
|
45
|
-
sessionFile: join(sessionDirectory, "session.jsonl"),
|
|
41
|
+
...placement,
|
|
46
42
|
};
|
|
47
43
|
}
|
|
48
44
|
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -21,9 +21,10 @@ import type {
|
|
|
21
21
|
RoleTurnRequest,
|
|
22
22
|
RoleTurnResult,
|
|
23
23
|
} from "../host-contracts.ts";
|
|
24
|
-
import { ExplicitInternalActivationError } from "../host-contracts.ts";
|
|
25
|
-
import { applyEngineChildEnv } from "../engine-detour.ts";
|
|
24
|
+
import { ExplicitInternalActivationError, isOfficerReviewSeat } from "../host-contracts.ts";
|
|
25
|
+
import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.ts";
|
|
26
26
|
import { projectActivationFlags } from "../role-activation-flags.ts";
|
|
27
|
+
import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
/** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
|
|
@@ -119,7 +120,7 @@ export function applyPiNativeSkillInvocation(
|
|
|
119
120
|
* Pi last-hop argv after `--no-extensions -e entry` (#819).
|
|
120
121
|
* Activation flag membership comes from middle-layer projectActivationFlags;
|
|
121
122
|
* this function only renders session coords, controlled constants, and pairs.
|
|
122
|
-
*
|
|
123
|
+
* User dialogue is not an argv element (#879): it rides spawn stdin.
|
|
123
124
|
*/
|
|
124
125
|
export function buildPiTurnExtraArgs(
|
|
125
126
|
request: RoleTurnRequest,
|
|
@@ -127,14 +128,6 @@ export function buildPiTurnExtraArgs(
|
|
|
127
128
|
extraPiArgs: readonly string[] = [],
|
|
128
129
|
): string[] {
|
|
129
130
|
const { sessionFile, sessionDirectory } = authority.decode(request.principal);
|
|
130
|
-
const rawPrompt =
|
|
131
|
-
request.continuation.kind === "initial" || request.continuation.kind === "resume"
|
|
132
|
-
? request.continuation.prompt
|
|
133
|
-
: (() => {
|
|
134
|
-
const _exhaustive: never = request.continuation;
|
|
135
|
-
return _exhaustive;
|
|
136
|
-
})();
|
|
137
|
-
const prompt = applyPiNativeSkillInvocation(request.methods, rawPrompt);
|
|
138
131
|
return [
|
|
139
132
|
"--no-skills",
|
|
140
133
|
...buildMethodArgs(request.methods),
|
|
@@ -148,13 +141,31 @@ export function buildPiTurnExtraArgs(
|
|
|
148
141
|
...extraPiArgs,
|
|
149
142
|
// Envelope assembly = projectActivationFlags; pi only renders argv pairs.
|
|
150
143
|
...activationFlagsToPiArgv(projectActivationFlags(request)),
|
|
144
|
+
...piEngineModelArgs(request),
|
|
151
145
|
"--mode",
|
|
152
146
|
"json",
|
|
153
147
|
...buildSeatModelCliArgs(request.model),
|
|
154
|
-
prompt,
|
|
155
148
|
];
|
|
156
149
|
}
|
|
157
150
|
|
|
151
|
+
/** Engine name stays on child env; model has no env fallback (#883 / #879). */
|
|
152
|
+
function piEngineModelArgs(request: RoleTurnRequest): string[] {
|
|
153
|
+
const model = normalizeEngineName(request.engineModel);
|
|
154
|
+
if (model === undefined) return [];
|
|
155
|
+
return [`--${ENGINE_MODEL_FLAG_NAME}`, model];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function piUserDialogueBody(request: RoleTurnRequest): string {
|
|
159
|
+
const rawPrompt =
|
|
160
|
+
request.continuation.kind === "initial" || request.continuation.kind === "resume"
|
|
161
|
+
? request.continuation.prompt
|
|
162
|
+
: (() => {
|
|
163
|
+
const _exhaustive: never = request.continuation;
|
|
164
|
+
return _exhaustive;
|
|
165
|
+
})();
|
|
166
|
+
return applyPiNativeSkillInvocation(request.methods, rawPrompt);
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
export type PiSpawnRunner = (
|
|
159
170
|
args: readonly string[],
|
|
160
171
|
options: {
|
|
@@ -163,6 +174,8 @@ export type PiSpawnRunner = (
|
|
|
163
174
|
timeoutMs?: number;
|
|
164
175
|
/** Parent cancellation; the child gets the same graceful SIGTERM as a budget. */
|
|
165
176
|
signal?: AbortSignal;
|
|
177
|
+
/** User dialogue body; omitted from argv so execve cannot E2BIG (#879). */
|
|
178
|
+
stdin?: string;
|
|
166
179
|
},
|
|
167
180
|
) => Promise<{
|
|
168
181
|
code: number | null;
|
|
@@ -271,11 +284,22 @@ export function createDefaultPiSpawnRunner(options: {
|
|
|
271
284
|
const child = spawn(piIdentity.executable, [...args], {
|
|
272
285
|
cwd: spawnOptions.cwd,
|
|
273
286
|
env: spawnOptions.env,
|
|
274
|
-
stdio: ["
|
|
287
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
275
288
|
});
|
|
289
|
+
if (child.stdin === null) {
|
|
290
|
+
throw new Error("Pi child stdin pipe was not created");
|
|
291
|
+
}
|
|
276
292
|
if (child.stderr === null) {
|
|
277
293
|
throw new Error("Pi child stderr pipe was not created");
|
|
278
294
|
}
|
|
295
|
+
let stdinDeliveryError: Error | undefined;
|
|
296
|
+
child.stdin.on("error", (error) => {
|
|
297
|
+
stdinDeliveryError ??= error;
|
|
298
|
+
});
|
|
299
|
+
if (spawnOptions.stdin !== undefined) {
|
|
300
|
+
child.stdin.write(spawnOptions.stdin);
|
|
301
|
+
}
|
|
302
|
+
child.stdin.end();
|
|
279
303
|
let stderr = "";
|
|
280
304
|
let timedOut = false;
|
|
281
305
|
// No default wall clock. Only an explicit caller budget arms a timer (ADR 0010).
|
|
@@ -344,6 +368,10 @@ export function createDefaultPiSpawnRunner(options: {
|
|
|
344
368
|
reject(executionError);
|
|
345
369
|
return;
|
|
346
370
|
}
|
|
371
|
+
if (stdinDeliveryError !== undefined) {
|
|
372
|
+
reject(stdinDeliveryError);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
347
375
|
resolveResult({
|
|
348
376
|
code,
|
|
349
377
|
stderr,
|
|
@@ -375,9 +403,13 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
375
403
|
async executeTurn(request: RoleTurnRequest): Promise<RoleTurnResult> {
|
|
376
404
|
// #617 DK-7: Pi argv gets projected native paths once; never record bytes.
|
|
377
405
|
// Pi already owns its own session file, so only sitian prior volume rides in.
|
|
406
|
+
// #879: station-child officer dialogue keeps peer words — do not splice
|
|
407
|
+
// host-transition priorNativePaths into the review prompt body.
|
|
378
408
|
let turnRequest = request;
|
|
409
|
+
const officerStationChild =
|
|
410
|
+
request.stationChild === true && isOfficerReviewSeat(request.activation.role);
|
|
379
411
|
const paths =
|
|
380
|
-
request.hostTransition?.priorNativeKind === "sitian"
|
|
412
|
+
!officerStationChild && request.hostTransition?.priorNativeKind === "sitian"
|
|
381
413
|
? request.hostTransition.priorNativePaths
|
|
382
414
|
: undefined;
|
|
383
415
|
if (
|
|
@@ -400,6 +432,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
400
432
|
config.extraPiArgs ?? [],
|
|
401
433
|
);
|
|
402
434
|
const args = buildExplicitInternalActivationArgs(roleEntry, extraArgs);
|
|
435
|
+
const stdin = encodeUserDialogueStdin(piUserDialogueBody(turnRequest));
|
|
403
436
|
// Shared envelope isolates this call's court identity: omitting courtAttemptId
|
|
404
437
|
// must not inherit a parent process.env.AK_ROLE_COURT_ATTEMPT (#637).
|
|
405
438
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -444,6 +477,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
444
477
|
return await spawnRunner(args, {
|
|
445
478
|
cwd: request.cwd,
|
|
446
479
|
env,
|
|
480
|
+
stdin,
|
|
447
481
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
448
482
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
449
483
|
});
|
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { lstat, mkdir, open } from "node:fs/promises";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
|
|
19
20
|
import type {
|
|
20
21
|
DurablePrincipal,
|
|
21
22
|
DurablePrincipalAuthority,
|
|
@@ -143,11 +144,6 @@ export class TurnDispatchedFailure extends Error {
|
|
|
143
144
|
/** Session custom-entry type carrying the pointer to one dispatch error file. */
|
|
144
145
|
export const DISPATCH_ERROR_RETENTION_ENTRY_TYPE = "ak_run_dispatch_error_retention" as const;
|
|
145
146
|
|
|
146
|
-
/** Artifacts subdirectory of a run directory (established run-artifacts location). */
|
|
147
|
-
function runArtifactsDirectory(runDirectory: string): string {
|
|
148
|
-
return join(runDirectory, "artifacts");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
147
|
/**
|
|
152
148
|
* #182-A hardened path identity, mirrored from settlement.ts's
|
|
153
149
|
* ensureAuditEvidenceDirectory: a planted symlink at the run directory or the
|
|
@@ -163,7 +159,7 @@ export async function ensureRealArtifactsDirectory(runDirectory: string): Promis
|
|
|
163
159
|
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
164
160
|
throw new Error("run artifact retention: run directory is not a real directory");
|
|
165
161
|
}
|
|
166
|
-
const artifactsDir =
|
|
162
|
+
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
167
163
|
try {
|
|
168
164
|
const existing = await lstat(artifactsDir);
|
|
169
165
|
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
@@ -5,13 +5,31 @@
|
|
|
5
5
|
* 只读已有案卷:不刷新、不生成、不校验内容、不新增拒收或停工条件。
|
|
6
6
|
* 机器文本仅中立标识材料(ADR 0073),用途说明归角色材料所有。
|
|
7
7
|
*
|
|
8
|
-
* 递送挂载点唯一:`post-admission` 在 beforeDispatch
|
|
8
|
+
* 递送挂载点唯一:`post-admission` 在 beforeDispatch 之后为每个公共入口挂载。
|
|
9
|
+
* 普通入口把本段追加进 continuation;station-child 审核轮次走 attachments
|
|
10
|
+
* 冻结 + role-runtime `loadCaseDossierReadingMaterial` → readingMaterial →
|
|
11
|
+
* systemPrompt.materials fold(#879:对话 instruction 保持父腿 payload 原文;
|
|
12
|
+
* 起居录作独立附件面,不新造 RoleTurnRequest.materials)。
|
|
9
13
|
*/
|
|
14
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
10
18
|
import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
|
|
19
|
+
import {
|
|
20
|
+
freezeAttachmentsIntoRun,
|
|
21
|
+
type FrozenAttachment,
|
|
22
|
+
} from "./invocation.ts";
|
|
11
23
|
|
|
12
24
|
/** Section heading of the system-delivered dossier pointer (presentation only). */
|
|
13
25
|
const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)" as const;
|
|
14
26
|
|
|
27
|
+
/** Stable freeze key under run/attachments/ for station-child 0081 delivery. */
|
|
28
|
+
const CASE_DOSSIER_ATTACH_KEY = "case-dossier" as const;
|
|
29
|
+
|
|
30
|
+
/** Leaf name of the frozen pointer section (content = purpose + paths). */
|
|
31
|
+
const CASE_DOSSIER_ATTACH_FILE = "case-dossier-pointer.md" as const;
|
|
32
|
+
|
|
15
33
|
/** Pointer only — presence/absence is for the role to observe at the path. */
|
|
16
34
|
function describeDossierFile(path: string): string {
|
|
17
35
|
return path;
|
|
@@ -42,3 +60,70 @@ export async function projectCaseDossierPointerSection(input: {
|
|
|
42
60
|
`记录卷宗:${describeDossierFile(volume.recordFile)}`,
|
|
43
61
|
].join("\n");
|
|
44
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ADR 0081 delivery for station-child officer turns (#879): freeze the same
|
|
66
|
+
* pointer section through the existing attachments seam. Role-runtime loads
|
|
67
|
+
* that freeze via loadCaseDossierReadingMaterial onto readingMaterial; the
|
|
68
|
+
* envelope then folds it into systemPrompt.materials. Peer dialogue instruction
|
|
69
|
+
* stays the parent payload; never RoleTurnRequest.materials.
|
|
70
|
+
* Returns frozen attachments, or undefined when unbound (no dossier).
|
|
71
|
+
*/
|
|
72
|
+
export async function deliverCaseDossierAsAttachment(input: {
|
|
73
|
+
readonly ticketNumber: number | undefined;
|
|
74
|
+
readonly projectRoot: string;
|
|
75
|
+
readonly home: string;
|
|
76
|
+
readonly runDirectory: string;
|
|
77
|
+
}): Promise<readonly FrozenAttachment[] | undefined> {
|
|
78
|
+
const section = await projectCaseDossierPointerSection({
|
|
79
|
+
ticketNumber: input.ticketNumber,
|
|
80
|
+
projectRoot: input.projectRoot,
|
|
81
|
+
home: input.home,
|
|
82
|
+
});
|
|
83
|
+
if (section === undefined) return undefined;
|
|
84
|
+
// Stage in OS temp only — never leave a run-local .case-dossier-stage copy.
|
|
85
|
+
const stagingDir = await mkdtemp(join(tmpdir(), "ak-case-dossier-"));
|
|
86
|
+
try {
|
|
87
|
+
const stagingPath = join(stagingDir, CASE_DOSSIER_ATTACH_FILE);
|
|
88
|
+
await writeFile(stagingPath, `${section}\n`, "utf8");
|
|
89
|
+
return await freezeAttachmentsIntoRun(
|
|
90
|
+
[stagingPath],
|
|
91
|
+
input.runDirectory,
|
|
92
|
+
CASE_DOSSIER_ATTACH_KEY,
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Typed reading-material face for a frozen station-child 0081 attachment. */
|
|
100
|
+
export type CaseDossierReadingMaterial = {
|
|
101
|
+
readonly kind: "case-dossier-pointer";
|
|
102
|
+
readonly frozenPath: string;
|
|
103
|
+
readonly section: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load a previously frozen case-dossier attachment as reading material
|
|
108
|
+
* (existing agent-start / systemPrompt.materials fold — not dialogue prompt,
|
|
109
|
+
* not RoleTurnRequest.materials). Undefined when the run has no such freeze.
|
|
110
|
+
*/
|
|
111
|
+
export async function loadCaseDossierReadingMaterial(
|
|
112
|
+
runDirectory: string,
|
|
113
|
+
): Promise<CaseDossierReadingMaterial | undefined> {
|
|
114
|
+
const frozenPath = join(
|
|
115
|
+
runDirectory,
|
|
116
|
+
"attachments",
|
|
117
|
+
CASE_DOSSIER_ATTACH_KEY,
|
|
118
|
+
`00-${CASE_DOSSIER_ATTACH_FILE}`,
|
|
119
|
+
);
|
|
120
|
+
let section: string;
|
|
121
|
+
try {
|
|
122
|
+
section = await readFile(frozenPath, "utf8");
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
if (section.trim() === "") return undefined;
|
|
128
|
+
return { kind: "case-dossier-pointer", frozenPath, section };
|
|
129
|
+
}
|
|
@@ -25,10 +25,16 @@
|
|
|
25
25
|
import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
|
|
26
26
|
import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts";
|
|
27
27
|
import { CliUsageError } from "./cli-errors.ts";
|
|
28
|
+
import {
|
|
29
|
+
projectCourtTicketNumbers,
|
|
30
|
+
} from "../diarist-contracts.ts";
|
|
31
|
+
import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
|
|
28
32
|
import {
|
|
29
33
|
admitCountersignInvocation,
|
|
30
34
|
bindAdmittedTicketNumber,
|
|
35
|
+
bindCourtTicketNumbersOnAdmitted,
|
|
31
36
|
buildCountersignTransportPrompt,
|
|
37
|
+
relocateAdmittedRunToTicket,
|
|
32
38
|
type AdmittedCountersignInvocation,
|
|
33
39
|
type ParseCountersignArgvResult,
|
|
34
40
|
} from "./invocation.ts";
|
|
@@ -46,6 +52,7 @@ import {
|
|
|
46
52
|
markRunAdmitted,
|
|
47
53
|
markRunTerminal,
|
|
48
54
|
type PublicResumeRequest,
|
|
55
|
+
type RunWriterLease,
|
|
49
56
|
type SameTicketSummonsMaterials,
|
|
50
57
|
} from "./run-lifecycle.ts";
|
|
51
58
|
import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
|
|
@@ -70,6 +77,12 @@ export type CountersignRunEnv = PostAdmissionEnv & {
|
|
|
70
77
|
runCourtDiaristStation?: (
|
|
71
78
|
admitted: AdmittedCountersignInvocation,
|
|
72
79
|
) => Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* #871: typed co-review set from the identity 起居郎 turn, applied onto the
|
|
82
|
+
* resumed run before bound refresh (same-ticket re-summons path). Whole-set
|
|
83
|
+
* replace — never union. Production leaves unset outside that handoff.
|
|
84
|
+
*/
|
|
85
|
+
pendingCourtTicketNumbers?: readonly number[];
|
|
73
86
|
};
|
|
74
87
|
|
|
75
88
|
/** Project admitted invocation onto the host-neutral turn request. */
|
|
@@ -94,7 +107,16 @@ export function buildCountersignTurnRequest(
|
|
|
94
107
|
|
|
95
108
|
/** 起居郎 identity outcome — escalate stays distinct from missing terminal. */
|
|
96
109
|
type CourtDiaristIdentity =
|
|
97
|
-
| {
|
|
110
|
+
| {
|
|
111
|
+
readonly kind: "ticket";
|
|
112
|
+
readonly ticketNumber: number;
|
|
113
|
+
/**
|
|
114
|
+
* #871 typed co-review set when the LLM explicitly submitted one.
|
|
115
|
+
* Absent means "no new set this turn" — resume must keep the stored run fact
|
|
116
|
+
* (never silently degrade a multi-ticket set to [main]).
|
|
117
|
+
*/
|
|
118
|
+
readonly courtTicketNumbers?: readonly number[];
|
|
119
|
+
}
|
|
98
120
|
| { readonly kind: "unbound" }
|
|
99
121
|
| { readonly kind: "escalate" };
|
|
100
122
|
|
|
@@ -103,6 +125,53 @@ type CourtDiaristInvocationResult = {
|
|
|
103
125
|
readonly failedWithoutEscalate?: { readonly diagnostic: string };
|
|
104
126
|
};
|
|
105
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Read #871 set from preserved diarist payloads.
|
|
130
|
+
* - Field absent / null / non-array → undefined (no new set; resume keeps store).
|
|
131
|
+
* - Explicit [] → single-ticket [main] whole-set replace (signed empty-set contract).
|
|
132
|
+
* - Non-empty array with zero lawful typed members after projection → not a new set
|
|
133
|
+
* (do not impersonate explicit empty and wipe a stored multi-ticket fact).
|
|
134
|
+
* - Non-empty array with ≥1 lawful member → sole type/dedupe projection + principal guarantee.
|
|
135
|
+
* - Multiple submissions: last qualifying set wins.
|
|
136
|
+
* Live path never shape-rejects the role turn; durable damage is a separate seam.
|
|
137
|
+
*/
|
|
138
|
+
function courtTicketNumbersFromOutcome(
|
|
139
|
+
roleOutcome: TerminalRoleOutcome | undefined,
|
|
140
|
+
principalTicket: number,
|
|
141
|
+
): readonly number[] | undefined {
|
|
142
|
+
if (roleOutcome === undefined) return undefined;
|
|
143
|
+
const payloads =
|
|
144
|
+
roleOutcome.kind === "accepted" ||
|
|
145
|
+
roleOutcome.kind === "audit_escalation" ||
|
|
146
|
+
roleOutcome.kind === "failure"
|
|
147
|
+
? roleOutcome.payloads ?? []
|
|
148
|
+
: [];
|
|
149
|
+
let latest: readonly number[] | undefined;
|
|
150
|
+
for (const payload of payloads) {
|
|
151
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const record = payload as Record<string, unknown>;
|
|
155
|
+
if (!Object.hasOwn(record, "courtTicketNumbers")) continue;
|
|
156
|
+
const raw = record.courtTicketNumbers;
|
|
157
|
+
// Only a real array is a candidate set. Non-array is not explicit empty.
|
|
158
|
+
if (!Array.isArray(raw)) continue;
|
|
159
|
+
// Members only — do not inject principal yet, or all-invalid non-empty becomes [main].
|
|
160
|
+
const members = projectCourtTicketNumbers(raw);
|
|
161
|
+
if (members === null) continue;
|
|
162
|
+
if (members.length === 0) {
|
|
163
|
+
// Literal [] is the intentional principal-only replace; non-empty garbage is not.
|
|
164
|
+
if (raw.length === 0) {
|
|
165
|
+
latest = projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
latest =
|
|
170
|
+
projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
|
|
171
|
+
}
|
|
172
|
+
return latest;
|
|
173
|
+
}
|
|
174
|
+
|
|
106
175
|
/** Routing boolean over the child's own typed sequence — does not pick or rewrite a sole row. */
|
|
107
176
|
function courtDiaristEscalated(roleOutcome: TerminalRoleOutcome | undefined): boolean {
|
|
108
177
|
if (roleOutcome === undefined) return false;
|
|
@@ -181,13 +250,14 @@ async function invokeCourtDiarist(input: {
|
|
|
181
250
|
}
|
|
182
251
|
|
|
183
252
|
const asserted = (result.admitted as { ticketNumber?: number } | undefined)?.ticketNumber;
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
Number.isSafeInteger(asserted) &&
|
|
187
|
-
asserted >= 1
|
|
188
|
-
) {
|
|
253
|
+
if (isSafePositiveTicketNumber(asserted)) {
|
|
254
|
+
const courtTicketNumbers = courtTicketNumbersFromOutcome(roleOutcome, asserted);
|
|
189
255
|
return {
|
|
190
|
-
identity: {
|
|
256
|
+
identity: {
|
|
257
|
+
kind: "ticket",
|
|
258
|
+
ticketNumber: asserted,
|
|
259
|
+
...(courtTicketNumbers === undefined ? {} : { courtTicketNumbers }),
|
|
260
|
+
},
|
|
191
261
|
};
|
|
192
262
|
}
|
|
193
263
|
return {
|
|
@@ -219,25 +289,45 @@ export async function runCountersignCourtDiaristStation(
|
|
|
219
289
|
// First-entry unbound identity is owned by runPublicCountersign.
|
|
220
290
|
if (admitted.ticketNumber === undefined) return;
|
|
221
291
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
// #871: refresh every member of the typed co-review set; single-ticket face
|
|
293
|
+
// is just the set [main]. Present-but-empty / missing-principal is damage —
|
|
294
|
+
// never silently fall back to main-only (legacy absent field still may).
|
|
295
|
+
let refreshTickets: readonly number[];
|
|
296
|
+
if (admitted.courtTicketNumbers !== undefined) {
|
|
297
|
+
if (
|
|
298
|
+
admitted.courtTicketNumbers.length === 0 ||
|
|
299
|
+
!admitted.courtTicketNumbers.includes(admitted.ticketNumber)
|
|
300
|
+
) {
|
|
301
|
+
throw new StationChildExhaustedError(
|
|
302
|
+
`court diarist station: courtTicketNumbers is damaged (empty or missing principal #${admitted.ticketNumber})`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
refreshTickets = admitted.courtTicketNumbers;
|
|
306
|
+
} else {
|
|
307
|
+
refreshTickets = [admitted.ticketNumber];
|
|
308
|
+
}
|
|
233
309
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
310
|
+
for (const ticketNumber of refreshTickets) {
|
|
311
|
+
const outcome = await invokeCourtDiarist(
|
|
312
|
+
{
|
|
313
|
+
instruction: `整理 #${ticketNumber} 的本案依据。`,
|
|
314
|
+
projectRoot: admitted.projectRoot,
|
|
315
|
+
failureLabel: `ticket #${ticketNumber}`,
|
|
316
|
+
// Refresh holds a typed key — hand it off so identity is bound before turn.
|
|
317
|
+
boundTicketNumber: ticketNumber,
|
|
318
|
+
},
|
|
319
|
+
env,
|
|
320
|
+
io,
|
|
237
321
|
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
322
|
+
|
|
323
|
+
if (outcome.identity.kind === "escalate") {
|
|
324
|
+
throw new StationChildExhaustedError(
|
|
325
|
+
"court diarist station escalated (cannot identify court target)",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (outcome.failedWithoutEscalate !== undefined) {
|
|
329
|
+
throw new StationChildExhaustedError(outcome.failedWithoutEscalate.diagnostic);
|
|
330
|
+
}
|
|
241
331
|
}
|
|
242
332
|
}
|
|
243
333
|
|
|
@@ -293,6 +383,7 @@ export async function runPublicCountersign(
|
|
|
293
383
|
// hook failures stay on the parent call-local budget, exhausted nested
|
|
294
384
|
// station children still skip parent auto-resume (#840 父子不层叠).
|
|
295
385
|
let typedTicket: number | undefined;
|
|
386
|
+
let typedCourtTicketNumbers: readonly number[] | undefined;
|
|
296
387
|
let identityDiaristRan = false;
|
|
297
388
|
|
|
298
389
|
if (env.runCourtDiaristStation === undefined) {
|
|
@@ -347,7 +438,10 @@ export async function runPublicCountersign(
|
|
|
347
438
|
// unbound and continue (true-unbound face). Escalate / typed failure above;
|
|
348
439
|
// bound refresh still fails honest via runCountersignCourtDiaristStation.
|
|
349
440
|
if (outcome.identity.kind === "ticket") {
|
|
350
|
-
|
|
441
|
+
const assertedTicket = outcome.identity.ticketNumber;
|
|
442
|
+
typedTicket = assertedTicket;
|
|
443
|
+
// #871: identity may hand a co-review set; absent field defaults to [main].
|
|
444
|
+
typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
|
|
351
445
|
const summons: SameTicketSummonsMaterials = {
|
|
352
446
|
instruction: parsed.instruction,
|
|
353
447
|
instructionEmpty: parsed.instruction.trim() === "",
|
|
@@ -361,16 +455,26 @@ export async function runPublicCountersign(
|
|
|
361
455
|
freshSummons: env.freshSummons,
|
|
362
456
|
summons,
|
|
363
457
|
resume: async (runId, materials) => {
|
|
364
|
-
// Resume selected:
|
|
458
|
+
// Resume selected: file the provisional run under the asserted ticket
|
|
459
|
+
// before abandoning it, so typed identity never leaves an unbound row.
|
|
460
|
+
await bindAdmittedTicketNumber(admitted, assertedTicket);
|
|
461
|
+
await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
|
|
365
462
|
await markRunTerminal(admitted.runDirectory);
|
|
366
463
|
// Identity 起居郎 asserted unbound (no issue face). Resume still runs
|
|
367
464
|
// the bound refresh station under the typed key (refresh-every-court).
|
|
465
|
+
// #871: hand the identity set so resume can whole-replace the run fact
|
|
466
|
+
// when this summons produced a new typed set (never union).
|
|
368
467
|
return await runPublicCountersignResume(
|
|
369
468
|
{
|
|
370
469
|
runId,
|
|
371
470
|
...(materials === undefined ? {} : { summons: materials }),
|
|
372
471
|
},
|
|
373
|
-
|
|
472
|
+
{
|
|
473
|
+
...env,
|
|
474
|
+
...(typedCourtTicketNumbers === undefined
|
|
475
|
+
? {}
|
|
476
|
+
: { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
|
|
477
|
+
},
|
|
374
478
|
io,
|
|
375
479
|
);
|
|
376
480
|
},
|
|
@@ -381,6 +485,33 @@ export async function runPublicCountersign(
|
|
|
381
485
|
}
|
|
382
486
|
}
|
|
383
487
|
|
|
488
|
+
if (identityDiaristRan && typedTicket !== undefined) {
|
|
489
|
+
try {
|
|
490
|
+
await bindAdmittedTicketNumber(admitted, typedTicket);
|
|
491
|
+
await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
|
|
492
|
+
// First court: explicit set wins; omitted field → single-ticket face [main].
|
|
493
|
+
// Set persistence is outside beforeDispatch — route write failures into the
|
|
494
|
+
// same controlled-failure settlement as station children (#871 B7).
|
|
495
|
+
await bindCourtTicketNumbersOnAdmitted(
|
|
496
|
+
admitted,
|
|
497
|
+
typedCourtTicketNumbers ?? [typedTicket],
|
|
498
|
+
);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return await presentControlledFailure(
|
|
501
|
+
admitted,
|
|
502
|
+
{
|
|
503
|
+
timedOut: false,
|
|
504
|
+
code: null,
|
|
505
|
+
stderr: "",
|
|
506
|
+
thrown: error,
|
|
507
|
+
},
|
|
508
|
+
countersignAdapters(),
|
|
509
|
+
env.principalAuthority,
|
|
510
|
+
io,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
384
515
|
const turnProjection: RoleTurnRequestProjectionOptions = {
|
|
385
516
|
packageRoot: env.packageRoot,
|
|
386
517
|
home: env.home,
|
|
@@ -405,23 +536,25 @@ export async function runPublicCountersign(
|
|
|
405
536
|
// Mutable shell: ticket bind re-projects activation before executeTurn.
|
|
406
537
|
const turnRequest = buildCountersignTurnRequest(admitted, turnProjection);
|
|
407
538
|
|
|
408
|
-
|
|
539
|
+
const result = await runPostAdmissionOneShot({
|
|
409
540
|
admitted,
|
|
410
541
|
env,
|
|
411
542
|
io,
|
|
412
543
|
request: turnRequest,
|
|
413
544
|
adapters: countersignAdapters({
|
|
414
|
-
beforeDispatch: async (admittedSeat) => {
|
|
545
|
+
beforeDispatch: async (admittedSeat, lease) => {
|
|
415
546
|
// Dossier pointer delivery rides post-admission after this hook (#709).
|
|
416
547
|
if (!identityDiaristRan) {
|
|
417
548
|
// Test seam (or any deferred identity): station owns assert + bind.
|
|
418
549
|
await runCountersignCourtDiaristStation(admittedSeat, env, io);
|
|
419
550
|
} else if (typedTicket !== undefined) {
|
|
420
|
-
// Production identity asserted unbound; bind typed key, then bound
|
|
421
|
-
// refresh so freeze loads issue face (typed handoff, not prose match).
|
|
422
|
-
await bindAdmittedTicketNumber(admittedSeat, typedTicket);
|
|
423
551
|
await runCountersignCourtDiaristStation(admittedSeat, env, io);
|
|
424
552
|
}
|
|
553
|
+
await relocateAdmittedRunToTicket(
|
|
554
|
+
admittedSeat,
|
|
555
|
+
env.principalAuthority,
|
|
556
|
+
lease,
|
|
557
|
+
);
|
|
425
558
|
Object.assign(
|
|
426
559
|
turnRequest,
|
|
427
560
|
buildCountersignTurnRequest(admittedSeat, turnProjection),
|
|
@@ -430,11 +563,14 @@ export async function runPublicCountersign(
|
|
|
430
563
|
}),
|
|
431
564
|
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
432
565
|
});
|
|
566
|
+
await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
|
|
567
|
+
return result;
|
|
433
568
|
}
|
|
434
569
|
|
|
435
570
|
function countersignAdapters(options?: {
|
|
436
571
|
beforeDispatch?: (
|
|
437
572
|
admitted: AdmittedCountersignInvocation,
|
|
573
|
+
lease: RunWriterLease,
|
|
438
574
|
) => void | Promise<void>;
|
|
439
575
|
}) {
|
|
440
576
|
return {
|
|
@@ -496,6 +632,20 @@ export async function runPublicCountersignResume(
|
|
|
496
632
|
},
|
|
497
633
|
adapters: countersignAdapters({
|
|
498
634
|
beforeDispatch: async (admitted) => {
|
|
635
|
+
// #871 B7: durable set damage already identified at load — settle as
|
|
636
|
+
// station-child exhausted → presentControlledFailure (not structural exit 2).
|
|
637
|
+
if (admitted.courtTicketNumbersDamage !== undefined) {
|
|
638
|
+
throw new StationChildExhaustedError(admitted.courtTicketNumbersDamage);
|
|
639
|
+
}
|
|
640
|
+
// #871: same-ticket re-summons may hand a fresh typed set from identity.
|
|
641
|
+
// Whole-set replace onto this run fact; no new set → keep stored set.
|
|
642
|
+
// Manual resume (no pending) keeps the durable set and never invents one.
|
|
643
|
+
if (env.pendingCourtTicketNumbers !== undefined) {
|
|
644
|
+
await bindCourtTicketNumbersOnAdmitted(
|
|
645
|
+
admitted,
|
|
646
|
+
env.pendingCourtTicketNumbers,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
499
649
|
await runCountersignCourtDiaristStation(admitted, env, io);
|
|
500
650
|
},
|
|
501
651
|
}),
|