@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +6312 -6376
- package/dist/analyst-gate-cycles-read.js +28 -78
- package/dist/analyst-ledger.js +26 -42
- package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
- package/dist/analyst-metric-families/round-timeline.js +2 -5
- package/dist/archivist-record-entry.js +4 -3
- package/dist/archivist-record-pointer.js +71 -36
- package/dist/atomic-write.js +2 -2
- package/dist/auditor-dossier-tool.js +11 -6
- package/dist/auditor-soul.js +5 -12
- package/dist/collector-config.js +6 -5
- package/dist/doctor-evidence.js +8 -4
- package/dist/engine-detour-usage.js +8 -22
- package/dist/headless-host/production-host.js +5969 -6011
- package/dist/host-descriptions.js +0 -4
- package/dist/host-session-record.js +266 -267
- package/dist/ledger-session-read.js +5 -0
- package/dist/migrate-book-topology.js +1369 -722
- package/dist/navigator-public-session.js +13 -17
- package/dist/notary-source-run.js +8 -16
- package/dist/packaged-role-registry.js +5 -56
- package/dist/pi/durable-principal.js +2 -3
- package/dist/pi/role-turn-host.js +79 -15
- package/dist/public-cli/auto-resume.js +8 -47
- package/dist/public-cli/instruction-seat-run.js +9 -7
- package/dist/public-cli/invocation.js +67 -100
- package/dist/public-cli/main.js +2689 -2532
- package/dist/public-cli/post-admission.js +96 -55
- package/dist/public-cli/run-lifecycle.js +44 -100
- package/dist/public-cli/settlement.js +184 -429
- package/dist/public-cli/terminal.js +0 -12
- package/dist/role-run-placement.js +53 -16
- package/dist/role-run-relocation.js +111 -52
- package/dist/run-dossier-files.js +30 -0
- package/dist/run-dossier.js +322 -0
- package/dist/run-terminal-artifacts.js +69 -202
- package/dist/run-ticket-number.js +49 -7
- package/dist/session-assistant-usage.js +9 -18
- package/dist/session-identity.js +49 -19
- package/dist/sitian-appender.js +29 -18
- package/dist/sitian-facade.js +11 -0
- package/dist/sitian-reader.js +44 -29
- package/dist/submission-gate.js +1 -1
- package/dist/submission-ledger.js +4 -4
- package/dist/ticket-provenance-contracts.js +2 -8
- package/dist/ticket-provenance.js +25 -9
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/acp-host/production-host.ts +1 -1
- package/src/acp-host/role-turn-host.ts +11 -4
- package/src/analyst-gate-cycles-read.ts +31 -88
- package/src/analyst-ledger.ts +36 -51
- package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
- package/src/analyst-metric-families/gate-cycles.ts +1 -1
- package/src/analyst-metric-families/round-timeline.ts +2 -5
- package/src/analyst-page.ts +3 -3
- package/src/archivist-record-entry.ts +6 -5
- package/src/archivist-record-pointer.ts +92 -42
- package/src/atomic-write.ts +3 -1
- package/src/auditor-dossier-tool.ts +14 -8
- package/src/auditor-soul.ts +5 -10
- package/src/book-topology-migration-placement.ts +16 -3
- package/src/book-topology-record-class-migrators.ts +4 -2
- package/src/book-topology-runs-migrator.ts +6 -3
- package/src/collector-config.ts +5 -3
- package/src/doctor-contracts.ts +1 -1
- package/src/doctor-evidence.ts +7 -3
- package/src/engine-detour-tool.ts +1 -1
- package/src/engine-detour-usage.ts +12 -25
- package/src/external-host-turn-loop.ts +25 -0
- package/src/factory-board.ts +16 -81
- package/src/headless-host/production-host.ts +1 -4
- package/src/headless-host/role-turn-host.ts +56 -21
- package/src/host-descriptions.ts +0 -6
- package/src/host-session-record.ts +224 -140
- package/src/ledger-session-read.ts +6 -0
- package/src/navigator-public-session.ts +13 -18
- package/src/navigator-work-context.ts +3 -1
- package/src/notary-role.ts +2 -1
- package/src/notary-source-run.ts +8 -18
- package/src/packaged-role-registry.ts +1 -65
- package/src/pi/adapter.ts +50 -7
- package/src/pi/durable-principal.ts +2 -4
- package/src/pi/role-turn-host.ts +85 -14
- package/src/public-cli/auto-resume.ts +7 -43
- package/src/public-cli/instruction-seat-run.ts +13 -6
- package/src/public-cli/invocation.ts +90 -144
- package/src/public-cli/post-admission.ts +111 -72
- package/src/public-cli/run-lifecycle.ts +47 -115
- package/src/public-cli/settlement.ts +221 -589
- package/src/public-cli/terminal.ts +1 -61
- package/src/role-envelope.ts +4 -2
- package/src/role-run-placement.ts +65 -17
- package/src/role-run-relocation.ts +110 -70
- package/src/role-runtime.ts +46 -69
- package/src/run-dossier-files.ts +33 -0
- package/src/run-dossier.ts +357 -0
- package/src/run-terminal-artifacts.ts +84 -240
- package/src/run-ticket-number.ts +82 -14
- package/src/session-assistant-usage.ts +8 -20
- package/src/session-identity.ts +52 -21
- package/src/sitian-appender.ts +33 -26
- package/src/sitian-facade.ts +22 -0
- package/src/sitian-reader.ts +62 -32
- package/src/submission-gate.ts +1 -1
- package/src/submission-ledger.ts +4 -4
- package/src/ticket-provenance-contracts.ts +3 -6
- package/src/ticket-provenance.ts +23 -11
- package/src/ticket-snapshot.ts +3 -2
- package/src/ticket-trajectory.ts +95 -109
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { basename, join } from "node:path";
|
|
4
2
|
import { isRecord } from "./unknown-value.ts";
|
|
3
|
+
import { latestOfficerPointersFromRecords, RUN_HISTORY_FILE } from "./run-dossier.ts";
|
|
4
|
+
import { OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.ts";
|
|
5
|
+
import { readSitianRecords, reportRunRecord } from "./sitian-facade.ts";
|
|
6
|
+
|
|
7
|
+
import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
|
|
8
|
+
import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
|
|
9
|
+
import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
|
|
10
|
+
import { parseRunLeaf, runDirectoryOfSessionFile } from "./role-run-placement.ts";
|
|
5
11
|
|
|
6
12
|
/** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
|
|
7
13
|
export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
|
|
@@ -21,14 +27,17 @@ export type DirectOfficerRunPointer = {
|
|
|
21
27
|
readonly submissionToolCallId?: string;
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
/** History record kind of a booked officer pointer (appender routes it to history.jsonl). */
|
|
31
|
+
export { OFFICER_POINTER_RECORD_KIND };
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
|
-
* Book a typed pointer
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* Book a typed pointer as one history record of the parent run; the settlement
|
|
35
|
+
* seam projects the latest per officer into current.json `officers`.
|
|
36
|
+
* Never fabricates user/assistant/toolResult rows (#675).
|
|
28
37
|
*
|
|
29
|
-
*
|
|
30
|
-
* same-parent re-summons
|
|
31
|
-
*
|
|
38
|
+
* Latest record wins per officer under one parent (#753 gate-round accounting):
|
|
39
|
+
* a same-parent re-summons supersedes the earlier pointer instead of adding a
|
|
40
|
+
* second one that re-scans the full officer session and multiplies gate-cycle counts.
|
|
32
41
|
*/
|
|
33
42
|
export function bookDirectOfficerRunPointer(options: {
|
|
34
43
|
readonly parentSessionFile: string;
|
|
@@ -37,8 +46,6 @@ export function bookDirectOfficerRunPointer(options: {
|
|
|
37
46
|
readonly runDirectory?: string;
|
|
38
47
|
readonly submissionToolCallId?: string;
|
|
39
48
|
}): DirectOfficerRunPointer {
|
|
40
|
-
const nest = join(dirname(options.parentSessionFile), "auditor-roles");
|
|
41
|
-
mkdirSync(nest, { recursive: true });
|
|
42
49
|
const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
|
|
43
50
|
const pointer: DirectOfficerRunPointer = {
|
|
44
51
|
version: 1,
|
|
@@ -50,47 +57,90 @@ export function bookDirectOfficerRunPointer(options: {
|
|
|
50
57
|
: {}),
|
|
51
58
|
...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
|
|
52
59
|
};
|
|
53
|
-
|
|
54
|
-
join(nest, `${options.officer}.pointer.json`),
|
|
55
|
-
`${JSON.stringify(pointer)}\n`,
|
|
56
|
-
"utf8",
|
|
57
|
-
);
|
|
60
|
+
reportRunRecord(runDirectoryOfSessionFile(options.parentSessionFile), OFFICER_POINTER_RECORD_KIND, pointer, "submission-gate");
|
|
58
61
|
return pointer;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
|
|
65
|
+
export async function resolveOfficerPointer(
|
|
66
|
+
raw: unknown,
|
|
67
|
+
parentRunDirectory: string,
|
|
68
|
+
): Promise<{
|
|
69
|
+
sessionFile: string;
|
|
70
|
+
officer?: DirectOfficerRunPointer["officer"];
|
|
71
|
+
runDirectory?: string;
|
|
72
|
+
submissionToolCallId?: string;
|
|
73
|
+
}> {
|
|
74
|
+
const where = `${parentRunDirectory} officers`;
|
|
75
|
+
if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
|
|
76
|
+
throw new Error(`direct officer run pointer has unknown shape in ${where}`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
|
|
79
|
+
throw new Error(`direct officer run pointer missing sessionFile in ${where}`);
|
|
80
|
+
}
|
|
81
|
+
const officer = raw.officer === "inspector" || raw.officer === "notary"
|
|
82
|
+
|| raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
|
|
83
|
+
let runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
|
|
84
|
+
? raw.runDirectory : undefined;
|
|
85
|
+
let sessionFile = raw.sessionFile;
|
|
86
|
+
const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
|
|
87
|
+
const leaf = parseRunLeaf(basename(recordedRunDirectory));
|
|
88
|
+
const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
|
|
89
|
+
if (leaf !== undefined && parentIdentity !== undefined) {
|
|
90
|
+
const currentRunDirectory = await findRunDirectoryById(
|
|
91
|
+
homeFromRunDirectory(parentRunDirectory), leaf.runId, parentIdentity.bookKey, leaf.role,
|
|
92
|
+
);
|
|
93
|
+
if (currentRunDirectory !== undefined) {
|
|
94
|
+
sessionFile = rewriteRunDirectoryPathValue(sessionFile, recordedRunDirectory, currentRunDirectory) as string;
|
|
95
|
+
if (runDirectory !== undefined) runDirectory = currentRunDirectory;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const submissionToolCallId = typeof raw.submissionToolCallId === "string"
|
|
99
|
+
&& raw.submissionToolCallId.trim() !== "" ? raw.submissionToolCallId : undefined;
|
|
100
|
+
return {
|
|
101
|
+
sessionFile,
|
|
102
|
+
...(officer === undefined ? {} : { officer }),
|
|
103
|
+
...(runDirectory === undefined ? {} : { runDirectory }),
|
|
104
|
+
...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
61
108
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
109
|
+
* The latest pointer booked per officer on one run, as stored. History absent → empty.
|
|
110
|
+
* Canonical malformed diagnostics are not discarded: a damaged officer source must
|
|
111
|
+
* not wash into lawful zero rounds (#1161 O1).
|
|
64
112
|
*/
|
|
113
|
+
export async function readBookedOfficerPointers(parentRunDirectory: string): Promise<Readonly<Record<string, unknown>>> {
|
|
114
|
+
const { records, diagnostics } = await readSitianRecords(join(parentRunDirectory, RUN_HISTORY_FILE));
|
|
115
|
+
if (diagnostics.length > 0) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`${RUN_HISTORY_FILE} has ${diagnostics.length} malformed row(s); officer pointers unreadable: ${parentRunDirectory}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return latestOfficerPointersFromRecords(records);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Every pointer booked on one run, in officer-name order. No pointers → []. */
|
|
124
|
+
export async function readOfficerPointers(parentRunDirectory: string): Promise<readonly Awaited<ReturnType<typeof resolveOfficerPointer>>[]> {
|
|
125
|
+
const officers = await readBookedOfficerPointers(parentRunDirectory);
|
|
126
|
+
return Promise.all(
|
|
127
|
+
Object.keys(officers).sort().map((name) => resolveOfficerPointer(officers[name], parentRunDirectory)),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
|
|
65
132
|
export async function readDirectOfficerRunPointer(
|
|
66
133
|
parentSessionFile: string,
|
|
67
134
|
officer: DirectOfficerRunPointer["officer"],
|
|
68
135
|
): Promise<DirectOfficerRunPointer | undefined> {
|
|
69
136
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (raw.officer !== officer) return undefined;
|
|
78
|
-
if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") return undefined;
|
|
79
|
-
const runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
|
|
80
|
-
? raw.runDirectory
|
|
81
|
-
: undefined;
|
|
82
|
-
const submissionToolCallId = typeof raw.submissionToolCallId === "string"
|
|
83
|
-
&& raw.submissionToolCallId.trim() !== ""
|
|
84
|
-
? raw.submissionToolCallId
|
|
85
|
-
: undefined;
|
|
86
|
-
return {
|
|
87
|
-
version: 1,
|
|
88
|
-
kind: DIRECT_OFFICER_RUN_POINTER_KIND,
|
|
89
|
-
officer,
|
|
90
|
-
sessionFile: raw.sessionFile,
|
|
91
|
-
...(runDirectory === undefined ? {} : { runDirectory }),
|
|
92
|
-
...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
|
|
93
|
-
};
|
|
137
|
+
const parentRunDirectory = runDirectoryOfSessionFile(parentSessionFile);
|
|
138
|
+
const pointer = await resolveOfficerPointer(
|
|
139
|
+
(await readBookedOfficerPointers(parentRunDirectory))[officer],
|
|
140
|
+
parentRunDirectory,
|
|
141
|
+
);
|
|
142
|
+
if (pointer.officer !== officer) return undefined;
|
|
143
|
+
return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
|
|
94
144
|
} catch {
|
|
95
145
|
return undefined;
|
|
96
146
|
}
|
package/src/atomic-write.ts
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
* ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
+
import type { Mode } from "node:fs";
|
|
10
11
|
import { rename, rm, writeFile } from "node:fs/promises";
|
|
11
12
|
import { dirname, join } from "node:path";
|
|
12
13
|
|
|
13
14
|
export async function writeFileAtomically(
|
|
14
15
|
destination: string,
|
|
15
16
|
contents: string | Uint8Array,
|
|
17
|
+
options?: { readonly mode?: Mode },
|
|
16
18
|
): Promise<void> {
|
|
17
19
|
const parent = dirname(destination);
|
|
18
20
|
const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
|
|
19
21
|
try {
|
|
20
|
-
await writeFile(temporary, contents);
|
|
22
|
+
await writeFile(temporary, contents, options);
|
|
21
23
|
await rename(temporary, destination);
|
|
22
24
|
} catch (error) {
|
|
23
25
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
2
|
|
|
3
|
+
import { RUN_HISTORY_FILE, RUN_LOG_FILE, runCurrentPath } from "./run-dossier.ts";
|
|
3
4
|
import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import type { HostContext } from "./host-contracts.ts";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
runDirectoryOfSessionFile,
|
|
8
|
+
sessionFileOf,
|
|
9
|
+
} from "./role-run-placement.ts";
|
|
6
10
|
import { Type } from "typebox";
|
|
7
11
|
|
|
8
12
|
export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
|
|
9
13
|
|
|
10
14
|
export type AuditorDossierLocation = {
|
|
11
15
|
readonly runDirectory: string;
|
|
12
|
-
readonly
|
|
16
|
+
readonly current: string;
|
|
13
17
|
readonly parentSessionCandidate: string;
|
|
14
18
|
readonly attachments: string;
|
|
15
|
-
readonly
|
|
19
|
+
readonly log: string;
|
|
20
|
+
readonly history: string;
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
/** Resolve the exact run binding already carried by the parent record session. */
|
|
@@ -20,7 +25,7 @@ export function auditorRunDirectory(context: ExtensionContext | HostContext): st
|
|
|
20
25
|
const sessionFile = context.sessionManager?.getSessionFile?.();
|
|
21
26
|
if (sessionFile === undefined) return undefined;
|
|
22
27
|
// Sole layout: <run>/session/session.jsonl → climb two levels to the run directory.
|
|
23
|
-
return resolve(
|
|
28
|
+
return resolve(runDirectoryOfSessionFile(sessionFile));
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/** The one shared, run-bound dossier locator exposed to every auditor seat. */
|
|
@@ -43,10 +48,11 @@ export function createAuditorDossierTool(
|
|
|
43
48
|
// #836: whole run directory pointer — officer finds materials; no leaf preference (A7.2).
|
|
44
49
|
const details: AuditorDossierLocation = {
|
|
45
50
|
runDirectory,
|
|
46
|
-
|
|
47
|
-
parentSessionCandidate:
|
|
51
|
+
current: runCurrentPath(runDirectory),
|
|
52
|
+
parentSessionCandidate: sessionFileOf(runDirectory),
|
|
48
53
|
attachments: join(runDirectory, "attachments"),
|
|
49
|
-
|
|
54
|
+
log: join(runDirectory, RUN_LOG_FILE),
|
|
55
|
+
history: join(runDirectory, RUN_HISTORY_FILE),
|
|
50
56
|
};
|
|
51
57
|
return {
|
|
52
58
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
package/src/auditor-soul.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readPageSync } from "./run-dossier.ts";
|
|
2
3
|
import { basename, join } from "node:path";
|
|
3
4
|
|
|
5
|
+
import { parseRunLeaf } from "./role-run-placement.ts";
|
|
4
6
|
import { isRecord } from "./unknown-value.ts";
|
|
5
7
|
|
|
6
8
|
import {
|
|
@@ -109,7 +111,7 @@ export function loadAuditorReferenceMaterialsFromSubjectInput(raw?: string): Pro
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
function subjectFromSourceDirectory(sourceRunDirectory: string): AuditorSoulRole | undefined {
|
|
112
|
-
const role = basename(sourceRunDirectory)
|
|
114
|
+
const role = parseRunLeaf(basename(sourceRunDirectory))?.role;
|
|
113
115
|
return isAuditorSoulRole(role) ? role : undefined;
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -121,15 +123,8 @@ export async function readAuditorResumeBinding(runDirectory: string): Promise<
|
|
|
121
123
|
| { readonly subject: AuditorSoulRole; readonly sourceRunDirectory: string }
|
|
122
124
|
| undefined
|
|
123
125
|
> {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8")) as unknown;
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
if (!isRecord(raw)) return undefined;
|
|
132
|
-
const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown };
|
|
126
|
+
const record = readPageSync(runDirectory, "admitted");
|
|
127
|
+
if (record === undefined) return undefined;
|
|
133
128
|
if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined;
|
|
134
129
|
const subject = isAuditorSoulRole(record.auditorSubject)
|
|
135
130
|
? record.auditorSubject
|
|
@@ -15,9 +15,10 @@ import { isUnboundRunDirectory, roleRunPlacement } from "./role-run-placement.ts
|
|
|
15
15
|
export { isUnboundRunDirectory };
|
|
16
16
|
import {
|
|
17
17
|
MIGRATION_TICKET_DERIVATION_PAGE,
|
|
18
|
-
|
|
18
|
+
readMigrationBoardTicketNumber,
|
|
19
19
|
readMigrationDerivedTicketNumber,
|
|
20
20
|
} from "./run-ticket-number.ts";
|
|
21
|
+
import { readPageSync } from "./run-dossier.ts";
|
|
21
22
|
|
|
22
23
|
import { isRecord, isEnoent } from "./unknown-value.ts";
|
|
23
24
|
|
|
@@ -83,11 +84,23 @@ export function ticketNumberFromWorktreeBasename(
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* projectRoot from a retained run's durable pages.
|
|
86
|
-
*
|
|
87
|
+
* New落点 (state rows) first, then pre-#1161 page files at the migration boundary.
|
|
88
|
+
* ENOENT → try next / undefined; bad JSON and non-ENOENT IO propagate.
|
|
87
89
|
*/
|
|
88
90
|
async function readProjectRootFromRun(
|
|
89
91
|
runDirectory: string,
|
|
90
92
|
): Promise<{ readonly projectRoot: string; readonly sourcePage: string } | undefined> {
|
|
93
|
+
for (const section of ["admitted", "invocation", "runState"] as const) {
|
|
94
|
+
try {
|
|
95
|
+
const page = readPageSync(runDirectory, section);
|
|
96
|
+
const projectRoot = page?.projectRoot;
|
|
97
|
+
if (typeof projectRoot === "string" && projectRoot.length > 0) {
|
|
98
|
+
return { projectRoot, sourcePage: `state.jsonl:${section}` };
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!isEnoent(error)) throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
91
104
|
for (const page of ["admitted-request.json", "invocation.json", "run-state.json"] as const) {
|
|
92
105
|
const path = join(runDirectory, page);
|
|
93
106
|
try {
|
|
@@ -123,7 +136,7 @@ export async function resolveMigratingRunTicket(runDirectory: string): Promise<{
|
|
|
123
136
|
readonly ticketNumber: number | undefined;
|
|
124
137
|
readonly derivation: MigratingRunTicketDerivation | undefined;
|
|
125
138
|
}> {
|
|
126
|
-
const boardTicket = await
|
|
139
|
+
const boardTicket = await readMigrationBoardTicketNumber(runDirectory);
|
|
127
140
|
if (boardTicket !== undefined) {
|
|
128
141
|
return {
|
|
129
142
|
ticketNumber: boardTicket,
|
|
@@ -31,12 +31,14 @@ import {
|
|
|
31
31
|
type BookTopologyPartitionMigrator,
|
|
32
32
|
type MigrationItemOutcome,
|
|
33
33
|
} from "./book-topology-migration.ts";
|
|
34
|
-
import { S4_SUBMISSION_LEDGER_KINDS } from "./sitian-appender.ts";
|
|
35
34
|
import { projectTicketProvenanceHeader } from "./ticket-provenance-contracts.ts";
|
|
36
35
|
|
|
37
36
|
import { sha256Hex } from "./sha256.ts";
|
|
38
37
|
import { isRecord, isEnoent } from "./unknown-value.ts";
|
|
39
38
|
|
|
39
|
+
/** Kinds the retired #852-era submission-ledger volume held (migration of stored runs only). */
|
|
40
|
+
const LEGACY_SUBMISSION_LEDGER_KINDS = new Set(["candidate", "roundContext", "outcome", "sealed", "post-seal-anomaly"]);
|
|
41
|
+
|
|
40
42
|
const TICKET_PROVENANCE = "ticket-provenance";
|
|
41
43
|
const SUBMISSION_LEDGER = "submission-ledger";
|
|
42
44
|
const ATTEMPT_HISTORY = "attempt-history";
|
|
@@ -210,7 +212,7 @@ function runIdFromPayload(payload: unknown): string | undefined {
|
|
|
210
212
|
function recordClassOfKind(kind: unknown): RecordClass | undefined {
|
|
211
213
|
if (kind === TICKET_PROVENANCE) return TICKET_PROVENANCE;
|
|
212
214
|
if (kind === ATTEMPT_HISTORY) return ATTEMPT_HISTORY;
|
|
213
|
-
if (typeof kind === "string" &&
|
|
215
|
+
if (typeof kind === "string" && LEGACY_SUBMISSION_LEDGER_KINDS.has(kind)) return SUBMISSION_LEDGER;
|
|
214
216
|
return undefined;
|
|
215
217
|
}
|
|
216
218
|
|
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
rewriteRoleRunDurablePages,
|
|
27
27
|
type RunDirectoryPathRewrite,
|
|
28
28
|
} from "./role-run-relocation.ts";
|
|
29
|
+
import { renderCurrentSync } from "./run-dossier.ts";
|
|
29
30
|
import {
|
|
30
31
|
MIGRATION_TICKET_DERIVATION_PAGE,
|
|
31
|
-
|
|
32
|
+
readMigrationBoardTicketNumber,
|
|
32
33
|
} from "./run-ticket-number.ts";
|
|
33
34
|
|
|
34
35
|
import { isEnoent } from "./unknown-value.ts";
|
|
@@ -151,7 +152,7 @@ async function planBookMoves(
|
|
|
151
152
|
// #863: board typed ticket on an unbound leaf → place under that ticket.
|
|
152
153
|
// No board ticket → stay unbound. Never invent from prose or derivation.
|
|
153
154
|
const boardTicket = leaf.isDirectory
|
|
154
|
-
? await
|
|
155
|
+
? await readMigrationBoardTicketNumber(leaf.sourcePath)
|
|
155
156
|
: undefined;
|
|
156
157
|
if (parsed !== undefined && boardTicket !== undefined) {
|
|
157
158
|
targetPath = destinationRunDirectory(
|
|
@@ -318,7 +319,7 @@ export async function relocateBoardBoundUnboundRunsInBook(
|
|
|
318
319
|
const parsed = parseRunLeaf(entry.name);
|
|
319
320
|
if (parsed === undefined) continue;
|
|
320
321
|
const sourcePath = join(unboundRuns, entry.name);
|
|
321
|
-
const boardTicket = await
|
|
322
|
+
const boardTicket = await readMigrationBoardTicketNumber(sourcePath);
|
|
322
323
|
if (boardTicket === undefined) continue;
|
|
323
324
|
const targetPath = destinationRunDirectory(
|
|
324
325
|
booksDirectory,
|
|
@@ -367,6 +368,8 @@ export async function relocateBoardBoundUnboundRunsInBook(
|
|
|
367
368
|
for (const move of planned) {
|
|
368
369
|
await mkdir(dirname(move.targetPath), { recursive: true });
|
|
369
370
|
await rename(move.sourcePath, move.targetPath);
|
|
371
|
+
// Same live relocate seam: derived host.original follows the new run directory.
|
|
372
|
+
renderCurrentSync(move.targetPath);
|
|
370
373
|
}
|
|
371
374
|
|
|
372
375
|
return planned.map((move) => ({
|
package/src/collector-config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { sha256Hex } from "./sha256.ts";
|
|
4
4
|
import { isRecord } from "./unknown-value.ts";
|
|
5
|
+
import { isTicketNumberString, parseTicketNumber } from "./run-ticket-number.ts";
|
|
5
6
|
|
|
6
7
|
export const COLLECTOR_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
7
8
|
export const COLLECTOR_REPO_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
|
|
@@ -46,10 +47,11 @@ export function parseCollectorRepository(raw: unknown): CollectorRepository {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function parseCollectorPrNumber(raw: unknown): number {
|
|
49
|
-
if (typeof raw === "string" && !/^[1-9][0-9]*$/.test(raw)) fail("Collector pull request number must be a positive safe integer string");
|
|
50
50
|
if (typeof raw !== "string" && typeof raw !== "number") fail("Collector pull request number is required");
|
|
51
|
-
const value =
|
|
52
|
-
if (
|
|
51
|
+
const value = parseTicketNumber(raw);
|
|
52
|
+
if (value === undefined) fail(typeof raw === "string" && !isTicketNumberString(raw)
|
|
53
|
+
? "Collector pull request number must be a positive safe integer string"
|
|
54
|
+
: "Collector pull request number must be a positive safe integer");
|
|
53
55
|
return value;
|
|
54
56
|
}
|
|
55
57
|
|
package/src/doctor-contracts.ts
CHANGED
|
@@ -49,7 +49,7 @@ export type DoctorSubmission =
|
|
|
49
49
|
// #836: runtime cost is a fact beside the role payload, never merged into it —
|
|
50
50
|
// DoctorOutput is the accepted payload itself, identical to DoctorSubmission.
|
|
51
51
|
export type DoctorOutput = DoctorSubmission;
|
|
52
|
-
export type DoctorEvidenceEntry = { id: string; kind: "session" | "
|
|
52
|
+
export type DoctorEvidenceEntry = { id: string; kind: "session" | "log"; byteLength: number; contentLength: number; sha256: string; content: string };
|
|
53
53
|
export type DoctorCase = { version: 1; identity: DoctorCaseIdentity; evidence: DoctorEvidenceEntry[]; cost: DoctorCaseCost };
|
|
54
54
|
|
|
55
55
|
// status 的合法词写在 description。交卷原样入账(validateDoctorSubmissionShape),
|
package/src/doctor-evidence.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { sha256Hex } from "./sha256.ts";
|
|
4
|
+
import { physicallyContainedIn } from "./activation-ledger-topology.ts";
|
|
4
5
|
import type { DoctorCase, DoctorCaseCost, DoctorCount, DoctorEvidenceEntry } from "./doctor-contracts.ts";
|
|
5
6
|
import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, validateAcceptedDetails } from "./package-contracts/terminating-tools.ts";
|
|
6
7
|
|
|
7
8
|
import { isRecord, isMissingPathError } from "./unknown-value.ts";
|
|
8
9
|
|
|
9
|
-
async function discoverCaseFiles(root: string): Promise<string[]> { const found: string[] = []; async function walk(dir: string, depth: number) { for (const item of await readdir(dir, { withFileTypes: true })) { const path = resolve(dir, item.name); if (item.isDirectory()) { await walk(path, depth + 1); } else if (item.isFile() && (item.name.endsWith(".jsonl") || (item.name === "
|
|
10
|
+
async function discoverCaseFiles(root: string): Promise<string[]> { const found: string[] = []; async function walk(dir: string, depth: number) { for (const item of await readdir(dir, { withFileTypes: true })) { const path = resolve(dir, item.name); if (item.isDirectory()) { await walk(path, depth + 1); } else if (item.isFile() && ((item.name.endsWith(".jsonl") && item.name !== "state.jsonl" && !(item.name === "log.jsonl" && depth === 1)) || (item.name === "log.jsonl" && depth === 1))) found.push(path); } } await walk(root, 0); return found.sort(); }
|
|
10
11
|
function sourceList(count: number, sources: string[]) { return { count, sources: [...new Set(sources)].sort() }; }
|
|
11
12
|
function accumulate(metric: DoctorCount, value: number, source: string) { metric.count += value; if (value) metric.sources.push(source); }
|
|
12
13
|
function timestamp(row: Record<string, unknown>) { return typeof row.timestamp === "string" && Number.isFinite(Date.parse(row.timestamp)) ? row.timestamp : undefined; }
|
|
@@ -38,14 +39,17 @@ function deriveSession(content: string, id: string): SessionDerivation {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Read Pi's retained session directory as the sole raw material for one case. */
|
|
41
|
-
export async function loadDoctorCase(runsPath: string): Promise<DoctorCase> {
|
|
42
|
+
export async function loadDoctorCase(runsPath: string, projectRoot?: string): Promise<DoctorCase> {
|
|
42
43
|
const root = await realpath(runsPath);
|
|
44
|
+
if (projectRoot !== undefined && !physicallyContainedIn(projectRoot, root)) {
|
|
45
|
+
throw new Error("doctor --runs escapes the project root");
|
|
46
|
+
}
|
|
43
47
|
// Canonical <book>/<ticket>/runs; legacy issues/<n>/runs remains read-only compatible.
|
|
44
48
|
const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/);
|
|
45
49
|
if (!match) throw new Error("Doctor case must be an .ak-roles/books/<book>/<n>/runs directory");
|
|
46
50
|
const evidence: DoctorEvidenceEntry[] = [], sessions: DoctorCaseCost["sessions"] = [], statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
|
|
47
51
|
const turns: DoctorCount = { count: 0, sources: [] }, calls: DoctorCount = { count: 0, sources: [] }, tokens: DoctorCount = { count: 0, sources: [] };
|
|
48
|
-
for (const path of await discoverCaseFiles(root)) { const id = relative(root, path).split(sep).join("/"); const bytes = await readFile(path); const content = bytes.toString("utf8"); const kind = id.endsWith(".jsonl") ? "
|
|
52
|
+
for (const path of await discoverCaseFiles(root)) { const id = relative(root, path).split(sep).join("/"); const bytes = await readFile(path); const content = bytes.toString("utf8"); const kind = id.split("/").length === 2 && id.endsWith("/log.jsonl") ? "log" : "session"; evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content }); if (kind === "log") continue; const result = deriveSession(content, id); sessions.push(result.session); statuses.push(...result.statuses); commits.push(...result.commits); accumulate(turns, result.turns, id); accumulate(calls, result.calls, id); accumulate(tokens, result.tokens, id); }
|
|
49
53
|
const runDirs = (await readdir(root, { withFileTypes: true })).filter((item) => item.isDirectory()).map((item) => item.name).sort(); const legs = evidence.filter((entry) => entry.kind === "session").map((entry) => entry.id); const retryDirs = runDirs.filter((name) => /(?:^|[-_])retry(?:[-_]|$)/i.test(name)); const rawBytes = evidence.filter((entry) => entry.kind === "session").reduce((sum, entry) => sum + entry.byteLength, 0);
|
|
50
54
|
const cost: DoctorCaseCost = { invocations: sourceList(runDirs.length, runDirs), legs: sourceList(legs.length, legs), modelApiTurns: sourceList(turns.count, turns.sources), outputTokens: sourceList(tokens.count, tokens.sources), toolCalls: sourceList(calls.count, calls.sources), retries: { ...sourceList(retryDirs.length, retryDirs), evidence: "literal run-dir naming" }, statuses, commits, sessions, outputBytes: { ...sourceList(rawBytes, legs), payload: "raw JSONL bytes", providerWireBytes: "unavailable" } };
|
|
51
55
|
return { version: 1, identity: { issueNumber: Number(match[1]), runsPath: await stableRunsIdentity(root) }, evidence, cost };
|
|
@@ -125,7 +125,7 @@ export function createEngineDetourToolDefinition(input: {
|
|
|
125
125
|
: undefined;
|
|
126
126
|
const runId = runDirectory === undefined ? undefined : runIdFromRunDirectory(runDirectory);
|
|
127
127
|
// Public-invocation scope + selected host from Host envelope only — never
|
|
128
|
-
// courtAttemptId, never sidecar file, never pre-spawn
|
|
128
|
+
// courtAttemptId, never sidecar file, never pre-spawn current.json I/O.
|
|
129
129
|
const invocationScopeId =
|
|
130
130
|
typeof ctx.invocationScopeId === "string" && ctx.invocationScopeId.trim() !== ""
|
|
131
131
|
? ctx.invocationScopeId.trim()
|
|
@@ -15,9 +15,15 @@
|
|
|
15
15
|
* `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
|
|
16
16
|
* toolResult join keys, and never a detour-owned sidecar file.
|
|
17
17
|
*/
|
|
18
|
+
import { readPageSync, RUN_LOG_FILE } from "./run-dossier.ts";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
19
20
|
import { readFileSync } from "node:fs";
|
|
20
|
-
import {
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
runDirectoryFromSessionDirectory,
|
|
25
|
+
sessionFileIn as sessionFileFromSessionDirectory,
|
|
26
|
+
} from "./role-run-placement.ts";
|
|
21
27
|
|
|
22
28
|
import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
|
|
23
29
|
import {
|
|
@@ -36,12 +42,11 @@ export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call" as const;
|
|
|
36
42
|
export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as const;
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
|
-
* Run-relative
|
|
45
|
+
* Run-relative path of the log that holds one detour call's record.
|
|
40
46
|
* Openable once the run directory is known from the terminal's run id.
|
|
41
47
|
* Contains no runId bytes itself (#108 + #537).
|
|
42
48
|
*/
|
|
43
|
-
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE =
|
|
44
|
-
`session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl` as const;
|
|
49
|
+
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = RUN_LOG_FILE;
|
|
45
50
|
|
|
46
51
|
/** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
|
|
47
52
|
export function engineDetourStdoutByteLength(stdout: string): number {
|
|
@@ -246,22 +251,14 @@ export async function readEngineDetourToolUsage(input: {
|
|
|
246
251
|
return { callCount: calls.length, calls };
|
|
247
252
|
}
|
|
248
253
|
|
|
249
|
-
/** One shared
|
|
254
|
+
/** One shared state.jsonl invocation-row reader (absent → undefined; other errors propagate). */
|
|
250
255
|
function readInvocationRecord(
|
|
251
256
|
runDirectory: string,
|
|
252
257
|
): Record<string, unknown> | undefined {
|
|
253
|
-
|
|
254
|
-
const raw = JSON.parse(
|
|
255
|
-
readFileSync(join(runDirectory, "invocation.json"), "utf8"),
|
|
256
|
-
) as unknown;
|
|
257
|
-
return isRecord(raw) ? raw : undefined;
|
|
258
|
-
} catch (error) {
|
|
259
|
-
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
|
|
260
|
-
throw error;
|
|
261
|
-
}
|
|
258
|
+
return readPageSync(runDirectory, "invocation");
|
|
262
259
|
}
|
|
263
260
|
|
|
264
|
-
/** True when invocation
|
|
261
|
+
/** True when the invocation section carries a non-empty engine axis. */
|
|
265
262
|
export async function readInvocationEngineMounted(
|
|
266
263
|
runDirectory: string,
|
|
267
264
|
): Promise<boolean> {
|
|
@@ -288,16 +285,6 @@ export function withEngineDetourToolUsageFact<
|
|
|
288
285
|
};
|
|
289
286
|
}
|
|
290
287
|
|
|
291
|
-
/** runDirectory owning a session directory (.../runs/<id>@role/session). */
|
|
292
|
-
export function runDirectoryFromSessionDirectory(sessionDirectory: string): string {
|
|
293
|
-
return dirname(sessionDirectory);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** session.jsonl under a session directory. */
|
|
297
|
-
export function sessionFileFromSessionDirectory(sessionDirectory: string): string {
|
|
298
|
-
return join(sessionDirectory, "session.jsonl");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
288
|
/**
|
|
302
289
|
* Mint one public-invocation scope id.
|
|
303
290
|
* Call once at the public-entry boundary — never inside the auto-resume loop.
|
|
@@ -13,8 +13,33 @@ import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
|
|
|
13
13
|
import { projectThrownFailureLeaf, retainPackageFault } from "./public-cli/settlement.ts";
|
|
14
14
|
import { describeErrorIdentity } from "./public-cli/run-lifecycle.ts";
|
|
15
15
|
import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
|
|
16
|
+
import { reportRunRecord } from "./sitian-facade.ts";
|
|
16
17
|
import { isOneShotWorkerReminderCode } from "./submission-errors.ts";
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* What one host start was given: one `turn-delivery` row of history.jsonl per start. History is
|
|
21
|
+
* the volume whose write failure does not stop a run (#833), so a row that cannot be
|
|
22
|
+
* written is noted and the host still starts.
|
|
23
|
+
*/
|
|
24
|
+
export async function recordTurnDelivery(
|
|
25
|
+
runDirectory: string,
|
|
26
|
+
delivered: { readonly systemPrompt: string; readonly outputSchema?: unknown },
|
|
27
|
+
source: string,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
try {
|
|
30
|
+
reportRunRecord(runDirectory, "turn-delivery", {
|
|
31
|
+
systemPrompt: delivered.systemPrompt,
|
|
32
|
+
...(delivered.outputSchema === undefined ? {} : { outputSchema: delivered.outputSchema }),
|
|
33
|
+
}, source);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await retainPackageFault({
|
|
36
|
+
runDirectory,
|
|
37
|
+
diagnostic: `turn delivery record failed beside host start: ${describeErrorIdentity(error)}`,
|
|
38
|
+
error,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
|
|
19
44
|
|
|
20
45
|
export type ExternalHostRoundOutcome =
|