@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
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "node:fs";
|
|
2
13
|
import { dirname, resolve, join } from "node:path";
|
|
14
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
15
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
5
16
|
import {
|
|
6
17
|
ActivationLedgerError,
|
|
7
|
-
activationBookDirectory,
|
|
8
18
|
ensureRealDirectoryTree,
|
|
19
|
+
errnoCode,
|
|
9
20
|
errorText,
|
|
10
21
|
pathContainedIn,
|
|
11
22
|
physicallyContainedIn,
|
|
12
23
|
resolveActivationLedgerHomeForPath
|
|
13
24
|
} from "./activation-ledger-topology.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
25
|
+
import {
|
|
26
|
+
NAVIGATOR_RECORD_KIND,
|
|
27
|
+
resolveNavigatorWorkSubjectPlacement
|
|
28
|
+
} from "./archivist-record-topology.js";
|
|
29
|
+
import {
|
|
30
|
+
NAVIGATOR_RECORD_KIND as NAVIGATOR_RECORD_KIND2,
|
|
31
|
+
resolveNavigatorWorkSubjectPlacement as resolveNavigatorWorkSubjectPlacement2
|
|
32
|
+
} from "./archivist-record-topology.js";
|
|
16
33
|
const CURRENT_SESSION_LEDGER = "current-session.json";
|
|
17
34
|
function readCurrentSession(sessionDir) {
|
|
18
35
|
const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
|
|
@@ -41,6 +58,151 @@ function writeCurrentSession(sessionDir, sessionFile) {
|
|
|
41
58
|
);
|
|
42
59
|
}
|
|
43
60
|
}
|
|
61
|
+
function isCurrentSessionClaimRace(error) {
|
|
62
|
+
if (!(error instanceof ActivationLedgerError)) return errnoCode(error) === "EEXIST";
|
|
63
|
+
return errnoCode(error.cause) === "EEXIST";
|
|
64
|
+
}
|
|
65
|
+
function currentSessionLedgerPath(sessionDir) {
|
|
66
|
+
return join(sessionDir, CURRENT_SESSION_LEDGER);
|
|
67
|
+
}
|
|
68
|
+
const SESSION_HEADER_CHUNK_BYTES = 4096;
|
|
69
|
+
const MAX_SESSION_HEADER_SCAN_BYTES = 1024 * 1024;
|
|
70
|
+
function readBoundedSessionHeader(filePath) {
|
|
71
|
+
const fd = openSync(filePath, "r");
|
|
72
|
+
try {
|
|
73
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_CHUNK_BYTES);
|
|
74
|
+
const decoder = new StringDecoder("utf8");
|
|
75
|
+
let pending = "";
|
|
76
|
+
let scanned = 0;
|
|
77
|
+
const consume = (chunk) => {
|
|
78
|
+
pending += chunk;
|
|
79
|
+
let lineStart = 0;
|
|
80
|
+
let newlineIndex = pending.indexOf("\n", lineStart);
|
|
81
|
+
while (newlineIndex !== -1) {
|
|
82
|
+
const line = pending.slice(lineStart, newlineIndex);
|
|
83
|
+
lineStart = newlineIndex + 1;
|
|
84
|
+
newlineIndex = pending.indexOf("\n", lineStart);
|
|
85
|
+
if (line.trim() === "") continue;
|
|
86
|
+
pending = pending.slice(lineStart);
|
|
87
|
+
const header = parseSessionHeaderLine(line);
|
|
88
|
+
return header === void 0 ? { kind: "rejected" } : { kind: "accepted", header };
|
|
89
|
+
}
|
|
90
|
+
pending = pending.slice(lineStart);
|
|
91
|
+
return { kind: "incomplete" };
|
|
92
|
+
};
|
|
93
|
+
const finishPending = (tail) => {
|
|
94
|
+
const result = consume(tail);
|
|
95
|
+
if (result.kind === "accepted") return result.header;
|
|
96
|
+
if (result.kind === "rejected") return void 0;
|
|
97
|
+
if (pending.trim() === "") return void 0;
|
|
98
|
+
const header = parseSessionHeaderLine(pending);
|
|
99
|
+
pending = "";
|
|
100
|
+
return header;
|
|
101
|
+
};
|
|
102
|
+
while (scanned < MAX_SESSION_HEADER_SCAN_BYTES) {
|
|
103
|
+
const toRead = Math.min(buffer.length, MAX_SESSION_HEADER_SCAN_BYTES - scanned);
|
|
104
|
+
const bytesRead = readSync(fd, buffer, 0, toRead, null);
|
|
105
|
+
if (bytesRead === 0) {
|
|
106
|
+
return finishPending(decoder.end());
|
|
107
|
+
}
|
|
108
|
+
scanned += bytesRead;
|
|
109
|
+
const found = consume(decoder.write(buffer.subarray(0, bytesRead)));
|
|
110
|
+
if (found.kind === "accepted") return found.header;
|
|
111
|
+
if (found.kind === "rejected") return void 0;
|
|
112
|
+
}
|
|
113
|
+
const probe = Buffer.allocUnsafe(1);
|
|
114
|
+
if (readSync(fd, probe, 0, 1, null) === 0) {
|
|
115
|
+
return finishPending(decoder.end());
|
|
116
|
+
}
|
|
117
|
+
return void 0;
|
|
118
|
+
} finally {
|
|
119
|
+
closeSync(fd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function parseSessionHeaderLine(line) {
|
|
123
|
+
if (line.trim() === "") return void 0;
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(line);
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
if (typeof parsed !== "object" || parsed === null || parsed.type !== "session" || typeof parsed.id !== "string" || parsed.id.length === 0) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
const cwd = parsed.cwd;
|
|
134
|
+
return {
|
|
135
|
+
type: "session",
|
|
136
|
+
id: parsed.id,
|
|
137
|
+
...typeof cwd === "string" ? { cwd } : {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function mostRecentNavigatorSessionFile(sessionDir, cwd) {
|
|
141
|
+
const absoluteSessionDir = resolve(sessionDir);
|
|
142
|
+
const absoluteCwd = resolve(cwd);
|
|
143
|
+
let names;
|
|
144
|
+
try {
|
|
145
|
+
names = readdirSync(absoluteSessionDir);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new ActivationLedgerError(
|
|
148
|
+
`archivist navigator nest is not readable (${sessionDir}): ${errorText(error)}`,
|
|
149
|
+
{ cause: error }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const matched = [];
|
|
153
|
+
for (const name of names) {
|
|
154
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
155
|
+
const filePath = join(absoluteSessionDir, name);
|
|
156
|
+
let st;
|
|
157
|
+
try {
|
|
158
|
+
st = statSync(filePath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new ActivationLedgerError(
|
|
161
|
+
`archivist navigator session file is not stat-able (${filePath}): ${errorText(error)}`,
|
|
162
|
+
{ cause: error }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!st.isFile()) continue;
|
|
166
|
+
let header;
|
|
167
|
+
try {
|
|
168
|
+
header = readBoundedSessionHeader(filePath);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new ActivationLedgerError(
|
|
171
|
+
`archivist navigator session header is not readable (${filePath}): ${errorText(error)}`,
|
|
172
|
+
{ cause: error }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (header === void 0) continue;
|
|
176
|
+
if (typeof header.cwd !== "string" || header.cwd.length === 0) continue;
|
|
177
|
+
if (resolve(header.cwd) !== absoluteCwd) continue;
|
|
178
|
+
matched.push({ path: filePath, mtimeMs: st.mtimeMs });
|
|
179
|
+
}
|
|
180
|
+
if (matched.length === 0) return void 0;
|
|
181
|
+
matched.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
182
|
+
return matched[0].path;
|
|
183
|
+
}
|
|
184
|
+
function resolveNavigatorNestContinuation(sessionDir, cwd) {
|
|
185
|
+
const ledgerPath = currentSessionLedgerPath(sessionDir);
|
|
186
|
+
if (existsSync(ledgerPath)) {
|
|
187
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
188
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
189
|
+
return recentFile;
|
|
190
|
+
}
|
|
191
|
+
const adopted = mostRecentNavigatorSessionFile(sessionDir, cwd);
|
|
192
|
+
if (adopted === void 0) return void 0;
|
|
193
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, adopted);
|
|
194
|
+
try {
|
|
195
|
+
writeCurrentSession(sessionDir, adopted);
|
|
196
|
+
return adopted;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (isCurrentSessionClaimRace(error)) {
|
|
199
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
200
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
201
|
+
return recentFile;
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
44
206
|
const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
45
207
|
function assertRecentFinalFileUnderSessionDir(sessionDir, recentFile) {
|
|
46
208
|
const absoluteSessionDir = resolve(sessionDir);
|
|
@@ -77,34 +239,55 @@ function assertRecentFinalFileUnderSessionDir(sessionDir, recentFile) {
|
|
|
77
239
|
function createRecordSessionOpen(options) {
|
|
78
240
|
const cwd = options.cwd;
|
|
79
241
|
const parentFile = options.parent?.getSessionFile();
|
|
80
|
-
const ledgerHome = resolveActivationLedgerHomeForPath(parentFile);
|
|
81
242
|
let sessionDir;
|
|
82
243
|
let parentSession;
|
|
83
|
-
|
|
84
|
-
|
|
244
|
+
let mayResumeSameNest;
|
|
245
|
+
let ledgerHome;
|
|
246
|
+
if (options.kind === NAVIGATOR_RECORD_KIND && options.subject !== void 0) {
|
|
247
|
+
const placement = resolveNavigatorWorkSubjectPlacement({
|
|
85
248
|
cwd,
|
|
86
|
-
kind: options.kind,
|
|
87
249
|
subject: options.subject,
|
|
88
|
-
...parentFile === void 0 || parentFile.length === 0 ? {} : { parentSessionFile: parentFile }
|
|
250
|
+
...parentFile === void 0 || parentFile.length === 0 ? {} : { parentSessionFile: parentFile },
|
|
251
|
+
...options.home === void 0 ? {} : { home: options.home }
|
|
89
252
|
});
|
|
253
|
+
sessionDir = placement.sessionDir;
|
|
254
|
+
ledgerHome = placement.ledgerHome;
|
|
90
255
|
parentSession = parentFile && parentFile.length > 0 ? parentFile : void 0;
|
|
256
|
+
mayResumeSameNest = true;
|
|
91
257
|
} else if (parentFile === void 0 || parentFile.length === 0) {
|
|
92
|
-
|
|
258
|
+
if (options.subject === void 0) {
|
|
259
|
+
return { session: SessionManager.inMemory(cwd), resumed: false };
|
|
260
|
+
}
|
|
261
|
+
throw new Error("Durable record ownership requires a parent session inside the ledger home");
|
|
93
262
|
} else {
|
|
263
|
+
ledgerHome = resolveActivationLedgerHomeForPath(parentFile);
|
|
94
264
|
const parentResolved = resolve(parentFile);
|
|
95
|
-
|
|
265
|
+
if (!physicallyContainedIn(ledgerHome, parentResolved)) {
|
|
266
|
+
throw new Error("Durable record ownership requires a parent session inside the ledger home");
|
|
267
|
+
}
|
|
268
|
+
sessionDir = join(dirname(parentResolved), options.kind);
|
|
96
269
|
parentSession = parentFile;
|
|
270
|
+
mayResumeSameNest = options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
97
271
|
}
|
|
98
272
|
const nestAlreadyExists = existsSync(sessionDir);
|
|
99
273
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
100
|
-
const mayResumeSameNest = options.subject !== void 0 || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
101
274
|
if (mayResumeSameNest && nestAlreadyExists) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
275
|
+
if (options.kind === NAVIGATOR_RECORD_KIND) {
|
|
276
|
+
const recentFile = resolveNavigatorNestContinuation(sessionDir, cwd);
|
|
277
|
+
if (recentFile !== void 0) {
|
|
278
|
+
return {
|
|
279
|
+
session: SessionManager.open(recentFile, sessionDir, cwd),
|
|
280
|
+
resumed: true
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
285
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
286
|
+
return {
|
|
287
|
+
session: SessionManager.open(recentFile, sessionDir, cwd),
|
|
288
|
+
resumed: true
|
|
289
|
+
};
|
|
290
|
+
}
|
|
108
291
|
}
|
|
109
292
|
const session = SessionManager.create(
|
|
110
293
|
cwd,
|
|
@@ -151,9 +334,10 @@ function bookDirectOfficerRunPointer(options) {
|
|
|
151
334
|
}
|
|
152
335
|
export {
|
|
153
336
|
DIRECT_OFFICER_RUN_POINTER_KIND,
|
|
337
|
+
NAVIGATOR_RECORD_KIND2 as NAVIGATOR_RECORD_KIND,
|
|
154
338
|
WORKER_SUBMISSION_GATE_KIND,
|
|
155
339
|
bookDirectOfficerRunPointer,
|
|
156
340
|
createRecordSession,
|
|
157
341
|
createRecordSessionOpen,
|
|
158
|
-
|
|
342
|
+
resolveNavigatorWorkSubjectPlacement2 as resolveNavigatorWorkSubjectPlacement
|
|
159
343
|
};
|
|
@@ -1,20 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure archivist record placement topology (ADR 0048 / 0065 / dossier-topology).
|
|
3
|
+
* No SessionManager / pi-coding-agent — cold public-bin and ledger discovery
|
|
4
|
+
* may import this without pulling open-session runtime into the graph.
|
|
5
|
+
*
|
|
6
|
+
* Navigator work-subject is the sole book-top subject partition (#852):
|
|
7
|
+
* books/<book>/navigator/<work-subject-digest>/. Callers must not re-hash
|
|
8
|
+
* subject or re-join book/navigator/digest themselves.
|
|
9
|
+
*/
|
|
1
10
|
import { createHash } from "node:crypto";
|
|
2
11
|
import { join } from "node:path";
|
|
3
12
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
import { activationBookDirectory, resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
|
|
14
|
+
/** Book-top partition for cross-ticket navigator route-memory (dossier-topology). */
|
|
15
|
+
export const NAVIGATOR_RECORD_KIND = "navigator";
|
|
16
|
+
/**
|
|
17
|
+
* Sole navigator work-subject placement under the ledger book.
|
|
18
|
+
* Returns ledgerHome and nest once so createRecordSessionOpen does not re-branch
|
|
19
|
+
* parent/home. Work-subject key shape is sha256(subject) hex truncated to 32
|
|
20
|
+
* (#852 out of scope — shape unchanged).
|
|
21
|
+
*
|
|
22
|
+
* parentSessionFile contributes home only when tryHomeFromAkRolesPath succeeds;
|
|
23
|
+
* otherwise the explicit home (from HostContext.runDirectory) is kept. Never
|
|
24
|
+
* invents a second home via passwd/env fallback from a foreign parent path.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveNavigatorWorkSubjectPlacement(input) {
|
|
27
|
+
let ledgerHome;
|
|
28
|
+
if (input.parentSessionFile !== undefined && input.parentSessionFile.length > 0) {
|
|
29
|
+
const fromParent = tryHomeFromAkRolesPath(input.parentSessionFile);
|
|
30
|
+
if (fromParent !== undefined && fromParent.length > 0) {
|
|
31
|
+
ledgerHome = resolveActivationLedgerHome(fromParent);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ledgerHome ??= resolveActivationLedgerHome(input.home);
|
|
35
|
+
const digest = createHash("sha256").update(input.subject).digest("hex").slice(0, 32);
|
|
36
|
+
return {
|
|
37
|
+
ledgerHome,
|
|
38
|
+
sessionDir: join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(input.cwd)), NAVIGATOR_RECORD_KIND, digest),
|
|
39
|
+
};
|
|
17
40
|
}
|
|
18
|
-
export {
|
|
19
|
-
subjectKeyedRecordDirectory
|
|
20
|
-
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { roleRunArtifactsDirectory } from "./role-run-placement.js";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier";
|
|
4
5
|
function auditorRunDirectory(context) {
|
|
@@ -25,7 +26,7 @@ function createAuditorDossierTool(runDirectory) {
|
|
|
25
26
|
admittedRequest: join(runDirectory, "admitted-request.json"),
|
|
26
27
|
parentSessionCandidate: join(runDirectory, "session", "session.jsonl"),
|
|
27
28
|
attachments: join(runDirectory, "attachments"),
|
|
28
|
-
artifacts:
|
|
29
|
+
artifacts: roleRunArtifactsDirectory(runDirectory)
|
|
29
30
|
};
|
|
30
31
|
return {
|
|
31
32
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
@@ -7,8 +7,104 @@
|
|
|
7
7
|
* #779: no frozen candidate catalog; LLM finds sources and submits whole blocks.
|
|
8
8
|
* Mechanical layer does not judge LLM output (no quote/ticket/relevance verify).
|
|
9
9
|
*/
|
|
10
|
+
import { isSafePositiveTicketNumber, requireSafePositiveTicketNumber, } from "./run-ticket-number.js";
|
|
10
11
|
export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
|
|
11
12
|
export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
|
|
13
|
+
/**
|
|
14
|
+
* Sole shape projection for a typed co-review ticket set (#871).
|
|
15
|
+
* Positive safe integers, first-seen order, duplicates dropped. Not content judgment.
|
|
16
|
+
* Returns null when `raw` is not an array (callers classify absent vs durable damage).
|
|
17
|
+
* Returns a possibly empty list when `raw` is an array (unlawful members skipped).
|
|
18
|
+
* When `principalTicket` is set, that ticket is guaranteed as a member (prepended if missing).
|
|
19
|
+
*/
|
|
20
|
+
export function projectCourtTicketNumbers(raw, options) {
|
|
21
|
+
if (!Array.isArray(raw))
|
|
22
|
+
return null;
|
|
23
|
+
const out = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const item of raw) {
|
|
26
|
+
if (!isSafePositiveTicketNumber(item))
|
|
27
|
+
continue;
|
|
28
|
+
if (seen.has(item))
|
|
29
|
+
continue;
|
|
30
|
+
seen.add(item);
|
|
31
|
+
out.push(item);
|
|
32
|
+
}
|
|
33
|
+
const principal = options?.principalTicket;
|
|
34
|
+
if (principal === undefined)
|
|
35
|
+
return out;
|
|
36
|
+
requireSafePositiveTicketNumber(principal, "projectCourtTicketNumbers principalTicket");
|
|
37
|
+
if (seen.has(principal))
|
|
38
|
+
return out;
|
|
39
|
+
return [principal, ...out];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Durable-field interpreter for #871 co-review sets.
|
|
43
|
+
* Absent key (old-format run) is not damage. Present but empty / non-array /
|
|
44
|
+
* partially unlawful / missing principal is damage — callers must fail closed,
|
|
45
|
+
* never wash into main-only.
|
|
46
|
+
*/
|
|
47
|
+
export function interpretDurableCourtTicketNumbers(record, options) {
|
|
48
|
+
if (!Object.hasOwn(record, "courtTicketNumbers")) {
|
|
49
|
+
return { kind: "absent" };
|
|
50
|
+
}
|
|
51
|
+
const raw = record.courtTicketNumbers;
|
|
52
|
+
if (!Array.isArray(raw)) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "damage",
|
|
55
|
+
reason: "courtTicketNumbers is present but not an array",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (raw.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "damage",
|
|
61
|
+
reason: "courtTicketNumbers is present but empty",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
65
|
+
const item = raw[i];
|
|
66
|
+
if (!isSafePositiveTicketNumber(item)) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "damage",
|
|
69
|
+
reason: `courtTicketNumbers[${i}] is not a safe positive integer`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Members are all lawful numbers; sole projection owns dedupe/order.
|
|
74
|
+
const tickets = projectCourtTicketNumbers(raw);
|
|
75
|
+
if (tickets === null || tickets.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "damage",
|
|
78
|
+
reason: "courtTicketNumbers projected empty after lawful members",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const principal = options?.principalTicket;
|
|
82
|
+
if (principal !== undefined) {
|
|
83
|
+
if (!isSafePositiveTicketNumber(principal)) {
|
|
84
|
+
return {
|
|
85
|
+
kind: "damage",
|
|
86
|
+
reason: "courtTicketNumbers principal ticket is not a safe positive integer",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!tickets.includes(principal)) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "damage",
|
|
92
|
+
reason: `courtTicketNumbers is missing principal ticket #${principal}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { kind: "ok", tickets: Object.freeze([...tickets]) };
|
|
97
|
+
}
|
|
98
|
+
/** True when two durable #871 sets are byte-identical sequences. */
|
|
99
|
+
export function sameCourtTicketNumbers(left, right) {
|
|
100
|
+
if (left.length !== right.length)
|
|
101
|
+
return false;
|
|
102
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
103
|
+
if (left[i] !== right[i])
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
12
108
|
export function validateRecordedDiaristOutput(value) {
|
|
13
109
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
110
|
throw new Error("Diarist output has no execution discriminator");
|
package/dist/doctor-auditor.js
CHANGED
|
@@ -8,7 +8,7 @@ export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
|
|
|
8
8
|
*/
|
|
9
9
|
export function createPiDoctorAuditor() {
|
|
10
10
|
return async (options) => {
|
|
11
|
-
const dossier = resolveAuditDossier();
|
|
11
|
+
const dossier = resolveAuditDossier(options.context);
|
|
12
12
|
requireAuditMaterials(dossier);
|
|
13
13
|
const subjects = readDoctorAuditSubjects(options.context);
|
|
14
14
|
requireAuditMaterials(subjects);
|
package/dist/doctor-evidence.js
CHANGED
|
@@ -108,9 +108,10 @@ function deriveSession(content, id) {
|
|
|
108
108
|
/** Read Pi's retained session directory as the sole raw material for one case. */
|
|
109
109
|
export async function loadDoctorCase(runsPath) {
|
|
110
110
|
const root = await realpath(runsPath);
|
|
111
|
-
|
|
111
|
+
// Canonical <book>/<ticket>/runs; legacy issues/<n>/runs remains read-only compatible.
|
|
112
|
+
const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/);
|
|
112
113
|
if (!match)
|
|
113
|
-
throw new Error("Doctor case must be an .ak-roles/books/<book
|
|
114
|
+
throw new Error("Doctor case must be an .ak-roles/books/<book>/<n>/runs directory");
|
|
114
115
|
const evidence = [], sessions = [], statuses = [], commits = [];
|
|
115
116
|
const turns = { count: 0, sources: [] }, calls = { count: 0, sources: [] }, tokens = { count: 0, sources: [] };
|
|
116
117
|
for (const path of await discoverCaseFiles(root)) {
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unique dossier-resolution seam for 审刑院 (#233).
|
|
3
|
-
* Machine pointers only: cwd +
|
|
3
|
+
* Machine pointers only: cwd + per-turn HostContext (Pi child env fallback).
|
|
4
|
+
* No latest-run / mtime / global scan.
|
|
4
5
|
*/
|
|
5
6
|
import { existsSync, statSync } from "node:fs";
|
|
6
7
|
import { resolve } from "node:path";
|
|
8
|
+
import { runDirectoryFromHostContext } from "./host-contracts.js";
|
|
7
9
|
export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR";
|
|
8
10
|
export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate";
|
|
11
|
+
function isHostContext(value) {
|
|
12
|
+
return "sessionManager" in value;
|
|
13
|
+
}
|
|
14
|
+
function dossierPointerFrom(source) {
|
|
15
|
+
if (source !== undefined && isHostContext(source)) {
|
|
16
|
+
return runDirectoryFromHostContext(source);
|
|
17
|
+
}
|
|
18
|
+
const env = source ?? process.env;
|
|
19
|
+
const raw = env[AUDIT_RUN_DIR_ENV];
|
|
20
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw : undefined;
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
|
-
* Resolve the per-run dossier pointer
|
|
23
|
+
* Resolve the per-run dossier pointer.
|
|
11
24
|
*
|
|
25
|
+
* ACP/headless: typed HostContext.runDirectory.
|
|
26
|
+
* Pi child: HostContext projected from env, or env fallback when no context.
|
|
12
27
|
* Absent pointer = bare Pi internal seam (ADR 0052): audit proceeds; the model
|
|
13
|
-
* self-locates the dossier from its own fall-volume position per soul.
|
|
14
|
-
* always injects the pointer — only then does the machine validate the path.
|
|
15
|
-
* Concurrent runs stay isolated because a present pointer is per-process.
|
|
28
|
+
* self-locates the dossier from its own fall-volume position per soul.
|
|
16
29
|
*/
|
|
17
|
-
export function resolveAuditDossier(
|
|
18
|
-
const raw =
|
|
30
|
+
export function resolveAuditDossier(source) {
|
|
31
|
+
const raw = dossierPointerFrom(source);
|
|
19
32
|
// Bare Pi activation seam: no machine gate when the pointer was never injected.
|
|
20
|
-
if (
|
|
33
|
+
if (raw === undefined) {
|
|
21
34
|
return { status: "ok" };
|
|
22
35
|
}
|
|
23
36
|
const runDirectory = resolve(raw);
|
package/dist/gatekeeper-role.js
CHANGED
|
@@ -84,15 +84,26 @@ function projectOfficerDecision(officer, decision, fallbackStatus) {
|
|
|
84
84
|
return { status: "needs_reask", officer, receipt };
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
87
|
+
* This-court officer payloads for the parent return path (#879).
|
|
88
|
+
* Only settlement-scoped roleOutcome.payloads (courtAttempt seal) carry this-court
|
|
89
|
+
* identity. Never guess from undivided submissions — sole row included — history
|
|
90
|
+
* stays on terminal.submissions (#836 presentation).
|
|
90
91
|
*/
|
|
91
|
-
function
|
|
92
|
+
function thisCourtOfficerPayloads(terminal) {
|
|
92
93
|
const outcome = terminal?.roleOutcome;
|
|
93
94
|
if (outcome !== undefined && (outcome.kind === "accepted" || outcome.kind === "audit_escalation")) {
|
|
94
|
-
|
|
95
|
+
if (outcome.payloads !== undefined && outcome.payloads.length > 0)
|
|
96
|
+
return outcome.payloads;
|
|
97
|
+
}
|
|
98
|
+
if (outcome?.kind === "failure" && outcome.payloads !== undefined && outcome.payloads.length > 0) {
|
|
99
|
+
return outcome.payloads;
|
|
95
100
|
}
|
|
101
|
+
// No sole-row identity guess: undivided submissions are not this-court (#879).
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
/** Failure/transport channel may still surface every recorded payload beside the failure (#836 A.3). */
|
|
105
|
+
function officerFailurePayloads(terminal) {
|
|
106
|
+
const outcome = terminal?.roleOutcome;
|
|
96
107
|
if (outcome?.kind === "failure")
|
|
97
108
|
return outcome.payloads ?? terminal?.submissions ?? [];
|
|
98
109
|
return terminal?.submissions ?? [];
|
|
@@ -101,32 +112,24 @@ function projectOfficerPayloads(officer, payloads, fallbackStatus) {
|
|
|
101
112
|
if (payloads.length === 0) {
|
|
102
113
|
return projectOfficerDecision(officer, undefined, fallbackStatus);
|
|
103
114
|
}
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
if (payloads.length === 1)
|
|
108
|
-
return queued;
|
|
109
|
-
if (queued.status === "pass"
|
|
110
|
-
|| queued.status === "bounce"
|
|
111
|
-
|| queued.status === "escalate"
|
|
112
|
-
|| queued.status === "needs_reask") {
|
|
113
|
-
return { ...queued, receipt: payloads };
|
|
114
|
-
}
|
|
115
|
-
return queued;
|
|
115
|
+
// This-court multi-submit: queue the latest seal of THIS court only (#836 呈现≠排队).
|
|
116
|
+
// Single this-court seal is the common path. Never fold history into an array receipt.
|
|
117
|
+
return projectOfficerDecision(officer, payloads[payloads.length - 1], fallbackStatus);
|
|
116
118
|
}
|
|
117
119
|
function projectOfficerTerminal(officer, summoned) {
|
|
118
120
|
const terminal = summoned.terminal;
|
|
119
121
|
const outcome = terminal?.roleOutcome;
|
|
120
|
-
const
|
|
122
|
+
const thisCourt = thisCourtOfficerPayloads(terminal);
|
|
121
123
|
if (outcome === undefined) {
|
|
122
124
|
const detail = summoned.stderr ?? "";
|
|
125
|
+
const failurePayloads = officerFailurePayloads(terminal);
|
|
123
126
|
return {
|
|
124
127
|
status: "transport_failure",
|
|
125
128
|
stage: officer,
|
|
126
129
|
reason: detail.length > 0
|
|
127
130
|
? `${gateSeatLabel(officer)} public summon exit ${summoned.exitCode}: ${detail}`
|
|
128
131
|
: `${gateSeatLabel(officer)} public summon produced no terminal (exit ${summoned.exitCode})`,
|
|
129
|
-
submission:
|
|
132
|
+
submission: failurePayloads.length > 0 ? failurePayloads : summoned,
|
|
130
133
|
};
|
|
131
134
|
}
|
|
132
135
|
if (outcome.kind === "no_receipt") {
|
|
@@ -140,30 +143,33 @@ function projectOfficerTerminal(officer, summoned) {
|
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
if (outcome.kind === "failure") {
|
|
146
|
+
const failurePayloads = officerFailurePayloads(terminal);
|
|
143
147
|
return {
|
|
144
148
|
status: "transport_failure",
|
|
145
149
|
stage: officer,
|
|
146
150
|
reason: outcome.diagnostic,
|
|
147
|
-
submission:
|
|
151
|
+
submission: failurePayloads.length > 0 ? failurePayloads : outcome.decisiveFacts,
|
|
148
152
|
};
|
|
149
153
|
}
|
|
150
154
|
if (outcome.kind === "audit_escalation") {
|
|
151
155
|
return {
|
|
152
156
|
status: "escalate",
|
|
153
157
|
officer,
|
|
154
|
-
receipt
|
|
158
|
+
// This-court receipt only (#879) — historical rows remain on terminal.submissions.
|
|
159
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
155
160
|
};
|
|
156
161
|
}
|
|
157
162
|
if (outcome.kind === "accepted") {
|
|
158
163
|
// outcome.status is the fixture/compat leaf: production settlement leaves
|
|
159
164
|
// it undefined once payloads are recorded, so this only matters when a
|
|
160
165
|
// caller still supplies status without any recorded payload (#836 hang).
|
|
161
|
-
return projectOfficerPayloads(officer,
|
|
166
|
+
return projectOfficerPayloads(officer, thisCourt, outcome.status);
|
|
162
167
|
}
|
|
163
168
|
return {
|
|
164
169
|
status: "needs_reask",
|
|
165
170
|
officer,
|
|
166
|
-
|
|
171
|
+
// This-court receipt only (#879).
|
|
172
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
167
173
|
};
|
|
168
174
|
}
|
|
169
175
|
/**
|
|
@@ -188,12 +194,12 @@ export async function projectGatekeeperRun(options) {
|
|
|
188
194
|
},
|
|
189
195
|
};
|
|
190
196
|
}
|
|
191
|
-
// #
|
|
192
|
-
//
|
|
197
|
+
// #879: binding pointer = parent run directory; dialogue content = this-turn
|
|
198
|
+
// typed payload passed explicitly (never latest-toolCall recovery, A7.1–A7.3).
|
|
193
199
|
let summoned;
|
|
194
200
|
try {
|
|
195
201
|
const summon = options.summonOfficer
|
|
196
|
-
?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
|
|
202
|
+
?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask, nextSubmission) => {
|
|
197
203
|
const { summonGateOfficer } = await import("./public-role-summons.js");
|
|
198
204
|
return summonGateOfficer({
|
|
199
205
|
officer: nextOfficer,
|
|
@@ -201,13 +207,14 @@ export async function projectGatekeeperRun(options) {
|
|
|
201
207
|
cwd: options.context.cwd ?? process.cwd(),
|
|
202
208
|
...(officerSignal === undefined ? {} : { signal: officerSignal }),
|
|
203
209
|
...(reask === undefined ? {} : { reask }),
|
|
210
|
+
...(nextSubmission === undefined ? {} : { submission: nextSubmission }),
|
|
204
211
|
...(options.home === undefined ? {} : { home: options.home }),
|
|
205
212
|
...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
|
|
206
213
|
...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
|
|
207
214
|
...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
|
|
208
215
|
});
|
|
209
216
|
});
|
|
210
|
-
summoned = await summon(officer, runDirectory, options.signal, options.reask);
|
|
217
|
+
summoned = await summon(officer, runDirectory, options.signal, options.reask, options.submission);
|
|
211
218
|
}
|
|
212
219
|
catch (error) {
|
|
213
220
|
return {
|