@akagilnc/pi-workflow-roles 0.1.4062 → 0.1.4196
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 +2075 -1088
- 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-evidence.js +3 -2
- package/dist/headless-host/production-host.js +2215 -1228
- 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/pi/durable-principal.js +9 -6
- package/dist/public-cli/auto-resume.js +2 -5
- package/dist/public-cli/diarist-run.js +27 -25
- package/dist/public-cli/instruction-seat-run.js +8 -11
- package/dist/public-cli/invocation.js +127 -63
- package/dist/public-cli/main.js +1507 -645
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-cli/post-admission.js +47 -12
- package/dist/public-cli/run-lifecycle.js +206 -58
- package/dist/public-cli/seat-ticket-binding.js +3 -6
- package/dist/public-cli/settlement.js +5 -3
- 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 +78 -19
- package/dist/ticket-provenance.js +3 -1
- package/package.json +3 -2
- package/scripts/build-package.mjs +34 -2
- package/souls/countersign.md +2 -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-evidence.ts +4 -1
- package/src/doctor-role.ts +1 -1
- package/src/navigator-public-session.ts +33 -30
- package/src/notary-source-run.ts +9 -4
- package/src/pi/durable-principal.ts +9 -13
- package/src/public-cli/auto-resume.ts +2 -6
- package/src/public-cli/countersign-run.ts +182 -32
- package/src/public-cli/diarist-run.ts +34 -26
- package/src/public-cli/instruction-seat-run.ts +7 -11
- package/src/public-cli/invocation.ts +192 -58
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-cli/post-admission.ts +73 -13
- package/src/public-cli/run-lifecycle.ts +255 -65
- package/src/public-cli/seat-ticket-binding.ts +3 -6
- package/src/public-cli/settlement.ts +6 -4
- package/src/role-run-placement.ts +128 -0
- package/src/role-run-relocation.ts +491 -0
- package/src/role-runtime.ts +11 -5
- 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 +102 -19
- package/src/ticket-provenance.ts +4 -1
- package/src/ticket-trajectory.ts +22 -35
- package/src/worker-role.ts +4 -4
- package/src/worker-submission-gates.ts +6 -6
package/src/run-ticket-number.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed ticketNumber reader for a retained run's durable pages.
|
|
3
|
-
* admitted-request
|
|
4
|
-
*
|
|
3
|
+
* Board pages first (admitted-request, then invocation); migration derivation
|
|
4
|
+
* page last so worktree-derived placement stays readable without forging a
|
|
5
|
+
* board assertion. Missing page (ENOENT) → try next / undefined; damage and
|
|
6
|
+
* non-ENOENT IO failures propagate.
|
|
5
7
|
*/
|
|
6
8
|
import { readFile } from "node:fs/promises";
|
|
7
9
|
import { join } from "node:path";
|
|
8
10
|
|
|
11
|
+
/** Sole on-disk page for worktree-basename ticket derivation (#865). */
|
|
12
|
+
export const MIGRATION_TICKET_DERIVATION_PAGE =
|
|
13
|
+
"migration-ticket-derivation.json" as const;
|
|
14
|
+
|
|
9
15
|
function isEnoent(error: unknown): boolean {
|
|
10
16
|
return (
|
|
11
17
|
error instanceof Error &&
|
|
@@ -14,40 +20,94 @@ function isEnoent(error: unknown): boolean {
|
|
|
14
20
|
);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Sole safe-positive ticket invariant (bind / admission / placement / readers). */
|
|
24
|
+
export function isSafePositiveTicketNumber(value: unknown): value is number {
|
|
25
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Loud reject for non-safe-positive ticket numbers before placement or bind. */
|
|
29
|
+
export function requireSafePositiveTicketNumber(
|
|
30
|
+
ticketNumber: number,
|
|
31
|
+
context = "ticketNumber",
|
|
32
|
+
): number {
|
|
33
|
+
if (!isSafePositiveTicketNumber(ticketNumber)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`${context} requires a safe positive integer, got ${String(ticketNumber)}`,
|
|
36
|
+
);
|
|
25
37
|
}
|
|
26
|
-
return
|
|
38
|
+
return ticketNumber;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
function ticketFromRecord(record: Record<string, unknown>): number | undefined {
|
|
42
|
+
return isSafePositiveTicketNumber(record.ticketNumber)
|
|
43
|
+
? record.ticketNumber
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readJsonObject(
|
|
48
|
+
path: string,
|
|
49
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
33
50
|
try {
|
|
34
|
-
const raw = JSON.parse(await readFile(
|
|
51
|
+
const raw: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
35
52
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
36
53
|
return undefined;
|
|
37
54
|
}
|
|
38
|
-
return
|
|
55
|
+
return raw as Record<string, unknown>;
|
|
39
56
|
} catch (error) {
|
|
40
57
|
if (isEnoent(error)) return undefined;
|
|
41
58
|
throw error;
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
async function readBoardPageTicketNumber(
|
|
63
|
+
runDirectory: string,
|
|
64
|
+
page: "admitted-request.json" | "invocation.json",
|
|
65
|
+
): Promise<number | undefined> {
|
|
66
|
+
const record = await readJsonObject(join(runDirectory, page));
|
|
67
|
+
if (record === undefined) return undefined;
|
|
68
|
+
return ticketFromRecord(record);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Board-only ticketNumber (admitted → invocation). Placement attribution and
|
|
73
|
+
* any caller that must ignore derivation pages use this sole projection.
|
|
74
|
+
*/
|
|
75
|
+
export async function readBoardTicketNumber(
|
|
76
|
+
runDirectory: string,
|
|
77
|
+
): Promise<number | undefined> {
|
|
78
|
+
return (
|
|
79
|
+
(await readBoardPageTicketNumber(runDirectory, "admitted-request.json")) ??
|
|
80
|
+
(await readBoardPageTicketNumber(runDirectory, "invocation.json"))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Worktree-derivation ticket only — never a board assertion. Callers that must
|
|
86
|
+
* distinguish board vs derived use this; effective readers use readRunTicketNumber.
|
|
87
|
+
*/
|
|
88
|
+
export async function readMigrationDerivedTicketNumber(
|
|
89
|
+
runDirectory: string,
|
|
90
|
+
): Promise<number | undefined> {
|
|
91
|
+
const record = await readJsonObject(
|
|
92
|
+
join(runDirectory, MIGRATION_TICKET_DERIVATION_PAGE),
|
|
93
|
+
);
|
|
94
|
+
if (record === undefined) return undefined;
|
|
95
|
+
if (record.derivation !== "worktree-path-basename") return undefined;
|
|
96
|
+
return ticketFromRecord(record);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Display / historical-placement ticketNumber for a retained run:
|
|
101
|
+
* board admitted → board invocation → migration derivation.
|
|
102
|
+
* Derivation is last so board wins. Callers that mint, resume, inherit, or
|
|
103
|
+
* otherwise write ticket identity must use readBoardTicketNumber instead —
|
|
104
|
+
* derived placement is never a board assertion.
|
|
105
|
+
*/
|
|
46
106
|
export async function readRunTicketNumber(
|
|
47
107
|
runDirectory: string,
|
|
48
108
|
): Promise<number | undefined> {
|
|
49
109
|
return (
|
|
50
|
-
(await
|
|
51
|
-
(await
|
|
110
|
+
(await readBoardTicketNumber(runDirectory)) ??
|
|
111
|
+
(await readMigrationDerivedTicketNumber(runDirectory))
|
|
52
112
|
);
|
|
53
113
|
}
|
package/src/sitian-appender.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
unlinkSync,
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from "node:fs";
|
|
14
|
-
import {
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
15
|
|
|
16
16
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
17
17
|
import {
|
|
@@ -204,14 +204,6 @@ export function resolveSitianVolumeCategory(kind: string): string {
|
|
|
204
204
|
return kind;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
function safeBookKey(cwd: string): string {
|
|
208
|
-
try {
|
|
209
|
-
return resolveBookKeyFromGit(cwd);
|
|
210
|
-
} catch {
|
|
211
|
-
return basename(resolve(cwd)) || "default";
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
207
|
type SitianRecordPath = {
|
|
216
208
|
readonly sessionDir: string;
|
|
217
209
|
readonly recordFile: string;
|
|
@@ -223,30 +215,35 @@ export function resolveSitianRecordPathInLedger(
|
|
|
223
215
|
input: SitianRecordInput,
|
|
224
216
|
ledgerHome: string,
|
|
225
217
|
): SitianRecordPath {
|
|
226
|
-
const cwd = input.cwd ?? process.cwd();
|
|
227
218
|
const category = resolveSitianVolumeCategory(input.kind);
|
|
219
|
+
const ticketNumber = category === "ticket-provenance"
|
|
220
|
+
? typeof input.subject === "string" && /^[1-9][0-9]*$/.test(input.subject)
|
|
221
|
+
? input.subject
|
|
222
|
+
: typeof input.subject === "object"
|
|
223
|
+
&& typeof input.subject.ticketNumber === "number"
|
|
224
|
+
&& Number.isSafeInteger(input.subject.ticketNumber)
|
|
225
|
+
&& input.subject.ticketNumber > 0
|
|
226
|
+
? String(input.subject.ticketNumber)
|
|
227
|
+
: undefined
|
|
228
|
+
: undefined;
|
|
228
229
|
|
|
229
230
|
let sessionDir: string;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
if (ticketNumber !== undefined) {
|
|
232
|
+
// docs/dossier-topology.md: ticket dir holds records.jsonl + 起居录.md directly.
|
|
233
|
+
const bookDir = activationBookDirectory(
|
|
234
|
+
ledgerHome,
|
|
235
|
+
resolveBookKeyFromGit(input.cwd ?? process.cwd()),
|
|
236
|
+
);
|
|
237
|
+
sessionDir = join(bookDir, ticketNumber);
|
|
233
238
|
} else {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} else if (typeof input.subject.runId === "string" && input.subject.runId.length > 0) {
|
|
241
|
-
subjectStr = input.subject.runId;
|
|
242
|
-
} else {
|
|
243
|
-
subjectStr = JSON.stringify(input.subject);
|
|
244
|
-
}
|
|
245
|
-
const digest = createHash("sha256").update(subjectStr).digest("hex").slice(0, 32);
|
|
246
|
-
sessionDir = join(bookDir, category, digest);
|
|
247
|
-
} else {
|
|
248
|
-
sessionDir = join(bookDir, category);
|
|
239
|
+
if (
|
|
240
|
+
input.sessionParent === undefined
|
|
241
|
+
|| input.sessionParent.length === 0
|
|
242
|
+
|| !physicallyContainedIn(ledgerHome, input.sessionParent)
|
|
243
|
+
) {
|
|
244
|
+
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
249
245
|
}
|
|
246
|
+
sessionDir = join(dirname(input.sessionParent), category);
|
|
250
247
|
}
|
|
251
248
|
|
|
252
249
|
const recordFile = join(sessionDir, "records.jsonl");
|
package/src/submission-ledger.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
resolveActivationLedgerHome,
|
|
3
5
|
tryHomeFromAkRolesPath,
|
|
@@ -14,6 +16,8 @@ import { isAuditEscalationProjection } from "./audit-escalation.ts";
|
|
|
14
16
|
|
|
15
17
|
import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
|
|
16
18
|
import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport, type RecordPointer } from "./sitian-facade.ts";
|
|
19
|
+
import type { SitianRecord } from "./sitian-contracts.ts";
|
|
20
|
+
import { findRunDirectoryById } from "./public-cli/run-lifecycle.ts";
|
|
17
21
|
import type { TerminalRoleName } from "./public-cli/terminal.ts";
|
|
18
22
|
import { isCorrectableExecuteError } from "./submission-correctable-error.ts";
|
|
19
23
|
import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
@@ -111,28 +115,64 @@ export type ClosedSubmission = {
|
|
|
111
115
|
|
|
112
116
|
export type ClosedSubmissionProjection = ClosedSubmission;
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the submission-ledger record file for a run.
|
|
120
|
+
* Unknown run (no sessionParent and no discoverable directory) → undefined so
|
|
121
|
+
* read APIs return their empty set at the read boundary. Write paths always
|
|
122
|
+
* supply sessionParent and keep the ownership gate.
|
|
123
|
+
*/
|
|
124
|
+
async function submissionRecordFile(cwd: string, runId: string, scope: SubmissionLedgerReadScope): Promise<string | undefined> {
|
|
125
|
+
const ledgerHome = resolveActivationLedgerHome(scope.home);
|
|
126
|
+
let sessionParent = scope.sessionParent;
|
|
127
|
+
if (sessionParent === undefined) {
|
|
128
|
+
const discoveredRun = await findRunDirectoryById(scope.home, runId);
|
|
129
|
+
if (discoveredRun === undefined) return undefined;
|
|
130
|
+
sessionParent = join(discoveredRun, "session", "session.jsonl");
|
|
131
|
+
}
|
|
116
132
|
return resolveSitianRecordPathInLedger({
|
|
117
133
|
level: "event",
|
|
118
134
|
kind: "candidate",
|
|
119
135
|
subject: { runId },
|
|
120
136
|
cwd,
|
|
137
|
+
sessionParent,
|
|
121
138
|
}, ledgerHome).recordFile;
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Discriminated owned-row view:
|
|
143
|
+
* - unknown run → file absent + empty owned (read APIs only)
|
|
144
|
+
* - located ledger → file is string; prior/write consumers may build RecordPointer
|
|
145
|
+
*/
|
|
146
|
+
type OwnedSubmissionRecords =
|
|
147
|
+
| { readonly file: undefined; readonly owned: readonly [] }
|
|
148
|
+
| { readonly file: string; readonly owned: readonly SitianRecord[] };
|
|
149
|
+
|
|
150
|
+
async function readOwnedSubmissionRecords(
|
|
151
|
+
cwd: string,
|
|
152
|
+
runId: string,
|
|
153
|
+
scope: SubmissionLedgerReadScope = {},
|
|
154
|
+
): Promise<OwnedSubmissionRecords> {
|
|
155
|
+
const file = await submissionRecordFile(cwd, runId, scope);
|
|
156
|
+
if (file === undefined) {
|
|
157
|
+
return { file: undefined, owned: [] };
|
|
158
|
+
}
|
|
126
159
|
const { records } = await readSitianRecords(file);
|
|
127
160
|
return {
|
|
128
161
|
file,
|
|
129
|
-
owned: records.filter(
|
|
162
|
+
owned: records.filter(
|
|
163
|
+
(record) =>
|
|
164
|
+
typeof record.subject === "object"
|
|
165
|
+
&& record.subject !== null
|
|
166
|
+
&& (record.subject as { runId?: string }).runId === runId,
|
|
167
|
+
),
|
|
130
168
|
};
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
/** Optional court-turn scope for settlement reads (#637). */
|
|
134
172
|
export type SubmissionLedgerReadScope = {
|
|
135
173
|
readonly home?: string;
|
|
174
|
+
/** Durable run principal; the direct ownership coordinate when already known. */
|
|
175
|
+
readonly sessionParent?: string;
|
|
136
176
|
/**
|
|
137
177
|
* Recording tag for this court. Presentation of original payloads is run-scoped
|
|
138
178
|
* (#836: attempt/latest must not hide already-recorded rows).
|
|
@@ -184,9 +224,9 @@ export async function hasFreshAttemptSubmission(
|
|
|
184
224
|
cwd: string,
|
|
185
225
|
runId: string,
|
|
186
226
|
attemptId: string,
|
|
187
|
-
|
|
227
|
+
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
188
228
|
): Promise<boolean> {
|
|
189
|
-
const { owned } = await readOwnedSubmissionRecords(cwd, runId,
|
|
229
|
+
const { owned } = await readOwnedSubmissionRecords(cwd, runId, resolveReadScope(homeOrScope));
|
|
190
230
|
return owned.some((record) => {
|
|
191
231
|
if (recordAttemptId(record) !== attemptId) return false;
|
|
192
232
|
if (record.kind === "sealed") return true;
|
|
@@ -374,7 +414,7 @@ export async function readRecordedSubmissionRows(
|
|
|
374
414
|
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
375
415
|
): Promise<readonly RecordedSubmissionRow[]> {
|
|
376
416
|
const scope = resolveReadScope(homeOrScope);
|
|
377
|
-
const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope
|
|
417
|
+
const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope);
|
|
378
418
|
const scoped = recordsForAttempt(owned, scope.attemptId);
|
|
379
419
|
return mapOwnedToSubmissionRows(scoped);
|
|
380
420
|
}
|
|
@@ -391,7 +431,11 @@ export async function readAttemptScopedSubmissionRows(
|
|
|
391
431
|
home?: string,
|
|
392
432
|
): Promise<readonly RecordedSubmissionRow[]> {
|
|
393
433
|
if (attemptId.length === 0) return [];
|
|
394
|
-
const { owned } = await readOwnedSubmissionRecords(
|
|
434
|
+
const { owned } = await readOwnedSubmissionRecords(
|
|
435
|
+
cwd,
|
|
436
|
+
runId,
|
|
437
|
+
home === undefined ? {} : { home },
|
|
438
|
+
);
|
|
395
439
|
return mapOwnedToSubmissionRows(owned.filter((record) => recordAttemptId(record) === attemptId));
|
|
396
440
|
}
|
|
397
441
|
|
|
@@ -425,7 +469,7 @@ export async function readLatestSubmissionOutcome(
|
|
|
425
469
|
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
426
470
|
): Promise<LatestSubmissionOutcome | undefined> {
|
|
427
471
|
const scope = resolveReadScope(homeOrScope);
|
|
428
|
-
const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope
|
|
472
|
+
const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope);
|
|
429
473
|
const scoped = recordsForAttempt(owned, scope.attemptId);
|
|
430
474
|
for (let index = scoped.length - 1; index >= 0; index -= 1) {
|
|
431
475
|
const record = scoped[index];
|
|
@@ -440,14 +484,35 @@ export async function readLatestSubmissionOutcome(
|
|
|
440
484
|
|
|
441
485
|
type LedgerState = { prior?: RecordPointer; sequence: number };
|
|
442
486
|
|
|
443
|
-
async function restoreState(cwd: string, runId: string,
|
|
444
|
-
const
|
|
487
|
+
async function restoreState(cwd: string, runId: string, scope: SubmissionLedgerReadScope): Promise<LedgerState> {
|
|
488
|
+
const located = await readOwnedSubmissionRecords(cwd, runId, scope);
|
|
489
|
+
// Unknown-run empty set: no prior chain. Owned rows without a file are an internal bug.
|
|
490
|
+
if (located.file === undefined) {
|
|
491
|
+
if (located.owned.length !== 0) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`submission ledger invariant: owned rows present without recordFile for run ${runId}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return { sequence: 0 };
|
|
497
|
+
}
|
|
498
|
+
const { file, owned } = located;
|
|
445
499
|
const last = owned.at(-1);
|
|
446
500
|
return {
|
|
447
|
-
...(last === undefined
|
|
501
|
+
...(last === undefined
|
|
502
|
+
? {}
|
|
503
|
+
: {
|
|
504
|
+
prior: {
|
|
505
|
+
identity: last.identity,
|
|
506
|
+
recordFile: file,
|
|
507
|
+
kind: last.kind,
|
|
508
|
+
level: last.level,
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
448
511
|
sequence: owned.reduce((maximum, record) => {
|
|
449
512
|
const payload = record.payload as Partial<SubmissionLedgerEvent> | undefined;
|
|
450
|
-
return payload?.type === "candidate" && typeof payload.sequence === "number"
|
|
513
|
+
return payload?.type === "candidate" && typeof payload.sequence === "number"
|
|
514
|
+
? Math.max(maximum, payload.sequence)
|
|
515
|
+
: maximum;
|
|
451
516
|
}, 0),
|
|
452
517
|
};
|
|
453
518
|
}
|
|
@@ -466,21 +531,38 @@ export function createSubmissionLedgerHost(
|
|
|
466
531
|
options?: { home?: string },
|
|
467
532
|
): RoleHost {
|
|
468
533
|
const states = new Map<string, Promise<LedgerState>>();
|
|
534
|
+
/** Sole HostContext-derived run coordinate for restore and append (never process.env). */
|
|
535
|
+
const sessionParentFromContext = (context: HostContext): string | undefined => {
|
|
536
|
+
const runDirectory = runDirectoryFromHostContext(context);
|
|
537
|
+
if (runDirectory !== undefined) {
|
|
538
|
+
return join(runDirectory, "session", "session.jsonl");
|
|
539
|
+
}
|
|
540
|
+
const sessionFile = context.sessionManager.getSessionFile?.();
|
|
541
|
+
if (typeof sessionFile === "string" && sessionFile.length > 0) return sessionFile;
|
|
542
|
+
const sessionDir = context.sessionManager.getSessionDir?.();
|
|
543
|
+
if (typeof sessionDir === "string" && sessionDir.length > 0) {
|
|
544
|
+
return join(sessionDir, "session.jsonl");
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
};
|
|
469
548
|
const resolveHomeFromContext = (context: HostContext): string | undefined => {
|
|
470
549
|
if (options?.home !== undefined) return options.home;
|
|
471
|
-
const
|
|
472
|
-
return
|
|
473
|
-
? tryHomeFromAkRolesPath(sessionFile)
|
|
474
|
-
: undefined;
|
|
550
|
+
const sessionParent = sessionParentFromContext(context);
|
|
551
|
+
return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
|
|
475
552
|
};
|
|
476
553
|
const stateFor = (context: HostContext, runId: string) => states.get(runId) ?? (() => {
|
|
477
554
|
const home = resolveHomeFromContext(context);
|
|
478
|
-
const
|
|
555
|
+
const sessionParent = sessionParentFromContext(context);
|
|
556
|
+
const pending = restoreState(context.cwd, runId, {
|
|
557
|
+
...(home === undefined ? {} : { home }),
|
|
558
|
+
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
559
|
+
});
|
|
479
560
|
states.set(runId, pending);
|
|
480
561
|
return pending;
|
|
481
562
|
})();
|
|
482
563
|
const appendFor = (state: LedgerState, context: HostContext, runId: string, attemptId: string, event: SubmissionLedgerEvent): RecordPointer => {
|
|
483
564
|
const home = resolveHomeFromContext(context);
|
|
565
|
+
const sessionParent = sessionParentFromContext(context);
|
|
484
566
|
const pointer = sitianReport({
|
|
485
567
|
level: "event",
|
|
486
568
|
kind: event.type,
|
|
@@ -490,6 +572,7 @@ export function createSubmissionLedgerHost(
|
|
|
490
572
|
source: "role-runtime",
|
|
491
573
|
cwd: context.cwd,
|
|
492
574
|
...(home !== undefined ? { home } : {}),
|
|
575
|
+
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
493
576
|
});
|
|
494
577
|
state.prior = pointer;
|
|
495
578
|
return pointer;
|
package/src/ticket-provenance.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type RecordPointer,
|
|
14
14
|
type SitianRecord,
|
|
15
15
|
} from "./sitian-facade.ts";
|
|
16
|
+
import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
|
|
16
17
|
import {
|
|
17
18
|
TICKET_PROVENANCE_HUMAN_VIEW,
|
|
18
19
|
TICKET_PROVENANCE_KIND,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
|
|
26
27
|
/** Subject string for ticket-keyed volumes — history follows the ticket. */
|
|
27
28
|
export function ticketProvenanceSubject(ticketNumber: number): string {
|
|
28
|
-
if (!
|
|
29
|
+
if (!isSafePositiveTicketNumber(ticketNumber)) {
|
|
29
30
|
throw new Error(`ticket-provenance subject requires a positive ticket number, got ${String(ticketNumber)}`);
|
|
30
31
|
}
|
|
31
32
|
return String(ticketNumber);
|
|
@@ -57,6 +58,7 @@ export function ticketProvenanceEntryIdentity(
|
|
|
57
58
|
export type AppendTicketProvenanceInput = {
|
|
58
59
|
readonly ticketNumber: number;
|
|
59
60
|
readonly cwd: string;
|
|
61
|
+
readonly sessionParent: string;
|
|
60
62
|
/** Original diarist row (or `{ original, unprojected: true }` envelope). */
|
|
61
63
|
readonly payload: unknown;
|
|
62
64
|
/** Explicit package home (tests / admitted run); never process.env.HOME (#604). */
|
|
@@ -107,6 +109,7 @@ export function appendTicketProvenanceEntry(
|
|
|
107
109
|
identity,
|
|
108
110
|
subject,
|
|
109
111
|
cwd: input.cwd,
|
|
112
|
+
sessionParent: input.sessionParent,
|
|
110
113
|
...(input.home === undefined ? {} : { home: input.home }),
|
|
111
114
|
host: input.host ?? "diarist",
|
|
112
115
|
source: input.source ?? "diarist",
|
package/src/ticket-trajectory.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
formatTokensCompact,
|
|
30
30
|
formatUsdPrecise,
|
|
31
31
|
} from "./human-format.ts";
|
|
32
|
+
import { listBookRunDirectories } from "./role-run-placement.ts";
|
|
33
|
+
import { readRunTicketNumber } from "./run-ticket-number.ts";
|
|
32
34
|
import {
|
|
33
35
|
extractSessionTimestampSpan,
|
|
34
36
|
readLedgerSessionJsonl,
|
|
@@ -302,16 +304,12 @@ async function readInvocation(runDir: string): Promise<InvocationInfo | undefine
|
|
|
302
304
|
info.model = rawModel;
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
|
-
if (
|
|
306
|
-
typeof parsed.ticketNumber === "number" &&
|
|
307
|
-
Number.isInteger(parsed.ticketNumber) &&
|
|
308
|
-
parsed.ticketNumber >= 1
|
|
309
|
-
) {
|
|
310
|
-
info.ticketNumber = parsed.ticketNumber;
|
|
311
|
-
}
|
|
312
307
|
if (typeof parsed.correlationId === "string" && parsed.correlationId.trim() !== "") {
|
|
313
308
|
info.correlationId = parsed.correlationId;
|
|
314
309
|
}
|
|
310
|
+
// Display placement: board pages first, then migration-derived (never forges board).
|
|
311
|
+
const ticketNumber = await readRunTicketNumber(runDir);
|
|
312
|
+
if (ticketNumber !== undefined) info.ticketNumber = ticketNumber;
|
|
315
313
|
return info;
|
|
316
314
|
} catch (error) {
|
|
317
315
|
// Only genuine absence activates the invocation fallback. Malformed JSON and
|
|
@@ -656,56 +654,47 @@ ${stationBlocks || "<p data-empty=\"true\">no runs</p>"}
|
|
|
656
654
|
* Load one ticket's runs via the S1 tracer path (read-only ledger scan).
|
|
657
655
|
* Factory board reuses this — no parallel receipt parser.
|
|
658
656
|
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
657
|
+
* Book-run attribution (#176 / #859): read typed `ticketNumber` from each run's
|
|
658
|
+
* invocation.json under listBookRunDirectories (flat legacy `runs/` + subject-tree
|
|
659
|
+
* `<ticket|unbound>/runs/`). Legacy `issues/<N>/runs` remains a read-only
|
|
660
|
+
* compatibility entrance. Unbound runs never join a ticket.
|
|
662
661
|
*/
|
|
663
662
|
|
|
664
|
-
/** One
|
|
663
|
+
/** One book run's typed binding projection from invocation.json. */
|
|
665
664
|
export type FlatRunTicketBinding = {
|
|
666
665
|
readonly runDir: string;
|
|
667
666
|
readonly runFolder: string;
|
|
667
|
+
/** Book-relative coord (e.g. `runs/id@role` or `582/runs/id@role`). */
|
|
668
|
+
readonly ledgerCoord: string;
|
|
668
669
|
readonly ticketNumber?: number;
|
|
669
670
|
readonly role?: string;
|
|
670
671
|
readonly correlationId?: string;
|
|
671
672
|
};
|
|
672
673
|
|
|
673
674
|
/**
|
|
674
|
-
* Book-level
|
|
675
|
-
* Pure derived view of
|
|
675
|
+
* Book-level run index built once per lane/render and reused per ticket.
|
|
676
|
+
* Pure derived view of book runs' invocation.json files — not a parallel ledger.
|
|
676
677
|
*/
|
|
677
678
|
export type TicketTrajectoryBookIndex = {
|
|
678
|
-
/** ticket number →
|
|
679
|
+
/** ticket number → runs bound via invocation.json ticketNumber. */
|
|
679
680
|
readonly runsByTicket: ReadonlyMap<number, readonly FlatRunTicketBinding[]>;
|
|
680
|
-
/**
|
|
681
|
+
/** Runs with no typed ticketNumber (unknown/unbound seam). */
|
|
681
682
|
readonly unboundRuns: readonly FlatRunTicketBinding[];
|
|
682
683
|
};
|
|
683
684
|
|
|
684
|
-
async function listFlatRunDirectories(ledgerDir: string): Promise<string[]> {
|
|
685
|
-
const runsDir = join(resolve(ledgerDir), "runs");
|
|
686
|
-
try {
|
|
687
|
-
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
688
|
-
return entries
|
|
689
|
-
.filter((entry) => entry.isDirectory())
|
|
690
|
-
.map((entry) => join(runsDir, entry.name))
|
|
691
|
-
.sort();
|
|
692
|
-
} catch (error) {
|
|
693
|
-
if (isMissingPathError(error)) return [];
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
685
|
export async function buildTicketTrajectoryBookIndex(
|
|
699
686
|
ledgerDir: string,
|
|
700
687
|
): Promise<TicketTrajectoryBookIndex> {
|
|
701
688
|
const root = resolve(ledgerDir);
|
|
702
689
|
const runsByTicket = new Map<number, FlatRunTicketBinding[]>();
|
|
703
690
|
const unboundRuns: FlatRunTicketBinding[] = [];
|
|
704
|
-
for (const runDir of await
|
|
691
|
+
for (const runDir of await listBookRunDirectories(root)) {
|
|
705
692
|
const invocation = await readInvocation(runDir);
|
|
693
|
+
const rel = relative(root, runDir).split(sep).join("/");
|
|
706
694
|
const binding: FlatRunTicketBinding = {
|
|
707
695
|
runDir,
|
|
708
696
|
runFolder: basename(runDir),
|
|
697
|
+
ledgerCoord: rel,
|
|
709
698
|
...(invocation?.ticketNumber !== undefined
|
|
710
699
|
? { ticketNumber: invocation.ticketNumber }
|
|
711
700
|
: {}),
|
|
@@ -744,8 +733,7 @@ export async function loadUnboundTrajectoryRuns(
|
|
|
744
733
|
const index = bookIndex ?? (await buildTicketTrajectoryBookIndex(root));
|
|
745
734
|
const out: UnboundTrajectoryRun[] = [];
|
|
746
735
|
for (const binding of index.unboundRuns) {
|
|
747
|
-
const
|
|
748
|
-
const run = await parseRunDirectory(binding.runDir, ledgerCoord);
|
|
736
|
+
const run = await parseRunDirectory(binding.runDir, binding.ledgerCoord);
|
|
749
737
|
out.push({
|
|
750
738
|
run,
|
|
751
739
|
role: binding.role ?? run.station,
|
|
@@ -781,14 +769,13 @@ export async function loadTicketTrajectoryRuns(
|
|
|
781
769
|
runs.push(parsed);
|
|
782
770
|
}
|
|
783
771
|
|
|
784
|
-
//
|
|
772
|
+
// Book runs (flat + subject-tree) bound by typed invocation.json ticketNumber.
|
|
785
773
|
const flatBindings = index.runsByTicket.get(issueNumber) ?? [];
|
|
786
774
|
for (const binding of flatBindings) {
|
|
787
775
|
const resolvedRunDir = resolve(binding.runDir);
|
|
788
776
|
if (seenRunDirs.has(resolvedRunDir)) continue;
|
|
789
777
|
seenRunDirs.add(resolvedRunDir);
|
|
790
|
-
|
|
791
|
-
runs.push(await parseRunDirectory(binding.runDir, ledgerCoord));
|
|
778
|
+
runs.push(await parseRunDirectory(binding.runDir, binding.ledgerCoord));
|
|
792
779
|
}
|
|
793
780
|
|
|
794
781
|
return runs;
|
package/src/worker-role.ts
CHANGED
|
@@ -145,8 +145,8 @@ export type CoderRoleDependencies = {
|
|
|
145
145
|
|
|
146
146
|
export type WorkerRoleRuntime = {
|
|
147
147
|
activate(ctx?: HostContext): Promise<void>;
|
|
148
|
-
/** Arm gate ① baseline after envelope places the worktree (coder/fixer).
|
|
149
|
-
armSubmissionGate(cwd: string, parent
|
|
148
|
+
/** Arm gate ① baseline after envelope places the worktree (coder/fixer). Durable parent required (#857). */
|
|
149
|
+
armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }): void;
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -328,7 +328,7 @@ export function createFixerRoleRuntime(
|
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
330
|
},
|
|
331
|
-
armSubmissionGate(cwd: string, parent
|
|
331
|
+
armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }) {
|
|
332
332
|
submissionGate.arm(cwd, parent);
|
|
333
333
|
},
|
|
334
334
|
};
|
|
@@ -477,7 +477,7 @@ export function createCoderRoleRuntime(
|
|
|
477
477
|
});
|
|
478
478
|
}
|
|
479
479
|
},
|
|
480
|
-
armSubmissionGate(cwd: string, parent
|
|
480
|
+
armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }) {
|
|
481
481
|
submissionGate.arm(cwd, parent);
|
|
482
482
|
},
|
|
483
483
|
};
|
|
@@ -221,9 +221,8 @@ function reliableWindow(
|
|
|
221
221
|
|
|
222
222
|
export type CreateWorkerSubmissionGateOptions = {
|
|
223
223
|
/**
|
|
224
|
-
* Explicit package home for sitian writes
|
|
225
|
-
*
|
|
226
|
-
* (path-derive from parent, else package machine home). #604 Scope 2.
|
|
224
|
+
* Explicit package home for sitian writes (tests / admitted run).
|
|
225
|
+
* Path still derives from required parent session; home only pins ledger root. #604 Scope 2.
|
|
227
226
|
*/
|
|
228
227
|
readonly home?: string;
|
|
229
228
|
};
|
|
@@ -231,7 +230,8 @@ export type CreateWorkerSubmissionGateOptions = {
|
|
|
231
230
|
export function createWorkerSubmissionGate(
|
|
232
231
|
options: CreateWorkerSubmissionGateOptions = {},
|
|
233
232
|
): {
|
|
234
|
-
arm(
|
|
233
|
+
/** Durable parent is required — ownership must be known at arm (#857 loud failure). */
|
|
234
|
+
arm(cwd: string, parent: WorkerSubmissionGateParent): void;
|
|
235
235
|
assertAcceptable(status: string, details?: unknown): void;
|
|
236
236
|
} {
|
|
237
237
|
let baseline: string | null | undefined;
|
|
@@ -267,11 +267,11 @@ export function createWorkerSubmissionGate(
|
|
|
267
267
|
arm(cwd, parent) {
|
|
268
268
|
uninstallPackageWorkerHooks(cwd);
|
|
269
269
|
root = cwd;
|
|
270
|
-
sessionParent = parent
|
|
270
|
+
sessionParent = parent.getSessionFile();
|
|
271
271
|
record = createRecordSession({
|
|
272
272
|
cwd,
|
|
273
273
|
kind: WORKER_SUBMISSION_GATE_RECORD_KIND,
|
|
274
|
-
|
|
274
|
+
parent,
|
|
275
275
|
});
|
|
276
276
|
const prior = readGateState(record);
|
|
277
277
|
if (prior.baseline !== undefined) {
|