@akagilnc/pi-workflow-roles 0.1.5775 → 0.1.5797
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 +556 -209
- package/dist/external-host-turn-loop.js +3 -0
- package/dist/headless-host/production-host.js +529 -182
- package/dist/migrate-book-topology.js +63 -23
- package/dist/pi/role-turn-host.js +6 -1
- package/dist/public-cli/instruction-seat-run.js +10 -7
- package/dist/public-cli/invocation.js +95 -39
- package/dist/public-cli/main.js +357 -147
- package/dist/public-cli/post-admission.js +2 -0
- package/dist/public-cli/run-lifecycle.js +10 -4
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/report-ticket-tool.js +3 -3
- package/dist/role-run-placement.js +14 -2
- package/dist/role-run-relocation.js +2 -2
- package/dist/run-dossier.js +5 -4
- package/dist/session-identity.js +3 -2
- package/dist/sitian-appender.js +12 -3
- package/dist/submission-ledger.js +24 -0
- package/dist/ticket-progress.js +306 -0
- package/dist/user-dialogue-stdin.js +23 -17
- package/package.json +1 -1
- package/src/external-host-turn-loop.ts +7 -1
- package/src/host-contracts.ts +1 -1
- package/src/pi/adapter.ts +57 -45
- package/src/pi/role-turn-host.ts +6 -1
- package/src/prepared-role-turn.ts +6 -0
- package/src/public-cli/instruction-seat-run.ts +10 -7
- package/src/public-cli/invocation.ts +116 -38
- package/src/public-cli/post-admission.ts +3 -1
- package/src/public-cli/run-lifecycle.ts +10 -8
- package/src/public-cli/turn-request.ts +3 -0
- package/src/report-ticket-tool.ts +7 -3
- package/src/role-envelope.ts +5 -0
- package/src/role-run-placement.ts +19 -2
- package/src/role-run-relocation.ts +2 -2
- package/src/role-runtime.ts +14 -3
- package/src/run-dossier.ts +5 -3
- package/src/run-terminal-artifacts.ts +0 -1
- package/src/session-identity.ts +3 -2
- package/src/sitian-appender.ts +14 -3
- package/src/submission-ledger.ts +27 -0
- package/src/ticket-progress.ts +383 -0
- package/src/user-dialogue-stdin.ts +21 -14
|
@@ -408,6 +408,7 @@ function settlementScopeForTurn(request) {
|
|
|
408
408
|
/** Bare in-call催交 resume: hostSessionId absent; adapter is the sole load authority. */
|
|
409
409
|
function buildReceiptDeliveryRequest(input) {
|
|
410
410
|
const { request, receiptDelivery } = input;
|
|
411
|
+
// #1199: progress instruction is this turn's prompt (envelope books continuation.prompt).
|
|
411
412
|
return {
|
|
412
413
|
...request,
|
|
413
414
|
continuation: {
|
|
@@ -1070,6 +1071,7 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
|
|
|
1070
1071
|
const officerDialogue = isStationChildOfficerDialogue(admitted.role, env);
|
|
1071
1072
|
const fileFlags = summonsPrepared?.attachments ?? [];
|
|
1072
1073
|
let prompt;
|
|
1074
|
+
// #1199: progress instruction = this prompt (actual seat input, file flags included).
|
|
1073
1075
|
if (request.message !== undefined) {
|
|
1074
1076
|
if (summonsPrepared !== undefined) {
|
|
1075
1077
|
// #755: same-ticket review / open-court — caller words + caller file flags.
|
|
@@ -13,7 +13,7 @@ import { findRoleRunDirectory, listBookRunDirectories, parseRunLeaf, sessionFile
|
|
|
13
13
|
import { isSafePositiveTicketNumber } from "../run-ticket-number.js";
|
|
14
14
|
import { CliUsageError } from "./cli-errors.js";
|
|
15
15
|
import { parseCollectorRepository } from "../collector-config.js";
|
|
16
|
-
import { retainedRunPathsMatch, rewriteAdmittedRoleRunPage,
|
|
16
|
+
import { retainedRunPathsMatch, rewriteAdmittedRoleRunPage, rewritePrincipalSessionPaths, } from "../role-run-relocation.js";
|
|
17
17
|
import { packagedResumeSourcePath, packagedRoleMetadata, } from "../packaged-role-registry.js";
|
|
18
18
|
import { admittedReviewerBaseFault, admittedSourceRunPath, recordEffectiveInvocationModel, requireAuthorityRef, isReviewerLens, normalizeAdmittedAttachment, } from "./invocation.js";
|
|
19
19
|
import { readPageSync, writeSectionSync } from "../run-dossier.js";
|
|
@@ -136,10 +136,16 @@ async function readRoleRunStateDisk(runDirectory) {
|
|
|
136
136
|
: runDirectory;
|
|
137
137
|
const runDir = runDirectory;
|
|
138
138
|
// Principal wire stays uninterpreted — authority owns legacy sessionFile fallback.
|
|
139
|
+
// #1199: leg-local session/ moves with the run-prefix rewrite.
|
|
140
|
+
const principalCoords = {
|
|
141
|
+
sessionDirectory: record.sessionDirectory,
|
|
142
|
+
...(typeof record.sessionFile === "string" ? { sessionFile: record.sessionFile } : {}),
|
|
143
|
+
};
|
|
144
|
+
rewritePrincipalSessionPaths(principalCoords, storedRunDirectory, runDirectory);
|
|
139
145
|
const principalWire = {
|
|
140
|
-
sessionDirectory:
|
|
141
|
-
...(typeof
|
|
142
|
-
? { sessionFile:
|
|
146
|
+
sessionDirectory: principalCoords.sessionDirectory,
|
|
147
|
+
...(typeof principalCoords.sessionFile === "string"
|
|
148
|
+
? { sessionFile: principalCoords.sessionFile }
|
|
143
149
|
: {}),
|
|
144
150
|
};
|
|
145
151
|
const phase = record.phase === "plan" || record.phase === "apply"
|
|
@@ -51,6 +51,9 @@ export function projectRoleTurnRequest(admitted, roleDetails, options) {
|
|
|
51
51
|
...(options.deliveryRequestLimit === undefined
|
|
52
52
|
? {}
|
|
53
53
|
: { deliveryRequestLimit: options.deliveryRequestLimit }),
|
|
54
|
+
...(options.host === undefined || options.host.trim() === ""
|
|
55
|
+
? {}
|
|
56
|
+
: { host: options.host.trim() }),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
@@ -102,6 +102,7 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
|
|
|
102
102
|
if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
|
|
103
103
|
process.env.AK_ROLE_RUN_DIR = nextDirectory;
|
|
104
104
|
}
|
|
105
|
+
// Leg-local session/: rewrite the open handle under the renamed run prefix.
|
|
105
106
|
const projectedSessionFile = typeof priorSessionFile === "string" && priorSessionFile.trim() !== ""
|
|
106
107
|
? rewriteRunDirectoryPathValue(priorSessionFile, runDirectory, nextDirectory)
|
|
107
108
|
: sessionFileOf(nextDirectory);
|
|
@@ -109,9 +110,8 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
|
|
|
109
110
|
context.sessionManager.setSessionFile?.(projectedSessionFile);
|
|
110
111
|
}
|
|
111
112
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: await relocateAdmittedRunToTicket(admitted, authority, { relocate: applyLiveHandles });
|
|
113
|
+
// Always enter relocate: unbound→rename+publish; already-on-ticket→publish leftover staging.
|
|
114
|
+
const relocation = await relocateAdmittedRunToTicket(admitted, authority, alreadyPlaced ? undefined : { relocate: applyLiveHandles });
|
|
115
115
|
const nextDirectory = relocation?.newRunDirectory ?? admitted.runDirectory;
|
|
116
116
|
if (relocation === undefined)
|
|
117
117
|
applyLiveHandles(nextDirectory);
|
|
@@ -14,10 +14,20 @@ function parseRunLeaf(name) {
|
|
|
14
14
|
if (at <= 0 || at === name.length - 1) return void 0;
|
|
15
15
|
return { runId: name.slice(0, at), role: name.slice(at + 1) };
|
|
16
16
|
}
|
|
17
|
+
const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
|
|
18
|
+
function subjectDirectoryOfRun(runDirectory) {
|
|
19
|
+
const runsParent = dirname(runDirectory);
|
|
20
|
+
if (basename(runsParent) !== "runs") return void 0;
|
|
21
|
+
const subjectDirectory = dirname(runsParent);
|
|
22
|
+
const subjectName = basename(subjectDirectory);
|
|
23
|
+
if (subjectName === "unbound" || /^[1-9]\d*$/.test(subjectName)) {
|
|
24
|
+
return subjectDirectory;
|
|
25
|
+
}
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
17
28
|
function sessionDirectoryOf(runDirectory) {
|
|
18
29
|
return join(runDirectory, "session");
|
|
19
30
|
}
|
|
20
|
-
const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
|
|
21
31
|
function sessionFileIn(sessionDirectory) {
|
|
22
32
|
return join(sessionDirectory, ROLE_RUN_SESSION_FILENAME);
|
|
23
33
|
}
|
|
@@ -107,6 +117,7 @@ function ensureRoleRunDirectory(ledgerHome, directory) {
|
|
|
107
117
|
return ensureRealDirectoryTree(ledgerHome, directory);
|
|
108
118
|
}
|
|
109
119
|
function ensureRoleRunPlacement(ledgerHome, placement) {
|
|
120
|
+
ensureRoleRunDirectory(ledgerHome, placement.runDirectory);
|
|
110
121
|
ensureRoleRunDirectory(ledgerHome, placement.sessionDirectory);
|
|
111
122
|
}
|
|
112
123
|
export {
|
|
@@ -124,5 +135,6 @@ export {
|
|
|
124
135
|
runDirectoryOfSessionFile,
|
|
125
136
|
sessionDirectoryOf,
|
|
126
137
|
sessionFileIn,
|
|
127
|
-
sessionFileOf
|
|
138
|
+
sessionFileOf,
|
|
139
|
+
subjectDirectoryOfRun
|
|
128
140
|
};
|
|
@@ -12,7 +12,7 @@ import { dirname, join } from "node:path";
|
|
|
12
12
|
import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.js";
|
|
13
13
|
import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
|
|
14
14
|
import { readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
|
|
15
|
-
import { roleRunPlacement } from "./role-run-placement.js";
|
|
15
|
+
import { roleRunPlacement, sessionDirectoryOf } from "./role-run-placement.js";
|
|
16
16
|
import { INVOCATION_PAGE_PATH_FIELDS, PRINCIPAL_SESSION_PATH_FIELDS, RUN_STATE_PAGE_PATH_FIELDS, projectTurnRequestLiveRunDirectory, rewriteAdmittedRoleRunPage, rewritePrincipalSessionPaths, rewriteRunDirectoryPathFields, rewriteRunDirectoryPathFieldsAgainstRewrites, rewriteRunDirectoryPathValue, rewriteRunDirectoryPathValueAgainstRewrites, rewriteSummonsMaterials, } from "./role-run-path-rewrite.js";
|
|
17
17
|
import { readPageSync, updateSectionSync } from "./run-dossier.js";
|
|
18
18
|
import { isEnoent, isRecord } from "./unknown-value.js";
|
|
@@ -215,7 +215,7 @@ async function rewriteSessionTranscriptBindings(path, rewrites) {
|
|
|
215
215
|
* session transcript typed parent bindings. Never walks caller file paths.
|
|
216
216
|
*/
|
|
217
217
|
async function rewriteNestedMachinePathPages(pagesDirectory, rewrites) {
|
|
218
|
-
const sessionRoot =
|
|
218
|
+
const sessionRoot = sessionDirectoryOf(pagesDirectory);
|
|
219
219
|
async function walk(directory) {
|
|
220
220
|
let entries;
|
|
221
221
|
try {
|
package/dist/run-dossier.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { basename, join } from "node:path";
|
|
22
|
-
import { sessionFileOf } from "./role-run-placement.js";
|
|
22
|
+
import { sessionDirectoryOf, sessionFileOf } from "./role-run-placement.js";
|
|
23
23
|
import { projectRenderedPagePaths } from "./role-run-path-rewrite.js";
|
|
24
24
|
import { RUN_CURRENT_FILE, RUN_HISTORY_FILE, RUN_LOG_FILE, RUN_STATE_FILE, OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.js";
|
|
25
25
|
import { appendSitianRecord } from "./sitian-appender.js";
|
|
@@ -181,9 +181,10 @@ function render(runDirectory, history, state, log, unreadable) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
// The copy row keeps the absolute path it was written to; a run that was filed under its ticket
|
|
184
|
-
// since lives elsewhere. The original is projected from where this run
|
|
185
|
-
if (typeof original === "string")
|
|
186
|
-
original = join(runDirectory,
|
|
184
|
+
// since lives elsewhere. The original is projected from where this run's session lands now.
|
|
185
|
+
if (typeof original === "string") {
|
|
186
|
+
original = join(sessionDirectoryOf(runDirectory), basename(original));
|
|
187
|
+
}
|
|
187
188
|
whole.host = { sessions, ...(original === undefined ? {} : { original }) };
|
|
188
189
|
whole.submission = latest === undefined ? {} : { latest };
|
|
189
190
|
if (Object.keys(unreadable).length > 0)
|
package/dist/session-identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
import { DEFAULT_ROLE_TURN_HOST, lookupHeadlessHostDescription, lookupHostDescription, } from "./host-descriptions.js";
|
|
3
|
+
import { runDirectoryFromSessionDirectory } from "./role-run-placement.js";
|
|
3
4
|
import { renderCurrentSync, RUN_STATE_FILE } from "./run-dossier.js";
|
|
4
5
|
import { readSitianRecords, reportRunRecord } from "./sitian-facade.js";
|
|
5
6
|
import { errorText, isRecord } from "./unknown-value.js";
|
|
@@ -14,7 +15,7 @@ export const HOST_SESSION_ID_RECORD_KIND = "host-session-id";
|
|
|
14
15
|
* host/session failure (facts stay in state.jsonl; settlement still renders).
|
|
15
16
|
*/
|
|
16
17
|
export function createSessionIdentityAuthority(authority, hostName) {
|
|
17
|
-
const runDirectoryOf = (principal) =>
|
|
18
|
+
const runDirectoryOf = (principal) => runDirectoryFromSessionDirectory(authority.decode(principal).sessionDirectory);
|
|
18
19
|
return {
|
|
19
20
|
principalAuthority: authority,
|
|
20
21
|
resolveSessionFile(principal) {
|
package/dist/sitian-appender.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { appendFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseRunLeaf,
|
|
6
|
+
sessionDirectoryOf
|
|
7
|
+
} from "./role-run-placement.js";
|
|
5
8
|
import { RUN_HISTORY_FILE, RUN_HISTORY_KINDS, RUN_LOG_FILE, RUN_STATE_FILE, RUN_STATE_KINDS } from "./run-dossier-files.js";
|
|
6
9
|
import { ticketNumberFromSitianSubject } from "./run-ticket-number.js";
|
|
7
10
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
@@ -33,6 +36,12 @@ function appendSitianRecordBlock(input, block) {
|
|
|
33
36
|
function resolveSitianVolumeCategory(kind) {
|
|
34
37
|
return kind;
|
|
35
38
|
}
|
|
39
|
+
function runDirectoryOwningSessionParent(sessionParentDir) {
|
|
40
|
+
if (basename(sessionParentDir) !== "session") return void 0;
|
|
41
|
+
const runDirectory = dirname(sessionParentDir);
|
|
42
|
+
if (parseRunLeaf(basename(runDirectory)) === void 0) return void 0;
|
|
43
|
+
return runDirectory;
|
|
44
|
+
}
|
|
36
45
|
const SITIAN_RECORDS_LEAF = "records.jsonl";
|
|
37
46
|
function sitianVolumeDirectory(sessionDirectory, category) {
|
|
38
47
|
return join(sessionDirectory, category);
|
|
@@ -68,8 +77,8 @@ function resolveSitianRecordPathInLedger(input, ledgerHome) {
|
|
|
68
77
|
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
69
78
|
}
|
|
70
79
|
const sessionParentDir = dirname(input.sessionParent);
|
|
71
|
-
const runDirectory =
|
|
72
|
-
if (
|
|
80
|
+
const runDirectory = runDirectoryOwningSessionParent(sessionParentDir);
|
|
81
|
+
if (runDirectory !== void 0) {
|
|
73
82
|
sessionDir = runDirectory;
|
|
74
83
|
recordFile = join(runDirectory, RUN_HISTORY_KINDS.has(input.kind) ? RUN_HISTORY_FILE : RUN_STATE_KINDS.has(input.kind) ? RUN_STATE_FILE : RUN_LOG_FILE);
|
|
75
84
|
} else {
|
|
@@ -9,6 +9,7 @@ import { failOnInfrastructureFailureDeclaration, infrastructureFailureDiagnostic
|
|
|
9
9
|
import { reportTicketFromHostContext } from "./report-ticket-tool.js";
|
|
10
10
|
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.js";
|
|
11
11
|
import { readDeclaredTicketNumber } from "./run-ticket-number.js";
|
|
12
|
+
import { captureWorktreeHead, landTicketProgressForSealedSubmission, } from "./ticket-progress.js";
|
|
12
13
|
import { isRecord, errorText } from "./unknown-value.js";
|
|
13
14
|
/**
|
|
14
15
|
* Admitted run identity for the ledger subject.
|
|
@@ -294,6 +295,8 @@ export async function sealAcceptedSubmission(options) {
|
|
|
294
295
|
const attemptId = attemptIdentity(options.context, runId);
|
|
295
296
|
const sessionParent = sessionParentFromHostContext(options.context);
|
|
296
297
|
const home = homeFromHostContext(options.context, options.home);
|
|
298
|
+
// #1199: same seal-time HEAD + progress landing as the terminating-tool wrap.
|
|
299
|
+
const sealHead = captureWorktreeHead(options.context.cwd);
|
|
297
300
|
sitianReport({
|
|
298
301
|
level: "event",
|
|
299
302
|
kind: "sealed",
|
|
@@ -310,6 +313,12 @@ export async function sealAcceptedSubmission(options) {
|
|
|
310
313
|
...(home !== undefined ? { home } : {}),
|
|
311
314
|
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
312
315
|
});
|
|
316
|
+
landTicketProgressForSealedSubmission({
|
|
317
|
+
context: options.context,
|
|
318
|
+
role: options.role,
|
|
319
|
+
accepted: options.accepted,
|
|
320
|
+
sealHead,
|
|
321
|
+
});
|
|
313
322
|
}
|
|
314
323
|
/**
|
|
315
324
|
* Submission ledger host — record only (#836).
|
|
@@ -457,6 +466,9 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
|
|
|
457
466
|
kind: "accepted",
|
|
458
467
|
accepted: params,
|
|
459
468
|
};
|
|
469
|
+
// #1199: seal-time HEAD + ticket progress before projectClosure.
|
|
470
|
+
// One seal → one row; post-turn history guessing is not used.
|
|
471
|
+
const sealHead = captureWorktreeHead(context.cwd);
|
|
460
472
|
append({
|
|
461
473
|
type: "sealed",
|
|
462
474
|
attemptId,
|
|
@@ -464,6 +476,18 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
|
|
|
464
476
|
role,
|
|
465
477
|
accepted: params,
|
|
466
478
|
});
|
|
479
|
+
const rawLens = typeof host.getFlag === "function"
|
|
480
|
+
? host.getFlag("ak-review-lens")
|
|
481
|
+
: undefined;
|
|
482
|
+
landTicketProgressForSealedSubmission({
|
|
483
|
+
context,
|
|
484
|
+
role,
|
|
485
|
+
accepted: params,
|
|
486
|
+
sealHead,
|
|
487
|
+
...(rawLens === "completeness" || rawLens === "correctness"
|
|
488
|
+
? { lens: rawLens }
|
|
489
|
+
: {}),
|
|
490
|
+
});
|
|
467
491
|
await projectClosure(closed, context);
|
|
468
492
|
return result;
|
|
469
493
|
},
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1199 ticket-level progress: current.jsonl + leg-local receipts/session.
|
|
3
|
+
* Write at seal (createSubmissionLedgerHost → projectClosure), never by
|
|
4
|
+
* post-turn history guessing. Per-leg dossiers stay until #1202.
|
|
5
|
+
*
|
|
6
|
+
* Originals stay in the leg directory (owner 14b58f6c / a8d2e72d):
|
|
7
|
+
* `runs/<leg>/receipts/<n>.json`, `runs/<leg>/session/<原件>`.
|
|
8
|
+
* Unbound legs stage rows in `progress.jsonl` (round empty); bind renames the
|
|
9
|
+
* whole leg, then this module fills rounds and appends ticket current.jsonl.
|
|
10
|
+
* Round authority is progress lines only — no receipt-filename counting,
|
|
11
|
+
* occupancy placeholders, or claim-retry loops. Reviewer dual-lens series
|
|
12
|
+
* count independently (`<n>.<lens>`).
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
17
|
+
import { resolveHostDossierLandingPath } from "./host-session-record.js";
|
|
18
|
+
import { runDirectoryFromHostContext } from "./host-contracts.js";
|
|
19
|
+
import { acceptedFacts, isTerminatingToolName, } from "./package-contracts/terminating-tools.js";
|
|
20
|
+
import { packagedRoleOutputTool } from "./packaged-role-registry.js";
|
|
21
|
+
import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf, subjectDirectoryOfRun, } from "./role-run-placement.js";
|
|
22
|
+
import { readPageSync } from "./run-dossier.js";
|
|
23
|
+
import { parseSitianRecordText } from "./sitian-reader.js";
|
|
24
|
+
import { isRecord } from "./unknown-value.js";
|
|
25
|
+
const CURRENT_FILENAME = "current.jsonl";
|
|
26
|
+
const RECEIPTS_DIRNAME = "receipts";
|
|
27
|
+
const LEG_PROGRESS_FILENAME = "progress.jsonl";
|
|
28
|
+
/**
|
|
29
|
+
* Append-only progress JSONL — same posture as run-dossier row files:
|
|
30
|
+
* sole Sitian decoder keeps reachable rows; malformed lines leave diagnostics
|
|
31
|
+
* on stderr and do not abort seal/relocate.
|
|
32
|
+
*/
|
|
33
|
+
function parseProgressText(text, pathForDiagnostic) {
|
|
34
|
+
if (text.trim() === "")
|
|
35
|
+
return [];
|
|
36
|
+
const { records, diagnostics } = parseSitianRecordText(text);
|
|
37
|
+
for (const diagnostic of diagnostics) {
|
|
38
|
+
process.stderr.write(`[ticket-progress] ${pathForDiagnostic} has malformed row(s); keeps reachable rows and retains the diagnostic: line ${diagnostic.line}: ${diagnostic.error}\n`);
|
|
39
|
+
}
|
|
40
|
+
const lines = [];
|
|
41
|
+
for (const record of records) {
|
|
42
|
+
const parsed = record;
|
|
43
|
+
if (!isRecord(parsed))
|
|
44
|
+
continue;
|
|
45
|
+
if (typeof parsed.seat !== "string" || typeof parsed.round !== "string")
|
|
46
|
+
continue;
|
|
47
|
+
lines.push(parsed);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
function readProgressFile(path) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return [];
|
|
54
|
+
return parseProgressText(readFileSync(path, "utf8"), path);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read progress rows under a subject.
|
|
58
|
+
* Ticket: append-only `current.jsonl`.
|
|
59
|
+
* Unbound: each leg's `runs/<leaf>/progress.jsonl` (no shared file).
|
|
60
|
+
*/
|
|
61
|
+
export function readTicketProgressLines(subjectDirectory) {
|
|
62
|
+
if (basename(subjectDirectory) === "unbound") {
|
|
63
|
+
const runsDir = join(subjectDirectory, "runs");
|
|
64
|
+
if (!existsSync(runsDir))
|
|
65
|
+
return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const name of readdirSync(runsDir).sort()) {
|
|
68
|
+
out.push(...readProgressFile(join(runsDir, name, LEG_PROGRESS_FILENAME)));
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return readProgressFile(join(subjectDirectory, CURRENT_FILENAME));
|
|
73
|
+
}
|
|
74
|
+
function roundBaseNumber(round) {
|
|
75
|
+
if (round.trim() === "")
|
|
76
|
+
return 0;
|
|
77
|
+
const base = round.includes(".") ? round.slice(0, round.indexOf(".")) : round;
|
|
78
|
+
const n = Number(base);
|
|
79
|
+
return Number.isSafeInteger(n) && n > 0 ? n : 0;
|
|
80
|
+
}
|
|
81
|
+
function roundLens(round) {
|
|
82
|
+
return round.includes(".") ? round.slice(round.indexOf(".") + 1) : undefined;
|
|
83
|
+
}
|
|
84
|
+
function nextRoundFromLines(lines, seat, lens) {
|
|
85
|
+
const want = lens === undefined || lens.trim() === "" ? undefined : lens;
|
|
86
|
+
let max = 0;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (line.seat !== seat)
|
|
89
|
+
continue;
|
|
90
|
+
if (roundLens(line.round) !== want)
|
|
91
|
+
continue;
|
|
92
|
+
max = Math.max(max, roundBaseNumber(line.round));
|
|
93
|
+
}
|
|
94
|
+
const base = max + 1;
|
|
95
|
+
return want === undefined ? String(base) : `${base}.${want}`;
|
|
96
|
+
}
|
|
97
|
+
/** Leg-local receipt ordinal: `receipts/<n>.json` (not seat#round). */
|
|
98
|
+
function nextLegReceiptIndex(runDirectory) {
|
|
99
|
+
const dir = join(runDirectory, RECEIPTS_DIRNAME);
|
|
100
|
+
if (!existsSync(dir))
|
|
101
|
+
return 1;
|
|
102
|
+
let max = 0;
|
|
103
|
+
for (const name of readdirSync(dir)) {
|
|
104
|
+
const match = /^(\d+)\.json$/.exec(name);
|
|
105
|
+
if (match === null)
|
|
106
|
+
continue;
|
|
107
|
+
const n = Number(match[1]);
|
|
108
|
+
if (Number.isSafeInteger(n) && n > max)
|
|
109
|
+
max = n;
|
|
110
|
+
}
|
|
111
|
+
return max + 1;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Copy structured status/summary from the seat receipt.
|
|
115
|
+
* Status leaves reuse acceptedFacts (terminating-tools authority) via the seat's
|
|
116
|
+
* current output tool — legacy judgeStatus/countersignStatus keys are unreachable
|
|
117
|
+
* at this seal seam (tool is the live registry name, not ak_judge_output).
|
|
118
|
+
* summary is seat-authored prose on the receipt and is not part of AcceptedFacts.
|
|
119
|
+
* Missing fields stay absent — never invent "" or "accepted".
|
|
120
|
+
*
|
|
121
|
+
* Not receiptStatusFromRegistry: that scans every registry key for navigator
|
|
122
|
+
* projection when the seat is unknown; here the seal already knows the seat.
|
|
123
|
+
*/
|
|
124
|
+
function statusAndSummaryFromPayload(seat, payload) {
|
|
125
|
+
if (!isRecord(payload))
|
|
126
|
+
return {};
|
|
127
|
+
const toolName = packagedRoleOutputTool(seat);
|
|
128
|
+
const status = toolName !== undefined && isTerminatingToolName(toolName)
|
|
129
|
+
? acceptedFacts(toolName, payload).status
|
|
130
|
+
: undefined;
|
|
131
|
+
const summary = typeof payload.summary === "string" ? payload.summary : undefined;
|
|
132
|
+
return {
|
|
133
|
+
...(status === undefined ? {} : { status }),
|
|
134
|
+
...(summary === undefined ? {} : { summary }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Worktree HEAD at seal. On failure head is empty and fault carries the true cause. */
|
|
138
|
+
export function captureWorktreeHead(projectRoot) {
|
|
139
|
+
try {
|
|
140
|
+
const result = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
141
|
+
cwd: projectRoot,
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
maxBuffer: 1024 * 1024,
|
|
144
|
+
});
|
|
145
|
+
if (result.error != null) {
|
|
146
|
+
const err = result.error;
|
|
147
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
148
|
+
return { head: "", fault: `git rev-parse HEAD spawn failed cwd=${projectRoot}: ${detail}` };
|
|
149
|
+
}
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
const detail = [result.stderr, result.stdout]
|
|
152
|
+
.map((part) => (typeof part === "string" ? part.trim() : ""))
|
|
153
|
+
.filter((part) => part !== "")
|
|
154
|
+
.join(" | ");
|
|
155
|
+
const status = result.status === null ? "null" : String(result.status);
|
|
156
|
+
const signal = result.signal == null ? "" : ` signal=${result.signal}`;
|
|
157
|
+
return {
|
|
158
|
+
head: "",
|
|
159
|
+
fault: detail === ""
|
|
160
|
+
? `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}`
|
|
161
|
+
: `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}: ${detail}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { head: (result.stdout ?? "").trim() };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
168
|
+
return { head: "", fault: `git rev-parse HEAD threw cwd=${projectRoot}: ${message}` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function progressSessionAbsolute(runDirectory, context) {
|
|
172
|
+
const host = typeof context.host === "string" ? context.host.trim() : "";
|
|
173
|
+
const sessionDirectory = sessionDirectoryOf(runDirectory);
|
|
174
|
+
// External hosts: ADR 0086 copy landing (codex.jsonl / grok-build / …), not the
|
|
175
|
+
// principal wire file. Responsibility unchanged (#1199 R3 disposition).
|
|
176
|
+
if (host !== "" && host !== "pi" && host !== "hermes") {
|
|
177
|
+
return resolveHostDossierLandingPath({ host, sessionDirectory });
|
|
178
|
+
}
|
|
179
|
+
// Pi/hermes: prefer the issued durable coordinate on the live handle.
|
|
180
|
+
const issued = context.sessionManager.getSessionFile?.();
|
|
181
|
+
if (typeof issued === "string" && issued.trim() !== "") {
|
|
182
|
+
return issued;
|
|
183
|
+
}
|
|
184
|
+
return sessionFileOf(runDirectory);
|
|
185
|
+
}
|
|
186
|
+
function lensFromRunDirectory(runDirectory, role) {
|
|
187
|
+
if (role !== "reviewer")
|
|
188
|
+
return undefined;
|
|
189
|
+
const lens = readPageSync(runDirectory, "admitted")?.lens;
|
|
190
|
+
return lens === "completeness" || lens === "correctness" ? lens : undefined;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Seal-time landing from createSubmissionLedgerHost (before projectClosure).
|
|
194
|
+
* Missing run directory → no-op (ledger still sealed).
|
|
195
|
+
*/
|
|
196
|
+
export function landTicketProgressForSealedSubmission(input) {
|
|
197
|
+
const runDirectory = runDirectoryFromHostContext(input.context);
|
|
198
|
+
if (runDirectory === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const subjectDirectory = subjectDirectoryOfRun(runDirectory);
|
|
201
|
+
if (subjectDirectory === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
const lens = (input.lens === "completeness" || input.lens === "correctness"
|
|
204
|
+
? input.lens
|
|
205
|
+
: lensFromRunDirectory(runDirectory, input.role));
|
|
206
|
+
const leaf = basename(runDirectory);
|
|
207
|
+
const unbound = basename(subjectDirectory) === "unbound";
|
|
208
|
+
const progressPath = unbound
|
|
209
|
+
? join(runDirectory, LEG_PROGRESS_FILENAME)
|
|
210
|
+
: join(subjectDirectory, CURRENT_FILENAME);
|
|
211
|
+
// Original receipt first — independent of derived progress read/append.
|
|
212
|
+
// Progress I/O failure must not erase an already-accepted payload (#1199 F1).
|
|
213
|
+
const receiptIndex = nextLegReceiptIndex(runDirectory);
|
|
214
|
+
const relativePath = `runs/${leaf}/${RECEIPTS_DIRNAME}/${receiptIndex}.json`;
|
|
215
|
+
const absolutePath = join(subjectDirectory, relativePath);
|
|
216
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
217
|
+
writeFileSync(absolutePath, `${JSON.stringify(input.accepted, null, 2)}\n`, "utf8");
|
|
218
|
+
// #1199 R1: bound leg with leftover progress.jsonl must publish before this
|
|
219
|
+
// seal takes the next ticket round — optional ticketNumber must not be required.
|
|
220
|
+
if (!unbound) {
|
|
221
|
+
relocateTicketProgressForLeg({ runDirectory });
|
|
222
|
+
}
|
|
223
|
+
const existing = unbound
|
|
224
|
+
? readProgressFile(progressPath)
|
|
225
|
+
: readTicketProgressLines(subjectDirectory);
|
|
226
|
+
// Unbound: round stays empty until whole-leg bind publishes into ticket current.
|
|
227
|
+
const round = unbound ? "" : nextRoundFromLines(existing, input.role, lens);
|
|
228
|
+
const sessionFile = progressSessionAbsolute(runDirectory, input.context);
|
|
229
|
+
const sessionRelative = relative(subjectDirectory, sessionFile).split(sep).join("/");
|
|
230
|
+
const { status, summary } = statusAndSummaryFromPayload(input.role, input.accepted);
|
|
231
|
+
const instruction = typeof input.context.summonsInstruction === "string"
|
|
232
|
+
? input.context.summonsInstruction
|
|
233
|
+
: "";
|
|
234
|
+
const line = {
|
|
235
|
+
at: new Date().toISOString(),
|
|
236
|
+
seat: input.role,
|
|
237
|
+
round,
|
|
238
|
+
instruction,
|
|
239
|
+
head: input.sealHead.head,
|
|
240
|
+
...(status === undefined ? {} : { status }),
|
|
241
|
+
...(summary === undefined ? {} : { summary }),
|
|
242
|
+
receipt: relativePath,
|
|
243
|
+
session: sessionRelative,
|
|
244
|
+
};
|
|
245
|
+
mkdirSync(dirname(progressPath), { recursive: true });
|
|
246
|
+
appendFileSync(progressPath, `${JSON.stringify(line)}\n`, "utf8");
|
|
247
|
+
// HEAD capture fault: keep the raw cause on stderr. Do not invent a second
|
|
248
|
+
// dossier custom-frame channel beside post-admission's existing retain path.
|
|
249
|
+
if (input.sealHead.fault !== undefined) {
|
|
250
|
+
process.stderr.write(`${input.sealHead.fault}\n`);
|
|
251
|
+
}
|
|
252
|
+
return line;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* After whole-leg rename (or retry when already under the ticket): fill rounds
|
|
256
|
+
* on staged `progress.jsonl`, append ticket `current.jsonl`, then drop staging.
|
|
257
|
+
* Receipt / session / head / instruction keep seal-time values and relative
|
|
258
|
+
* paths — no copy, delete-source, or rewrite of relative pointers.
|
|
259
|
+
*
|
|
260
|
+
* Unbound staging leaves `round` empty; publish takes the leg's sealed lens from
|
|
261
|
+
* the admitted page (`lensFromRunDirectory`), never from receipt prose.
|
|
262
|
+
*/
|
|
263
|
+
export function relocateTicketProgressForLeg(input) {
|
|
264
|
+
if (isUnboundRunDirectory(input.runDirectory))
|
|
265
|
+
return;
|
|
266
|
+
const subjectDirectory = subjectDirectoryOfRun(input.runDirectory);
|
|
267
|
+
if (subjectDirectory === undefined)
|
|
268
|
+
return;
|
|
269
|
+
const stagedPath = join(input.runDirectory, LEG_PROGRESS_FILENAME);
|
|
270
|
+
const mine = readProgressFile(stagedPath);
|
|
271
|
+
if (mine.length === 0) {
|
|
272
|
+
if (existsSync(stagedPath)) {
|
|
273
|
+
rmSync(stagedPath, { force: true });
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const ticketLines = [...readTicketProgressLines(subjectDirectory)];
|
|
278
|
+
// Receipt path is the seal identity. Append-then-unlink partial success must
|
|
279
|
+
// not reassign rounds or re-append the same originals on re-entry (#1199 F2).
|
|
280
|
+
const publishedReceipts = new Set(ticketLines
|
|
281
|
+
.map((line) => line.receipt)
|
|
282
|
+
.filter((receipt) => typeof receipt === "string" && receipt !== ""));
|
|
283
|
+
const pending = mine.filter((line) => !publishedReceipts.has(line.receipt));
|
|
284
|
+
if (pending.length === 0) {
|
|
285
|
+
rmSync(stagedPath, { force: true });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const movedLines = [];
|
|
289
|
+
for (const line of pending) {
|
|
290
|
+
// Current unbound staging publishes empty rounds; lens comes only from this
|
|
291
|
+
// leg's admitted page. No legacy staged-round mirror-suffix branch.
|
|
292
|
+
const lens = lensFromRunDirectory(input.runDirectory, line.seat);
|
|
293
|
+
const round = nextRoundFromLines(ticketLines, line.seat, lens);
|
|
294
|
+
const moved = {
|
|
295
|
+
...line,
|
|
296
|
+
round,
|
|
297
|
+
};
|
|
298
|
+
movedLines.push(moved);
|
|
299
|
+
ticketLines.push(moved);
|
|
300
|
+
}
|
|
301
|
+
const newCurrent = join(subjectDirectory, CURRENT_FILENAME);
|
|
302
|
+
mkdirSync(dirname(newCurrent), { recursive: true });
|
|
303
|
+
appendFileSync(newCurrent, `${movedLines.map((line) => JSON.stringify(line)).join("\n")}\n`, "utf8");
|
|
304
|
+
// Append succeeded — drop staging. Failure above leaves progress.jsonl in place.
|
|
305
|
+
rmSync(stagedPath, { force: true });
|
|
306
|
+
}
|
|
@@ -2,33 +2,39 @@
|
|
|
2
2
|
* Typed user-dialogue stdin transport (#879 / #471).
|
|
3
3
|
* Pi's piped-stdin reader trims the wrapper; the body field keeps original bytes.
|
|
4
4
|
* No size cap and no path/pointer substitute — the whole dialogue rides the envelope.
|
|
5
|
+
*
|
|
6
|
+
* #1199: progress-line instruction is these same body bytes (what the seat
|
|
7
|
+
* actually receives). No parallel 传召词 field on the envelope.
|
|
5
8
|
*/
|
|
6
9
|
import { isRecord } from "./unknown-value.js";
|
|
7
10
|
export const USER_DIALOGUE_STDIN_KIND = "ak-user-dialogue";
|
|
8
11
|
export function encodeUserDialogueStdin(body) {
|
|
9
|
-
|
|
12
|
+
const envelope = { kind: USER_DIALOGUE_STDIN_KIND, body };
|
|
13
|
+
return JSON.stringify(envelope);
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return false;
|
|
14
|
-
const record = value;
|
|
15
|
-
return record.kind === USER_DIALOGUE_STDIN_KIND && typeof record.body === "string";
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Host read: recover original dialogue after a trim-capable pipe.
|
|
19
|
-
* Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
|
|
20
|
-
*/
|
|
21
|
-
export function readUserDialogueStdin(raw) {
|
|
15
|
+
/** One parse for the model-facing body — callers must not re-implement. */
|
|
16
|
+
export function tryParseUserDialogueStdin(raw) {
|
|
22
17
|
const trimmed = raw.trim();
|
|
23
18
|
if (!trimmed.startsWith("{"))
|
|
24
|
-
return
|
|
19
|
+
return undefined;
|
|
25
20
|
try {
|
|
26
21
|
const parsed = JSON.parse(trimmed);
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
22
|
+
if (!isRecord(parsed))
|
|
23
|
+
return undefined;
|
|
24
|
+
const record = parsed;
|
|
25
|
+
if (record.kind !== USER_DIALOGUE_STDIN_KIND || typeof record.body !== "string") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return { kind: USER_DIALOGUE_STDIN_KIND, body: record.body };
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
|
-
|
|
31
|
+
return undefined;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Host read: recover original dialogue after a trim-capable pipe.
|
|
36
|
+
* Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function readUserDialogueStdin(raw) {
|
|
39
|
+
return tryParseUserDialogueStdin(raw)?.body ?? raw;
|
|
34
40
|
}
|