@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
package/src/role-runtime.ts
CHANGED
|
@@ -1171,7 +1171,16 @@ export function createRoleRuntimeExtension(
|
|
|
1171
1171
|
settleNavigatorProjection,
|
|
1172
1172
|
);
|
|
1173
1173
|
};
|
|
1174
|
-
roleHost.on("input", (
|
|
1174
|
+
roleHost.on("input", (event, ctx) => {
|
|
1175
|
+
// #1199: progress instruction = bytes actually received this turn.
|
|
1176
|
+
// HostContext field only — never process.env body (nested identity / ARG_MAX).
|
|
1177
|
+
// Transport unpack is sole at the host adapter (Pi strips envelope once).
|
|
1178
|
+
// Do not re-parse model-facing body — a legal JSON literal must stay intact.
|
|
1179
|
+
// Adapter already booked the unpacked body; fill only when still absent
|
|
1180
|
+
// (plain hosts without a prior unpack).
|
|
1181
|
+
if (typeof event.text === "string" && ctx.summonsInstruction === undefined) {
|
|
1182
|
+
ctx.summonsInstruction = event.text;
|
|
1183
|
+
}
|
|
1175
1184
|
const role = roleHost.getFlag(ROLE_FLAG.name);
|
|
1176
1185
|
if (role !== undefined && !admitted) return { action: "handled" as const };
|
|
1177
1186
|
return { action: "continue" as const };
|
|
@@ -1377,7 +1386,9 @@ export function createRoleRuntimeExtension(
|
|
|
1377
1386
|
receiptDelivery.recordDeliveryRequest();
|
|
1378
1387
|
// Keep the package-owned continuation off the public input lifecycle:
|
|
1379
1388
|
// receipt delivery must not be mistaken for later caller input.
|
|
1380
|
-
// #1208: resend this turn's
|
|
1389
|
+
// #1208: resend this turn's dialogue; #1199 books the same actual input.
|
|
1390
|
+
const deliveryContent = turnDialoguePrompt ?? "";
|
|
1391
|
+
ctx.summonsInstruction = deliveryContent;
|
|
1381
1392
|
const scopeId = ctx.invocationScopeId?.trim() ?? "";
|
|
1382
1393
|
envelopeHost.appendEntry(
|
|
1383
1394
|
RECEIPT_DELIVERY_REQUEST_ENTRY,
|
|
@@ -1393,7 +1404,7 @@ export function createRoleRuntimeExtension(
|
|
|
1393
1404
|
});
|
|
1394
1405
|
envelopeHost.sendMessage({
|
|
1395
1406
|
customType: "ak-receipt-delivery-prompt",
|
|
1396
|
-
content:
|
|
1407
|
+
content: deliveryContent,
|
|
1397
1408
|
display: false,
|
|
1398
1409
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1399
1410
|
} else if (receiptDelivery.nextAction() === "no-receipt" && !noReceiptRecorded) {
|
package/src/run-dossier.ts
CHANGED
|
@@ -20,7 +20,7 @@ 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
22
|
|
|
23
|
-
import { sessionFileOf } from "./role-run-placement.ts";
|
|
23
|
+
import { sessionDirectoryOf, sessionFileOf } from "./role-run-placement.ts";
|
|
24
24
|
import { projectRenderedPagePaths } from "./role-run-path-rewrite.ts";
|
|
25
25
|
import { RUN_CURRENT_FILE, RUN_HISTORY_FILE, RUN_LOG_FILE, RUN_STATE_FILE, OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.ts";
|
|
26
26
|
import { appendSitianRecord } from "./sitian-appender.ts";
|
|
@@ -213,8 +213,10 @@ function render(
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
// The copy row keeps the absolute path it was written to; a run that was filed under its ticket
|
|
216
|
-
// since lives elsewhere. The original is projected from where this run
|
|
217
|
-
if (typeof original === "string")
|
|
216
|
+
// since lives elsewhere. The original is projected from where this run's session lands now.
|
|
217
|
+
if (typeof original === "string") {
|
|
218
|
+
original = join(sessionDirectoryOf(runDirectory), basename(original));
|
|
219
|
+
}
|
|
218
220
|
whole.host = { sessions, ...(original === undefined ? {} : { original }) };
|
|
219
221
|
whole.submission = latest === undefined ? {} : { latest };
|
|
220
222
|
if (Object.keys(unreadable).length > 0) whole.unreadable = unreadable;
|
package/src/session-identity.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { DurablePrincipal, DurablePrincipalAuthority } from "./host-contracts.ts";
|
|
4
4
|
import {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
lookupHeadlessHostDescription,
|
|
7
7
|
lookupHostDescription,
|
|
8
8
|
} from "./host-descriptions.ts";
|
|
9
|
+
import { runDirectoryFromSessionDirectory } from "./role-run-placement.ts";
|
|
9
10
|
import { renderCurrentSync, RUN_STATE_FILE } from "./run-dossier.ts";
|
|
10
11
|
import { readSitianRecords, reportRunRecord } from "./sitian-facade.ts";
|
|
11
12
|
import { errorText, isRecord } from "./unknown-value.ts";
|
|
@@ -27,7 +28,7 @@ export function createSessionIdentityAuthority(
|
|
|
27
28
|
hostName: string,
|
|
28
29
|
): SessionIdentityAuthority {
|
|
29
30
|
const runDirectoryOf = (principal: DurablePrincipal): string =>
|
|
30
|
-
|
|
31
|
+
runDirectoryFromSessionDirectory(authority.decode(principal).sessionDirectory);
|
|
31
32
|
return {
|
|
32
33
|
principalAuthority: authority,
|
|
33
34
|
resolveSessionFile(principal) {
|
package/src/sitian-appender.ts
CHANGED
|
@@ -7,7 +7,10 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { appendFileSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
parseRunLeaf,
|
|
12
|
+
sessionDirectoryOf,
|
|
13
|
+
} from "./role-run-placement.ts";
|
|
11
14
|
import { RUN_HISTORY_FILE, RUN_HISTORY_KINDS, RUN_LOG_FILE, RUN_STATE_FILE, RUN_STATE_KINDS } from "./run-dossier-files.ts";
|
|
12
15
|
import { ticketNumberFromSitianSubject } from "./run-ticket-number.ts";
|
|
13
16
|
|
|
@@ -48,6 +51,14 @@ export function resolveSitianVolumeCategory(kind: string): string {
|
|
|
48
51
|
return kind;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** Run directory owning `<run>/session` (leg-local originals; #1199 keeps this landing). */
|
|
55
|
+
function runDirectoryOwningSessionParent(sessionParentDir: string): string | undefined {
|
|
56
|
+
if (basename(sessionParentDir) !== "session") return undefined;
|
|
57
|
+
const runDirectory = dirname(sessionParentDir);
|
|
58
|
+
if (parseRunLeaf(basename(runDirectory)) === undefined) return undefined;
|
|
59
|
+
return runDirectory;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
type SitianRecordPath = {
|
|
52
63
|
readonly sessionDir: string;
|
|
53
64
|
readonly recordFile: string;
|
|
@@ -117,8 +128,8 @@ export function resolveSitianRecordPathInLedger(
|
|
|
117
128
|
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
118
129
|
}
|
|
119
130
|
const sessionParentDir = dirname(input.sessionParent);
|
|
120
|
-
const runDirectory =
|
|
121
|
-
if (
|
|
131
|
+
const runDirectory = runDirectoryOwningSessionParent(sessionParentDir);
|
|
132
|
+
if (runDirectory !== undefined) {
|
|
122
133
|
// A role run's own session: every record kind shares the run's one log (#1161).
|
|
123
134
|
sessionDir = runDirectory;
|
|
124
135
|
recordFile = join(runDirectory, RUN_HISTORY_KINDS.has(input.kind) ? RUN_HISTORY_FILE : RUN_STATE_KINDS.has(input.kind) ? RUN_STATE_FILE : RUN_LOG_FILE);
|
package/src/submission-ledger.ts
CHANGED
|
@@ -22,6 +22,10 @@ import { failOnInfrastructureFailureDeclaration, infrastructureFailureDiagnostic
|
|
|
22
22
|
import { reportTicketFromHostContext } from "./report-ticket-tool.ts";
|
|
23
23
|
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
|
|
24
24
|
import { readDeclaredTicketNumber } from "./run-ticket-number.ts";
|
|
25
|
+
import {
|
|
26
|
+
captureWorktreeHead,
|
|
27
|
+
landTicketProgressForSealedSubmission,
|
|
28
|
+
} from "./ticket-progress.ts";
|
|
25
29
|
|
|
26
30
|
import { isRecord, errorText } from "./unknown-value.ts";
|
|
27
31
|
|
|
@@ -463,6 +467,8 @@ export async function sealAcceptedSubmission(options: {
|
|
|
463
467
|
const attemptId = attemptIdentity(options.context, runId);
|
|
464
468
|
const sessionParent = sessionParentFromHostContext(options.context);
|
|
465
469
|
const home = homeFromHostContext(options.context, options.home);
|
|
470
|
+
// #1199: same seal-time HEAD + progress landing as the terminating-tool wrap.
|
|
471
|
+
const sealHead = captureWorktreeHead(options.context.cwd);
|
|
466
472
|
sitianReport({
|
|
467
473
|
level: "event",
|
|
468
474
|
kind: "sealed",
|
|
@@ -479,6 +485,12 @@ export async function sealAcceptedSubmission(options: {
|
|
|
479
485
|
...(home !== undefined ? { home } : {}),
|
|
480
486
|
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
481
487
|
});
|
|
488
|
+
landTicketProgressForSealedSubmission({
|
|
489
|
+
context: options.context,
|
|
490
|
+
role: options.role,
|
|
491
|
+
accepted: options.accepted,
|
|
492
|
+
sealHead,
|
|
493
|
+
});
|
|
482
494
|
}
|
|
483
495
|
|
|
484
496
|
/**
|
|
@@ -636,6 +648,9 @@ export function createSubmissionLedgerHost(
|
|
|
636
648
|
kind: "accepted",
|
|
637
649
|
accepted: params,
|
|
638
650
|
};
|
|
651
|
+
// #1199: seal-time HEAD + ticket progress before projectClosure.
|
|
652
|
+
// One seal → one row; post-turn history guessing is not used.
|
|
653
|
+
const sealHead = captureWorktreeHead(context.cwd);
|
|
639
654
|
append({
|
|
640
655
|
type: "sealed",
|
|
641
656
|
attemptId,
|
|
@@ -643,6 +658,18 @@ export function createSubmissionLedgerHost(
|
|
|
643
658
|
role,
|
|
644
659
|
accepted: params,
|
|
645
660
|
});
|
|
661
|
+
const rawLens = typeof host.getFlag === "function"
|
|
662
|
+
? host.getFlag("ak-review-lens")
|
|
663
|
+
: undefined;
|
|
664
|
+
landTicketProgressForSealedSubmission({
|
|
665
|
+
context,
|
|
666
|
+
role,
|
|
667
|
+
accepted: params,
|
|
668
|
+
sealHead,
|
|
669
|
+
...(rawLens === "completeness" || rawLens === "correctness"
|
|
670
|
+
? { lens: rawLens }
|
|
671
|
+
: {}),
|
|
672
|
+
});
|
|
646
673
|
await projectClosure(closed, context);
|
|
647
674
|
return result;
|
|
648
675
|
},
|
|
@@ -0,0 +1,383 @@
|
|
|
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 {
|
|
16
|
+
appendFileSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
25
|
+
|
|
26
|
+
import { resolveHostDossierLandingPath } from "./host-session-record.ts";
|
|
27
|
+
import type { HostContext } from "./host-contracts.ts";
|
|
28
|
+
import { runDirectoryFromHostContext } from "./host-contracts.ts";
|
|
29
|
+
import {
|
|
30
|
+
acceptedFacts,
|
|
31
|
+
isTerminatingToolName,
|
|
32
|
+
type AcceptedDetails,
|
|
33
|
+
} from "./package-contracts/terminating-tools.ts";
|
|
34
|
+
import { packagedRoleOutputTool } from "./packaged-role-registry.ts";
|
|
35
|
+
import {
|
|
36
|
+
isUnboundRunDirectory,
|
|
37
|
+
sessionDirectoryOf,
|
|
38
|
+
sessionFileOf,
|
|
39
|
+
subjectDirectoryOfRun,
|
|
40
|
+
} from "./role-run-placement.ts";
|
|
41
|
+
import { readPageSync } from "./run-dossier.ts";
|
|
42
|
+
import { parseSitianRecordText } from "./sitian-reader.ts";
|
|
43
|
+
import { isRecord } from "./unknown-value.ts";
|
|
44
|
+
|
|
45
|
+
const CURRENT_FILENAME = "current.jsonl" as const;
|
|
46
|
+
const RECEIPTS_DIRNAME = "receipts" as const;
|
|
47
|
+
const LEG_PROGRESS_FILENAME = "progress.jsonl" as const;
|
|
48
|
+
|
|
49
|
+
/** One accepted-receipt progress row on the ticket (or unbound leg) subject. */
|
|
50
|
+
export type TicketProgressLine = {
|
|
51
|
+
readonly at: string;
|
|
52
|
+
readonly seat: string;
|
|
53
|
+
/** Seat-local round; reviewer dual-lens uses `<n>.<lens>`. Empty while unbound. */
|
|
54
|
+
readonly round: string;
|
|
55
|
+
/** This-round summons instruction bytes; empty string when none. */
|
|
56
|
+
readonly instruction: string;
|
|
57
|
+
/** `git rev-parse HEAD` at seal from the seat worktree; empty on failure. */
|
|
58
|
+
readonly head: string;
|
|
59
|
+
/** Structured status when present on the receipt — omitted when absent. */
|
|
60
|
+
readonly status?: string;
|
|
61
|
+
/** Seat-authored short summary when present on the receipt. */
|
|
62
|
+
readonly summary?: string;
|
|
63
|
+
/** Path relative to the subject directory. */
|
|
64
|
+
readonly receipt: string;
|
|
65
|
+
/** Path relative to the subject directory. */
|
|
66
|
+
readonly session: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Append-only progress JSONL — same posture as run-dossier row files:
|
|
71
|
+
* sole Sitian decoder keeps reachable rows; malformed lines leave diagnostics
|
|
72
|
+
* on stderr and do not abort seal/relocate.
|
|
73
|
+
*/
|
|
74
|
+
function parseProgressText(text: string, pathForDiagnostic: string): TicketProgressLine[] {
|
|
75
|
+
if (text.trim() === "") return [];
|
|
76
|
+
const { records, diagnostics } = parseSitianRecordText(text);
|
|
77
|
+
for (const diagnostic of diagnostics) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`[ticket-progress] ${pathForDiagnostic} has malformed row(s); keeps reachable rows and retains the diagnostic: line ${diagnostic.line}: ${diagnostic.error}\n`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const lines: TicketProgressLine[] = [];
|
|
83
|
+
for (const record of records) {
|
|
84
|
+
const parsed: unknown = record;
|
|
85
|
+
if (!isRecord(parsed)) continue;
|
|
86
|
+
if (typeof parsed.seat !== "string" || typeof parsed.round !== "string") continue;
|
|
87
|
+
lines.push(parsed as unknown as TicketProgressLine);
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readProgressFile(path: string): TicketProgressLine[] {
|
|
93
|
+
if (!existsSync(path)) return [];
|
|
94
|
+
return parseProgressText(readFileSync(path, "utf8"), path);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read progress rows under a subject.
|
|
99
|
+
* Ticket: append-only `current.jsonl`.
|
|
100
|
+
* Unbound: each leg's `runs/<leaf>/progress.jsonl` (no shared file).
|
|
101
|
+
*/
|
|
102
|
+
export function readTicketProgressLines(subjectDirectory: string): TicketProgressLine[] {
|
|
103
|
+
if (basename(subjectDirectory) === "unbound") {
|
|
104
|
+
const runsDir = join(subjectDirectory, "runs");
|
|
105
|
+
if (!existsSync(runsDir)) return [];
|
|
106
|
+
const out: TicketProgressLine[] = [];
|
|
107
|
+
for (const name of readdirSync(runsDir).sort()) {
|
|
108
|
+
out.push(...readProgressFile(join(runsDir, name, LEG_PROGRESS_FILENAME)));
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
return readProgressFile(join(subjectDirectory, CURRENT_FILENAME));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function roundBaseNumber(round: string): number {
|
|
116
|
+
if (round.trim() === "") return 0;
|
|
117
|
+
const base = round.includes(".") ? round.slice(0, round.indexOf(".")) : round;
|
|
118
|
+
const n = Number(base);
|
|
119
|
+
return Number.isSafeInteger(n) && n > 0 ? n : 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function roundLens(round: string): string | undefined {
|
|
123
|
+
return round.includes(".") ? round.slice(round.indexOf(".") + 1) : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function nextRoundFromLines(
|
|
127
|
+
lines: readonly TicketProgressLine[],
|
|
128
|
+
seat: string,
|
|
129
|
+
lens?: string,
|
|
130
|
+
): string {
|
|
131
|
+
const want = lens === undefined || lens.trim() === "" ? undefined : lens;
|
|
132
|
+
let max = 0;
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (line.seat !== seat) continue;
|
|
135
|
+
if (roundLens(line.round) !== want) continue;
|
|
136
|
+
max = Math.max(max, roundBaseNumber(line.round));
|
|
137
|
+
}
|
|
138
|
+
const base = max + 1;
|
|
139
|
+
return want === undefined ? String(base) : `${base}.${want}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Leg-local receipt ordinal: `receipts/<n>.json` (not seat#round). */
|
|
143
|
+
function nextLegReceiptIndex(runDirectory: string): number {
|
|
144
|
+
const dir = join(runDirectory, RECEIPTS_DIRNAME);
|
|
145
|
+
if (!existsSync(dir)) return 1;
|
|
146
|
+
let max = 0;
|
|
147
|
+
for (const name of readdirSync(dir)) {
|
|
148
|
+
const match = /^(\d+)\.json$/.exec(name);
|
|
149
|
+
if (match === null) continue;
|
|
150
|
+
const n = Number(match[1]);
|
|
151
|
+
if (Number.isSafeInteger(n) && n > max) max = n;
|
|
152
|
+
}
|
|
153
|
+
return max + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Copy structured status/summary from the seat receipt.
|
|
158
|
+
* Status leaves reuse acceptedFacts (terminating-tools authority) via the seat's
|
|
159
|
+
* current output tool — legacy judgeStatus/countersignStatus keys are unreachable
|
|
160
|
+
* at this seal seam (tool is the live registry name, not ak_judge_output).
|
|
161
|
+
* summary is seat-authored prose on the receipt and is not part of AcceptedFacts.
|
|
162
|
+
* Missing fields stay absent — never invent "" or "accepted".
|
|
163
|
+
*
|
|
164
|
+
* Not receiptStatusFromRegistry: that scans every registry key for navigator
|
|
165
|
+
* projection when the seat is unknown; here the seal already knows the seat.
|
|
166
|
+
*/
|
|
167
|
+
function statusAndSummaryFromPayload(seat: string, payload: unknown): {
|
|
168
|
+
readonly status?: string;
|
|
169
|
+
readonly summary?: string;
|
|
170
|
+
} {
|
|
171
|
+
if (!isRecord(payload)) return {};
|
|
172
|
+
const toolName = packagedRoleOutputTool(seat);
|
|
173
|
+
const status = toolName !== undefined && isTerminatingToolName(toolName)
|
|
174
|
+
? acceptedFacts(toolName, payload as AcceptedDetails).status
|
|
175
|
+
: undefined;
|
|
176
|
+
const summary = typeof payload.summary === "string" ? payload.summary : undefined;
|
|
177
|
+
return {
|
|
178
|
+
...(status === undefined ? {} : { status }),
|
|
179
|
+
...(summary === undefined ? {} : { summary }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type WorktreeHeadCapture = {
|
|
184
|
+
readonly head: string;
|
|
185
|
+
readonly fault?: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Worktree HEAD at seal. On failure head is empty and fault carries the true cause. */
|
|
189
|
+
export function captureWorktreeHead(projectRoot: string): WorktreeHeadCapture {
|
|
190
|
+
try {
|
|
191
|
+
const result = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
192
|
+
cwd: projectRoot,
|
|
193
|
+
encoding: "utf8",
|
|
194
|
+
maxBuffer: 1024 * 1024,
|
|
195
|
+
});
|
|
196
|
+
if (result.error != null) {
|
|
197
|
+
const err = result.error;
|
|
198
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
199
|
+
return { head: "", fault: `git rev-parse HEAD spawn failed cwd=${projectRoot}: ${detail}` };
|
|
200
|
+
}
|
|
201
|
+
if (result.status !== 0) {
|
|
202
|
+
const detail = [result.stderr, result.stdout]
|
|
203
|
+
.map((part) => (typeof part === "string" ? part.trim() : ""))
|
|
204
|
+
.filter((part) => part !== "")
|
|
205
|
+
.join(" | ");
|
|
206
|
+
const status = result.status === null ? "null" : String(result.status);
|
|
207
|
+
const signal = result.signal == null ? "" : ` signal=${result.signal}`;
|
|
208
|
+
return {
|
|
209
|
+
head: "",
|
|
210
|
+
fault: detail === ""
|
|
211
|
+
? `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}`
|
|
212
|
+
: `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}: ${detail}`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { head: (result.stdout ?? "").trim() };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
218
|
+
return { head: "", fault: `git rev-parse HEAD threw cwd=${projectRoot}: ${message}` };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function progressSessionAbsolute(runDirectory: string, context: HostContext): string {
|
|
223
|
+
const host = typeof context.host === "string" ? context.host.trim() : "";
|
|
224
|
+
const sessionDirectory = sessionDirectoryOf(runDirectory);
|
|
225
|
+
// External hosts: ADR 0086 copy landing (codex.jsonl / grok-build / …), not the
|
|
226
|
+
// principal wire file. Responsibility unchanged (#1199 R3 disposition).
|
|
227
|
+
if (host !== "" && host !== "pi" && host !== "hermes") {
|
|
228
|
+
return resolveHostDossierLandingPath({ host, sessionDirectory });
|
|
229
|
+
}
|
|
230
|
+
// Pi/hermes: prefer the issued durable coordinate on the live handle.
|
|
231
|
+
const issued = context.sessionManager.getSessionFile?.();
|
|
232
|
+
if (typeof issued === "string" && issued.trim() !== "") {
|
|
233
|
+
return issued;
|
|
234
|
+
}
|
|
235
|
+
return sessionFileOf(runDirectory);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function lensFromRunDirectory(runDirectory: string, role: string): string | undefined {
|
|
239
|
+
if (role !== "reviewer") return undefined;
|
|
240
|
+
const lens = readPageSync(runDirectory, "admitted")?.lens;
|
|
241
|
+
return lens === "completeness" || lens === "correctness" ? lens : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Seal-time landing from createSubmissionLedgerHost (before projectClosure).
|
|
246
|
+
* Missing run directory → no-op (ledger still sealed).
|
|
247
|
+
*/
|
|
248
|
+
export function landTicketProgressForSealedSubmission(input: {
|
|
249
|
+
readonly context: HostContext;
|
|
250
|
+
readonly role: string;
|
|
251
|
+
readonly accepted: unknown;
|
|
252
|
+
readonly sealHead: WorktreeHeadCapture;
|
|
253
|
+
readonly lens?: string;
|
|
254
|
+
}): TicketProgressLine | undefined {
|
|
255
|
+
const runDirectory = runDirectoryFromHostContext(input.context);
|
|
256
|
+
if (runDirectory === undefined) return undefined;
|
|
257
|
+
|
|
258
|
+
const subjectDirectory = subjectDirectoryOfRun(runDirectory);
|
|
259
|
+
if (subjectDirectory === undefined) return undefined;
|
|
260
|
+
|
|
261
|
+
const lens = (
|
|
262
|
+
input.lens === "completeness" || input.lens === "correctness"
|
|
263
|
+
? input.lens
|
|
264
|
+
: lensFromRunDirectory(runDirectory, input.role)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const leaf = basename(runDirectory);
|
|
268
|
+
const unbound = basename(subjectDirectory) === "unbound";
|
|
269
|
+
const progressPath = unbound
|
|
270
|
+
? join(runDirectory, LEG_PROGRESS_FILENAME)
|
|
271
|
+
: join(subjectDirectory, CURRENT_FILENAME);
|
|
272
|
+
// Original receipt first — independent of derived progress read/append.
|
|
273
|
+
// Progress I/O failure must not erase an already-accepted payload (#1199 F1).
|
|
274
|
+
const receiptIndex = nextLegReceiptIndex(runDirectory);
|
|
275
|
+
const relativePath = `runs/${leaf}/${RECEIPTS_DIRNAME}/${receiptIndex}.json`;
|
|
276
|
+
const absolutePath = join(subjectDirectory, relativePath);
|
|
277
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
278
|
+
writeFileSync(absolutePath, `${JSON.stringify(input.accepted, null, 2)}\n`, "utf8");
|
|
279
|
+
|
|
280
|
+
// #1199 R1: bound leg with leftover progress.jsonl must publish before this
|
|
281
|
+
// seal takes the next ticket round — optional ticketNumber must not be required.
|
|
282
|
+
if (!unbound) {
|
|
283
|
+
relocateTicketProgressForLeg({ runDirectory });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const existing = unbound
|
|
287
|
+
? readProgressFile(progressPath)
|
|
288
|
+
: readTicketProgressLines(subjectDirectory);
|
|
289
|
+
// Unbound: round stays empty until whole-leg bind publishes into ticket current.
|
|
290
|
+
const round = unbound ? "" : nextRoundFromLines(existing, input.role, lens);
|
|
291
|
+
|
|
292
|
+
const sessionFile = progressSessionAbsolute(runDirectory, input.context);
|
|
293
|
+
const sessionRelative = relative(subjectDirectory, sessionFile).split(sep).join("/");
|
|
294
|
+
const { status, summary } = statusAndSummaryFromPayload(input.role, input.accepted);
|
|
295
|
+
const instruction = typeof input.context.summonsInstruction === "string"
|
|
296
|
+
? input.context.summonsInstruction
|
|
297
|
+
: "";
|
|
298
|
+
const line: TicketProgressLine = {
|
|
299
|
+
at: new Date().toISOString(),
|
|
300
|
+
seat: input.role,
|
|
301
|
+
round,
|
|
302
|
+
instruction,
|
|
303
|
+
head: input.sealHead.head,
|
|
304
|
+
...(status === undefined ? {} : { status }),
|
|
305
|
+
...(summary === undefined ? {} : { summary }),
|
|
306
|
+
receipt: relativePath,
|
|
307
|
+
session: sessionRelative,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
mkdirSync(dirname(progressPath), { recursive: true });
|
|
311
|
+
appendFileSync(progressPath, `${JSON.stringify(line)}\n`, "utf8");
|
|
312
|
+
// HEAD capture fault: keep the raw cause on stderr. Do not invent a second
|
|
313
|
+
// dossier custom-frame channel beside post-admission's existing retain path.
|
|
314
|
+
if (input.sealHead.fault !== undefined) {
|
|
315
|
+
process.stderr.write(`${input.sealHead.fault}\n`);
|
|
316
|
+
}
|
|
317
|
+
return line;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* After whole-leg rename (or retry when already under the ticket): fill rounds
|
|
322
|
+
* on staged `progress.jsonl`, append ticket `current.jsonl`, then drop staging.
|
|
323
|
+
* Receipt / session / head / instruction keep seal-time values and relative
|
|
324
|
+
* paths — no copy, delete-source, or rewrite of relative pointers.
|
|
325
|
+
*
|
|
326
|
+
* Unbound staging leaves `round` empty; publish takes the leg's sealed lens from
|
|
327
|
+
* the admitted page (`lensFromRunDirectory`), never from receipt prose.
|
|
328
|
+
*/
|
|
329
|
+
export function relocateTicketProgressForLeg(input: {
|
|
330
|
+
readonly runDirectory: string;
|
|
331
|
+
}): void {
|
|
332
|
+
if (isUnboundRunDirectory(input.runDirectory)) return;
|
|
333
|
+
|
|
334
|
+
const subjectDirectory = subjectDirectoryOfRun(input.runDirectory);
|
|
335
|
+
if (subjectDirectory === undefined) return;
|
|
336
|
+
|
|
337
|
+
const stagedPath = join(input.runDirectory, LEG_PROGRESS_FILENAME);
|
|
338
|
+
const mine = readProgressFile(stagedPath);
|
|
339
|
+
if (mine.length === 0) {
|
|
340
|
+
if (existsSync(stagedPath)) {
|
|
341
|
+
rmSync(stagedPath, { force: true });
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const ticketLines = [...readTicketProgressLines(subjectDirectory)];
|
|
347
|
+
// Receipt path is the seal identity. Append-then-unlink partial success must
|
|
348
|
+
// not reassign rounds or re-append the same originals on re-entry (#1199 F2).
|
|
349
|
+
const publishedReceipts = new Set(
|
|
350
|
+
ticketLines
|
|
351
|
+
.map((line) => line.receipt)
|
|
352
|
+
.filter((receipt) => typeof receipt === "string" && receipt !== ""),
|
|
353
|
+
);
|
|
354
|
+
const pending = mine.filter((line) => !publishedReceipts.has(line.receipt));
|
|
355
|
+
if (pending.length === 0) {
|
|
356
|
+
rmSync(stagedPath, { force: true });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const movedLines: TicketProgressLine[] = [];
|
|
361
|
+
for (const line of pending) {
|
|
362
|
+
// Current unbound staging publishes empty rounds; lens comes only from this
|
|
363
|
+
// leg's admitted page. No legacy staged-round mirror-suffix branch.
|
|
364
|
+
const lens = lensFromRunDirectory(input.runDirectory, line.seat);
|
|
365
|
+
const round = nextRoundFromLines(ticketLines, line.seat, lens);
|
|
366
|
+
const moved: TicketProgressLine = {
|
|
367
|
+
...line,
|
|
368
|
+
round,
|
|
369
|
+
};
|
|
370
|
+
movedLines.push(moved);
|
|
371
|
+
ticketLines.push(moved);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const newCurrent = join(subjectDirectory, CURRENT_FILENAME);
|
|
375
|
+
mkdirSync(dirname(newCurrent), { recursive: true });
|
|
376
|
+
appendFileSync(
|
|
377
|
+
newCurrent,
|
|
378
|
+
`${movedLines.map((line) => JSON.stringify(line)).join("\n")}\n`,
|
|
379
|
+
"utf8",
|
|
380
|
+
);
|
|
381
|
+
// Append succeeded — drop staging. Failure above leaves progress.jsonl in place.
|
|
382
|
+
rmSync(stagedPath, { force: true });
|
|
383
|
+
}
|
|
@@ -2,6 +2,9 @@
|
|
|
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.ts";
|
|
7
10
|
|
|
@@ -13,13 +16,25 @@ export type UserDialogueStdinEnvelope = {
|
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export function encodeUserDialogueStdin(body: string): string {
|
|
16
|
-
|
|
19
|
+
const envelope: UserDialogueStdinEnvelope = { kind: USER_DIALOGUE_STDIN_KIND, body };
|
|
20
|
+
return JSON.stringify(envelope);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
/** One parse for the model-facing body — callers must not re-implement. */
|
|
24
|
+
export function tryParseUserDialogueStdin(raw: string): UserDialogueStdinEnvelope | undefined {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (!trimmed.startsWith("{")) return undefined;
|
|
27
|
+
try {
|
|
28
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
29
|
+
if (!isRecord(parsed)) return undefined;
|
|
30
|
+
const record = parsed as { kind?: unknown; body?: unknown };
|
|
31
|
+
if (record.kind !== USER_DIALOGUE_STDIN_KIND || typeof record.body !== "string") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return { kind: USER_DIALOGUE_STDIN_KIND, body: record.body };
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
/**
|
|
@@ -27,13 +42,5 @@ function isUserDialogueEnvelope(value: unknown): value is UserDialogueStdinEnvel
|
|
|
27
42
|
* Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
|
|
28
43
|
*/
|
|
29
44
|
export function readUserDialogueStdin(raw: string): string {
|
|
30
|
-
|
|
31
|
-
if (!trimmed.startsWith("{")) return raw;
|
|
32
|
-
try {
|
|
33
|
-
const parsed: unknown = JSON.parse(trimmed);
|
|
34
|
-
if (isUserDialogueEnvelope(parsed)) return parsed.body;
|
|
35
|
-
} catch {
|
|
36
|
-
// Not our envelope — leave caller bytes intact.
|
|
37
|
-
}
|
|
38
|
-
return raw;
|
|
45
|
+
return tryParseUserDialogueStdin(raw)?.body ?? raw;
|
|
39
46
|
}
|