@akagilnc/pi-workflow-roles 0.1.5614 → 0.1.5650
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 +960 -465
- package/dist/activation-ledger-topology.js +12 -0
- package/dist/external-host-turn-loop.js +281 -0
- package/dist/headless-host/production-host.js +900 -406
- package/dist/migrate-book-topology.js +8 -5
- package/dist/pi/role-turn-host.js +33 -3
- package/dist/public-cli/instruction-seat-run.js +177 -46
- package/dist/public-cli/invocation.js +5 -12
- package/dist/public-cli/main.js +541 -165
- package/dist/public-cli/post-admission.js +171 -18
- package/dist/public-cli/settlement.js +20 -10
- package/dist/report-ticket-tool.js +136 -0
- package/dist/role-run-placement.js +1 -2
- package/dist/role-run-relocation.js +24 -4
- package/package.json +1 -1
- package/resources/engine-dispatch.md +6 -3
- package/resources/engines/claude-code.md +4 -5
- package/resources/missing-ticket-reask.md +1 -0
- package/src/acp-host/role-turn-host.ts +41 -31
- package/src/activation-ledger-topology.ts +16 -0
- package/src/external-host-turn-loop.ts +103 -12
- package/src/headless-host/role-turn-host.ts +41 -25
- package/src/pi/role-turn-host.ts +37 -3
- package/src/public-cli/instruction-seat-run.ts +248 -50
- package/src/public-cli/invocation.ts +15 -27
- package/src/public-cli/post-admission.ts +250 -33
- package/src/public-cli/run-lifecycle.ts +5 -5
- package/src/public-cli/settlement.ts +38 -11
- package/src/public-cli/terminal.ts +6 -0
- package/src/report-ticket-tool.ts +176 -0
- package/src/role-envelope.ts +28 -3
- package/src/role-run-placement.ts +0 -2
- package/src/role-run-relocation.ts +45 -4
- package/src/role-runtime.ts +6 -0
- package/src/worker-submission-gates.ts +26 -3
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
* Role runners supply only turn request projection and narrow settlement adapters.
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
9
10
|
import { writeFile } from "node:fs/promises";
|
|
10
11
|
import { buildAutoResumeContinuationPrompt, findRunDirectoryById, readRoleRunState, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
|
|
11
12
|
import { CliUsageError } from "./cli-errors.js";
|
|
12
13
|
import { projectPublicTurnAxes } from "./turn-request.js";
|
|
13
14
|
import { appendCallerFileFlagPaths, bindAdmittedTicketNumber, relocateAdmittedRunToTicket, } from "./invocation.js";
|
|
14
15
|
import { readRecordedSubmissionRows } from "../submission-ledger.js";
|
|
15
|
-
import { parseTicketNumber, readDeclaredTicketNumber } from "../run-ticket-number.js";
|
|
16
|
+
import { parseTicketNumber, readBoardTicketNumber, readDeclaredTicketNumber } from "../run-ticket-number.js";
|
|
17
|
+
import { homeFromRunDirectory } from "../activation-ledger-topology.js";
|
|
16
18
|
import { readStoredHostSessionId } from "../session-identity.js";
|
|
17
19
|
import { reportRunRecord } from "../sitian-facade.js";
|
|
18
20
|
import { createReceiptDeliveryPolicy, deliveryLimitFromConfig, } from "../receipt-delivery-policy.js";
|
|
19
|
-
import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
|
|
21
|
+
import { rewritePrincipalSessionPaths, rewriteRunDirectoryPathValue, rewriteAdmittedRoleRunPage } from "../role-run-relocation.js";
|
|
20
22
|
import { isOfficerReviewSeat } from "../packaged-role-registry.js";
|
|
21
23
|
/** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
|
|
22
24
|
function describeCaughtError(error) {
|
|
@@ -62,6 +64,89 @@ function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* #1171: after the host returns, re-locate the leg by run id (report-ticket may
|
|
69
|
+
* have moved it mid-turn in the tool process). Settlement and render use the
|
|
70
|
+
* live path; lease cleanup follows when the held path changed.
|
|
71
|
+
* Failure is not best-effort: a missing former path with no replacement must not
|
|
72
|
+
* settle or write back under the vanished unbound leaf (失败诚实宪法).
|
|
73
|
+
*/
|
|
74
|
+
export async function refreshAdmittedPlacementAfterHostTurn(admitted, authority, heldLease) {
|
|
75
|
+
const home = homeFromRunDirectory(admitted.runDirectory);
|
|
76
|
+
const found = await findRunDirectoryById(home, admitted.runId, admitted.bookKey, admitted.role);
|
|
77
|
+
if (found === undefined && !existsSync(admitted.runDirectory)) {
|
|
78
|
+
throw new Error(`admitted run ${admitted.runId} missing after host turn; not found under ${home}`);
|
|
79
|
+
}
|
|
80
|
+
// Live identity first: later board-ticket read must not leave settlement on the
|
|
81
|
+
// pre-relocate path when facts are damaged (#1171 F4-R1).
|
|
82
|
+
let relocation;
|
|
83
|
+
if (found !== undefined && found !== admitted.runDirectory) {
|
|
84
|
+
const oldRunDirectory = admitted.runDirectory;
|
|
85
|
+
const coords = { ...authority.decode(admitted.principal) };
|
|
86
|
+
rewritePrincipalSessionPaths(coords, oldRunDirectory, found);
|
|
87
|
+
const principal = authority.seal(coords);
|
|
88
|
+
rewriteAdmittedRoleRunPage(admitted, [{ oldRunDirectory, newRunDirectory: found }]);
|
|
89
|
+
admitted.principal = principal;
|
|
90
|
+
heldLease?.relocate(found);
|
|
91
|
+
relocation = { oldRunDirectory, newRunDirectory: found };
|
|
92
|
+
}
|
|
93
|
+
// Board re-read discovers a ticket written mid-turn while still unbound
|
|
94
|
+
// (report-ticket board page). Already-bound identity needs no discovery —
|
|
95
|
+
// path relocate already used findRunDirectoryById. This is not damage
|
|
96
|
+
// recovery: when unbound, readBoardTicketNumber damage/non-ENOENT IO still
|
|
97
|
+
// propagates (失败诚实; notary: no damaged-control-plane → no-ticket wash).
|
|
98
|
+
if (admitted.ticketNumber === undefined) {
|
|
99
|
+
const boardTicket = await readBoardTicketNumber(found ?? admitted.runDirectory);
|
|
100
|
+
if (boardTicket !== undefined) {
|
|
101
|
+
admitted.ticketNumber = boardTicket;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return relocation;
|
|
105
|
+
}
|
|
106
|
+
/** Apply a successful post-host placement refresh onto the live turn request. */
|
|
107
|
+
function applyRefreshedPlacementToTurn(request, turnRequest, admitted, refreshed) {
|
|
108
|
+
projectRelocatedTurnIdentity(request, {}, admitted, refreshed);
|
|
109
|
+
return {
|
|
110
|
+
...turnRequest,
|
|
111
|
+
runDirectory: admitted.runDirectory,
|
|
112
|
+
principal: admitted.principal,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* #1171: host throw after a started turn — one live placement refresh, then the
|
|
117
|
+
* one settlement authority (ADR 0080). Shared by first turn and 催交; never
|
|
118
|
+
* settle under a vanished unbound leaf.
|
|
119
|
+
*/
|
|
120
|
+
async function settleHostThrowAfterLivePlacementRefresh(input) {
|
|
121
|
+
let refreshError;
|
|
122
|
+
let turnRequest = input.turnRequest;
|
|
123
|
+
try {
|
|
124
|
+
const refreshed = await refreshAdmittedPlacementAfterHostTurn(input.admitted, input.env.principalAuthority, input.lease);
|
|
125
|
+
if (refreshed !== undefined) {
|
|
126
|
+
turnRequest = applyRefreshedPlacementToTurn(input.request, turnRequest, input.admitted, refreshed);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
refreshError = err;
|
|
131
|
+
}
|
|
132
|
+
const processCancelName = processCancelSignalName(input.env.signal);
|
|
133
|
+
// Secondary hostThrow only when the host actually threw and refresh also failed.
|
|
134
|
+
// Package-side refresh failure must not borrow that label (#1171 F10).
|
|
135
|
+
const hostThrowSecondary = refreshError !== undefined && input.hostTurnCompleted === false
|
|
136
|
+
? { knownDetails: { hostThrow: describeCaughtError(input.hostError) } }
|
|
137
|
+
: {};
|
|
138
|
+
const settled = await settleAfterTurnStarted(input.admitted, withEngineDetourInvocationScope({
|
|
139
|
+
timedOut: false,
|
|
140
|
+
code: null,
|
|
141
|
+
stderr: "",
|
|
142
|
+
// Prefer the refresh failure when the live path is gone; otherwise the
|
|
143
|
+
// host throw remains the cause (失败诚实:不在旧 unbound 上结算写回).
|
|
144
|
+
...(processCancelName === undefined ? {} : { cancelName: processCancelName }),
|
|
145
|
+
thrown: refreshError ?? input.hostError,
|
|
146
|
+
...hostThrowSecondary,
|
|
147
|
+
}, input.request.invocationScopeId), input.adapters, input.env.principalAuthority, input.io, input.persistRunState, input.notePackageFault, input.courtAttemptId);
|
|
148
|
+
return { settled, turnRequest };
|
|
149
|
+
}
|
|
65
150
|
/** #855: process-cancel settlement never re-enters auto-resume. */
|
|
66
151
|
function withProcessCancelSkipAutoResume(result, signal) {
|
|
67
152
|
if (processCancelSignalName(signal) === undefined) {
|
|
@@ -573,6 +658,8 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
573
658
|
if (afterDispatchApplied)
|
|
574
659
|
return result;
|
|
575
660
|
afterDispatchApplied = true;
|
|
661
|
+
// #1171: placement refresh runs once after each host return (below), before
|
|
662
|
+
// settlement — not again here. finishAfterTurn only binds/relocates sealed tickets.
|
|
576
663
|
// #855: re-read cancel before committing the terminal and afterDispatch.
|
|
577
664
|
// A signal after the history-first commit cannot retroactively erase it.
|
|
578
665
|
// skipRunStateWrite: run-state
|
|
@@ -603,12 +690,31 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
603
690
|
pendingTerminal !== undefined &&
|
|
604
691
|
result.terminal === pendingTerminal) {
|
|
605
692
|
try {
|
|
693
|
+
// #1171 F2-R2 / F2-R3 (#419) / F2-R5: only the soft-reask turn itself
|
|
694
|
+
// preserves the sealed terminal face. ticketReasksSpent is budget across
|
|
695
|
+
// rounds — after audit continue, ordinary no-volume turns publish
|
|
696
|
+
// no_receipt honestly. previewOnly skips the terminal pointer;
|
|
697
|
+
// recordAttemptHistory still appends.
|
|
698
|
+
const softTicketReaskNoSeal = pendingSettlement !== "sealed" && env.softTicketReaskTurn === true;
|
|
699
|
+
const noSealScope = softTicketReaskNoSeal
|
|
700
|
+
? {
|
|
701
|
+
...courtScope,
|
|
702
|
+
previewOnly: true,
|
|
703
|
+
recordAttemptHistory: true,
|
|
704
|
+
}
|
|
705
|
+
: { ...courtScope, recordAttemptHistory: true };
|
|
606
706
|
const terminal = pendingSettlement === "sealed"
|
|
607
707
|
? await adapters.trySettle(admitted, env.principalAuthority, { ...courtScope, recordAttemptHistory: true })
|
|
608
|
-
: await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority,
|
|
708
|
+
: await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, noSealScope, receiptDelivery.issuedDeliveryRequests()), noSealScope);
|
|
609
709
|
if (terminal === undefined)
|
|
610
710
|
throw new Error("settled host attempt vanished before publication");
|
|
611
711
|
result = { ...result, terminal, exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome) };
|
|
712
|
+
// #1171 F2-R7: ownership evidence at this soft-reask turn's seal —
|
|
713
|
+
// mutate the live ownership cell carried on env (survives spreads).
|
|
714
|
+
if (pendingSettlement === "sealed" && env.softTicketReaskTurn === true
|
|
715
|
+
&& env.softTicketReaskOwnership !== undefined) {
|
|
716
|
+
env.softTicketReaskOwnership.sealedSubstitute = true;
|
|
717
|
+
}
|
|
612
718
|
if (pendingSettlement === "sealed" && terminal.roleOutcome.kind === "accepted"
|
|
613
719
|
&& request.courtAttemptId !== undefined && request.courtAttemptId.length > 0) {
|
|
614
720
|
try {
|
|
@@ -719,6 +825,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
719
825
|
// (runPostAdmissionResumable / ManualResume / station-child), not here:
|
|
720
826
|
// auto-resume re-enters this dispatch and must reuse the same scope.
|
|
721
827
|
let result;
|
|
828
|
+
let hostTurnCompleted = false;
|
|
722
829
|
try {
|
|
723
830
|
// A bare `ak-role resume` only forwards the caller's instruction to the
|
|
724
831
|
// host CLI's native resume. Nothing here may gate, redirect or reshape
|
|
@@ -726,19 +833,36 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
726
833
|
// the authoritative post-turn settlement reads whatever really happened.
|
|
727
834
|
turnDispatched = true;
|
|
728
835
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
836
|
+
hostTurnCompleted = true;
|
|
837
|
+
// #1171: one placement refresh before settlement, same try as executeTurn.
|
|
838
|
+
// Failure shares the catch below (refresh-then-settle + #855 cancel wrap).
|
|
839
|
+
const refreshed = await refreshAdmittedPlacementAfterHostTurn(admitted, env.principalAuthority, lease);
|
|
840
|
+
if (refreshed !== undefined) {
|
|
841
|
+
turnRequest = applyRefreshedPlacementToTurn(request, turnRequest, admitted, refreshed);
|
|
842
|
+
}
|
|
729
843
|
}
|
|
730
844
|
catch (error) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
845
|
+
// #1171: one placement refresh before settlement — failure is a real cause,
|
|
846
|
+
// never catch-and-continue onto a vanished unbound leaf.
|
|
847
|
+
// Host throw and post-success refresh throw share this catch (#1171 F9).
|
|
848
|
+
const thrown = await settleHostThrowAfterLivePlacementRefresh({
|
|
849
|
+
admitted,
|
|
850
|
+
hostError: error,
|
|
851
|
+
hostTurnCompleted,
|
|
852
|
+
request,
|
|
853
|
+
turnRequest,
|
|
854
|
+
lease,
|
|
855
|
+
adapters,
|
|
856
|
+
env,
|
|
857
|
+
io,
|
|
858
|
+
persistRunState,
|
|
859
|
+
notePackageFault: courtScope.notePackageFault,
|
|
860
|
+
...(courtScope.courtAttemptId === undefined
|
|
861
|
+
? {}
|
|
862
|
+
: { courtAttemptId: courtScope.courtAttemptId }),
|
|
863
|
+
});
|
|
864
|
+
turnRequest = thrown.turnRequest;
|
|
865
|
+
return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...thrown.settled, turnDispatched: true, ...deferredPersist }, env.signal));
|
|
742
866
|
}
|
|
743
867
|
// #1132: settle this turn through the ONE post-turn settlement authority,
|
|
744
868
|
// then — if it simply produced no receipt — 催交 before settling no_receipt.
|
|
@@ -771,6 +895,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
771
895
|
// that never reached the host.
|
|
772
896
|
let deliveryResult;
|
|
773
897
|
let deliveryTurnRequest;
|
|
898
|
+
let deliveryHostTurnCompleted = false;
|
|
774
899
|
try {
|
|
775
900
|
// Host and the other axes already selected on this turn ride the
|
|
776
901
|
// shared projection. The pre-projection request does not have them.
|
|
@@ -788,15 +913,43 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
788
913
|
};
|
|
789
914
|
receiptDelivery.recordDeliveryRequest();
|
|
790
915
|
deliveryResult = await env.roleTurnHost.executeTurn(deliveryTurnRequest);
|
|
916
|
+
deliveryHostTurnCompleted = true;
|
|
917
|
+
// Same single refresh-before-settle rule as the first turn (#1171):
|
|
918
|
+
// stay inside this try so cancel wrap is not dropped (#1171 F9 / #855).
|
|
919
|
+
const refreshed = await refreshAdmittedPlacementAfterHostTurn(admitted, env.principalAuthority, lease);
|
|
920
|
+
if (refreshed !== undefined) {
|
|
921
|
+
turnRequest = applyRefreshedPlacementToTurn(request, turnRequest, admitted, refreshed);
|
|
922
|
+
deliveryTurnRequest = {
|
|
923
|
+
...deliveryTurnRequest,
|
|
924
|
+
runDirectory: admitted.runDirectory,
|
|
925
|
+
principal: admitted.principal,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
791
928
|
}
|
|
792
929
|
catch (error) {
|
|
793
930
|
// 催交 cannot substitute for host failure recovery: a real failure with
|
|
794
931
|
// its true cause, through the one settlement authority (ADR 0080).
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
const
|
|
932
|
+
// Same refresh-before-settle seam as the first turn (#1171 F4-R1).
|
|
933
|
+
// Host throw and post-success refresh throw share this catch (#1171 F9).
|
|
934
|
+
const thrown = await settleHostThrowAfterLivePlacementRefresh({
|
|
935
|
+
admitted,
|
|
936
|
+
hostError: error,
|
|
937
|
+
hostTurnCompleted: deliveryHostTurnCompleted,
|
|
938
|
+
request,
|
|
939
|
+
turnRequest,
|
|
940
|
+
lease,
|
|
941
|
+
adapters,
|
|
942
|
+
env,
|
|
943
|
+
io,
|
|
944
|
+
persistRunState,
|
|
945
|
+
notePackageFault: courtScope.notePackageFault,
|
|
946
|
+
...(courtScope.courtAttemptId === undefined
|
|
947
|
+
? {}
|
|
948
|
+
: { courtAttemptId: courtScope.courtAttemptId }),
|
|
949
|
+
});
|
|
950
|
+
turnRequest = thrown.turnRequest;
|
|
798
951
|
return await finishAfterTurn(withProcessCancelSkipAutoResume({
|
|
799
|
-
...settled,
|
|
952
|
+
...thrown.settled,
|
|
800
953
|
turnDispatched: true,
|
|
801
954
|
...deferredPersist,
|
|
802
955
|
}, env.signal));
|
|
@@ -116,16 +116,23 @@ function roleOutcomeFromRows(role, rows) {
|
|
|
116
116
|
// keep that earlier receipt (#1057).
|
|
117
117
|
const payloads = mine.map((row) => row.accepted);
|
|
118
118
|
const latest = terminal[terminal.length - 1];
|
|
119
|
+
const submissionToolCallId = typeof latest.toolCallId === "string" && latest.toolCallId.length > 0
|
|
120
|
+
? latest.toolCallId
|
|
121
|
+
: undefined;
|
|
122
|
+
const belonging = submissionToolCallId === undefined ? {} : { submissionToolCallId };
|
|
119
123
|
if (latest.kind === "audit-escalation") {
|
|
120
124
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
outcome: {
|
|
126
|
+
kind: "audit_escalation", role, status: "audit_escalation", payloads,
|
|
127
|
+
...(Object.hasOwn(latest, "auditReceipt") ? { decisiveFacts: {
|
|
128
|
+
auditEscalationReceipt: latest.auditReceipt,
|
|
129
|
+
...(Object.hasOwn(latest, "auditOfficer") ? { auditEscalationOfficer: latest.auditOfficer } : {}),
|
|
130
|
+
} } : {}),
|
|
131
|
+
},
|
|
132
|
+
...belonging,
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
|
-
return { kind: "accepted", role, payloads };
|
|
135
|
+
return { outcome: { kind: "accepted", role, payloads }, ...belonging };
|
|
129
136
|
}
|
|
130
137
|
async function sealedLedgerOutcome(admitted, role, scope) {
|
|
131
138
|
const home = sealedLedgerHome(admitted);
|
|
@@ -970,17 +977,17 @@ function entriesOf(read) {
|
|
|
970
977
|
/** Sealed seats present the ledger. A session tool error does not invent a host failure. */
|
|
971
978
|
async function settleSealedSeat(admitted, authority, scope, options) {
|
|
972
979
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
973
|
-
const
|
|
974
|
-
if (
|
|
980
|
+
const settled = await sealedLedgerOutcome(admitted, admitted.role, scope);
|
|
981
|
+
if (settled === undefined || (options.acceptedOnly && settled.outcome.kind !== "accepted")) {
|
|
975
982
|
return undefined;
|
|
976
983
|
}
|
|
977
984
|
const read = await readLawfulSettlementEntries(coordinates.sessionFile);
|
|
978
985
|
if (read.kind === "fault") {
|
|
979
986
|
await noteSettlementFault(admitted.runDirectory, scope, `session read failed beside lawful terminal: ${describeErrorIdentity(read.error)}`);
|
|
980
987
|
}
|
|
981
|
-
return finishLawfulSeat(admitted, coordinates, entriesOf(read),
|
|
988
|
+
return finishLawfulSeat(admitted, coordinates, entriesOf(read), settled.outcome, scope, settled.submissionToolCallId);
|
|
982
989
|
}
|
|
983
|
-
async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, scope) {
|
|
990
|
+
async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, scope, submissionToolCallId) {
|
|
984
991
|
const artifacts = scope?.previewOnly === true
|
|
985
992
|
? []
|
|
986
993
|
: await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
|
|
@@ -989,6 +996,9 @@ async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, sco
|
|
|
989
996
|
navigator: extractNavigatorFact(entries),
|
|
990
997
|
artifacts,
|
|
991
998
|
runId: admitted.runId,
|
|
999
|
+
...(submissionToolCallId === undefined || submissionToolCallId.length === 0
|
|
1000
|
+
? {}
|
|
1001
|
+
: { submissionToolCallId }),
|
|
992
1002
|
}, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
|
|
993
1003
|
return attachRecordedSubmissions(admitted, terminal, scope);
|
|
994
1004
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mid-turn ticket report (#1171): roles call this as soon as they know the
|
|
3
|
+
* ticket number; the executing process binds and relocates immediately.
|
|
4
|
+
* When to call lives in this tool's description — not Soul.
|
|
5
|
+
*/
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { runDirectoryFromHostContext } from "./host-contracts.js";
|
|
9
|
+
import { piDurablePrincipalAuthority } from "./pi/durable-principal.js";
|
|
10
|
+
import { bindAdmittedTicketNumber, relocateAdmittedRunToTicket, } from "./public-cli/invocation.js";
|
|
11
|
+
import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf } from "./role-run-placement.js";
|
|
12
|
+
import { readPageSync } from "./run-dossier.js";
|
|
13
|
+
import { readDeclaredTicketNumber } from "./run-ticket-number.js";
|
|
14
|
+
import { isRecord } from "./unknown-value.js";
|
|
15
|
+
export const REPORT_TICKET_TOOL_NAME = "ak_report_ticket";
|
|
16
|
+
const reportTicketArgsSchema = Type.Object({
|
|
17
|
+
ticketNumber: Type.Unknown({
|
|
18
|
+
description: "本票号(正整数、数字串或前导 #N)。已知票号时开工即报;尚无票号时不要调用。",
|
|
19
|
+
}),
|
|
20
|
+
}, { additionalProperties: true });
|
|
21
|
+
export const REPORT_TICKET_TOOL_DESCRIPTION = "向包申报本票号。已知票号时开工第一件事调用;包当场把本腿归到票号目录。"
|
|
22
|
+
+ "尚无票号(例如中书省建票前)不要调用,取得后再报。"
|
|
23
|
+
+ "交卷时若仍未报过且交卷亦未带票号,包会经既有重交通道再问一次。";
|
|
24
|
+
function pageString(page, key) {
|
|
25
|
+
const value = page?.[key];
|
|
26
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the relocate seam's admitted face from durable pages + live principal.
|
|
30
|
+
* The executing process owns this snapshot for the bind/relocate call only.
|
|
31
|
+
*/
|
|
32
|
+
export function admittedForLiveTicketReport(runDirectory, authority) {
|
|
33
|
+
const admittedPage = readPageSync(runDirectory, "admitted") ?? {};
|
|
34
|
+
const invocationPage = readPageSync(runDirectory, "invocation") ?? {};
|
|
35
|
+
const role = pageString(admittedPage, "role") ?? pageString(invocationPage, "role");
|
|
36
|
+
const runId = pageString(admittedPage, "runId") ?? pageString(invocationPage, "runId");
|
|
37
|
+
const bookKey = pageString(admittedPage, "bookKey") ?? pageString(invocationPage, "bookKey");
|
|
38
|
+
const projectRoot = pageString(admittedPage, "projectRoot")
|
|
39
|
+
?? pageString(invocationPage, "projectRoot");
|
|
40
|
+
if (role === undefined || runId === undefined || bookKey === undefined || projectRoot === undefined) {
|
|
41
|
+
throw new Error("ak_report_ticket requires durable role/runId/bookKey/projectRoot on the run");
|
|
42
|
+
}
|
|
43
|
+
const attachments = Array.isArray(admittedPage.attachments)
|
|
44
|
+
? admittedPage.attachments
|
|
45
|
+
: [];
|
|
46
|
+
const sessionDirectory = sessionDirectoryOf(runDirectory);
|
|
47
|
+
const sessionFile = sessionFileOf(runDirectory);
|
|
48
|
+
const principal = authority.seal({ sessionDirectory, sessionFile });
|
|
49
|
+
const ticketNumber = readDeclaredTicketNumber(admittedPage.ticketNumber)
|
|
50
|
+
?? readDeclaredTicketNumber(invocationPage.ticketNumber);
|
|
51
|
+
return {
|
|
52
|
+
role,
|
|
53
|
+
runId,
|
|
54
|
+
bookKey,
|
|
55
|
+
projectRoot,
|
|
56
|
+
instruction: typeof admittedPage.instruction === "string" ? admittedPage.instruction : "",
|
|
57
|
+
instructionEmpty: admittedPage.instructionEmpty === true
|
|
58
|
+
|| (typeof admittedPage.instruction === "string" && admittedPage.instruction.trim() === ""),
|
|
59
|
+
attachments,
|
|
60
|
+
runDirectory,
|
|
61
|
+
principal,
|
|
62
|
+
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Bind + relocate under the existing post-admission seams, then update the
|
|
67
|
+
* executing process's live path handles (HostContext + optional env + setSessionFile).
|
|
68
|
+
* Pi: native SessionManager.setSessionFile so later appends stay on the moved file.
|
|
69
|
+
*/
|
|
70
|
+
export async function reportTicketFromHostContext(context, rawTicketNumber, authority = piDurablePrincipalAuthority) {
|
|
71
|
+
const ticketNumber = readDeclaredTicketNumber(rawTicketNumber);
|
|
72
|
+
if (ticketNumber === undefined) {
|
|
73
|
+
throw new Error("ak_report_ticket requires an identifiable ticketNumber");
|
|
74
|
+
}
|
|
75
|
+
const runDirectory = runDirectoryFromHostContext(context);
|
|
76
|
+
if (runDirectory === undefined) {
|
|
77
|
+
throw new Error("ak_report_ticket requires a per-turn run directory");
|
|
78
|
+
}
|
|
79
|
+
if (!existsSync(runDirectory)) {
|
|
80
|
+
throw new Error(`ak_report_ticket run directory missing: ${runDirectory}`);
|
|
81
|
+
}
|
|
82
|
+
const admitted = admittedForLiveTicketReport(runDirectory, authority);
|
|
83
|
+
const alreadyPlaced = !isUnboundRunDirectory(runDirectory);
|
|
84
|
+
// #1171 B2 / #1025: already under a ticket leaf — do not rewrite durable identity.
|
|
85
|
+
// Placement stays put; no heterogenous-ticket refuse policy (#1171 judge).
|
|
86
|
+
if (!alreadyPlaced) {
|
|
87
|
+
await bindAdmittedTicketNumber(admitted, ticketNumber);
|
|
88
|
+
}
|
|
89
|
+
// Live handles belong with rename ownership — before derived renderCurrentSync
|
|
90
|
+
// (#1171 F4-R2). Reuse relocate's heldLease.relocate seam; keep raw I/O throw.
|
|
91
|
+
const applyLiveHandles = (nextDirectory) => {
|
|
92
|
+
context.runDirectory = nextDirectory;
|
|
93
|
+
// Same-leg spawn env only (#1171 F1): never overwrite another leg's ambient
|
|
94
|
+
// AK_ROLE_RUN_DIR in a shared process. HostContext is the per-turn authority
|
|
95
|
+
// (host-contracts); Pi dedicated children still refresh when env === old path.
|
|
96
|
+
if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
|
|
97
|
+
process.env.AK_ROLE_RUN_DIR = nextDirectory;
|
|
98
|
+
}
|
|
99
|
+
// Native host rebind only — SessionManager has no setSessionDir; appends follow sessionFile.
|
|
100
|
+
context.sessionManager.setSessionFile?.(sessionFileOf(nextDirectory));
|
|
101
|
+
};
|
|
102
|
+
const relocation = alreadyPlaced
|
|
103
|
+
? undefined
|
|
104
|
+
: await relocateAdmittedRunToTicket(admitted, authority, { relocate: applyLiveHandles });
|
|
105
|
+
const nextDirectory = relocation?.newRunDirectory ?? admitted.runDirectory;
|
|
106
|
+
if (relocation === undefined)
|
|
107
|
+
applyLiveHandles(nextDirectory);
|
|
108
|
+
return {
|
|
109
|
+
ticketNumber: alreadyPlaced
|
|
110
|
+
? (admitted.ticketNumber ?? ticketNumber)
|
|
111
|
+
: ticketNumber,
|
|
112
|
+
relocated: relocation !== undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Register the report-ticket tool on the shared host tool mouth (same as submission). */
|
|
116
|
+
export function registerReportTicketTool(roleHost) {
|
|
117
|
+
if (roleHost.getAllTools().some((tool) => tool.name === REPORT_TICKET_TOOL_NAME))
|
|
118
|
+
return;
|
|
119
|
+
roleHost.registerTool({
|
|
120
|
+
name: REPORT_TICKET_TOOL_NAME,
|
|
121
|
+
label: "报票号",
|
|
122
|
+
description: REPORT_TICKET_TOOL_DESCRIPTION,
|
|
123
|
+
promptSnippet: "已知票号时立即申报,包当场归位",
|
|
124
|
+
parameters: reportTicketArgsSchema,
|
|
125
|
+
async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
|
|
126
|
+
const raw = isRecord(parameters) ? parameters.ticketNumber : undefined;
|
|
127
|
+
const details = await reportTicketFromHostContext(ctx, raw);
|
|
128
|
+
return {
|
|
129
|
+
content: [],
|
|
130
|
+
details,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Soft re-ask when a sealed submission still leaves the leg unbound (#1171 / ADR 0073). */
|
|
136
|
+
export const MISSING_TICKET_REASK_MATERIAL = "resources/missing-ticket-reask.md";
|
|
@@ -97,8 +97,7 @@ function roleRunPlacement(ledgerHome, input) {
|
|
|
97
97
|
return {
|
|
98
98
|
runDirectory,
|
|
99
99
|
sessionDirectory,
|
|
100
|
-
sessionFile: sessionFileIn(sessionDirectory)
|
|
101
|
-
attachmentsDirectory: join(runDirectory, "attachments")
|
|
100
|
+
sessionFile: sessionFileIn(sessionDirectory)
|
|
102
101
|
};
|
|
103
102
|
}
|
|
104
103
|
function isUnboundRunDirectory(runDirectory) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Live callers project typed fields; only historical migrators walk durable pages.
|
|
4
4
|
*
|
|
5
5
|
* Scope is typed machine-consumed path fields only — never free text, never
|
|
6
|
-
*
|
|
6
|
+
* caller file-flag path strings. Nested walk is confined to package-owned
|
|
7
7
|
* `session/` seams.
|
|
8
8
|
*/
|
|
9
9
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -36,6 +36,11 @@ const RUN_STATE_PAGE_FIELDS = [
|
|
|
36
36
|
"sessionDirectory",
|
|
37
37
|
"sessionFile",
|
|
38
38
|
];
|
|
39
|
+
/** Typed principal session coordinates rewritten when the owning run leaf moves. */
|
|
40
|
+
export const PRINCIPAL_SESSION_PATH_FIELDS = [
|
|
41
|
+
"sessionDirectory",
|
|
42
|
+
"sessionFile",
|
|
43
|
+
];
|
|
39
44
|
const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
|
|
40
45
|
const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
|
|
41
46
|
const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"];
|
|
@@ -115,6 +120,21 @@ export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, n
|
|
|
115
120
|
{ oldRunDirectory, newRunDirectory },
|
|
116
121
|
]);
|
|
117
122
|
}
|
|
123
|
+
/** Principal session paths only — shared live + durable projection after a run move. */
|
|
124
|
+
export function rewritePrincipalSessionPaths(principal, oldRunDirectory, newRunDirectory) {
|
|
125
|
+
rewriteRunDirectoryPathFields(principal, PRINCIPAL_SESSION_PATH_FIELDS, oldRunDirectory, newRunDirectory);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Live turn request: move runDirectory and rewrite principal session paths in place.
|
|
129
|
+
* Callers that lack authority.seal use this; admitted refresh seals after the same rewrite.
|
|
130
|
+
*/
|
|
131
|
+
export function projectTurnRequestLiveRunDirectory(request, newRunDirectory) {
|
|
132
|
+
const old = request.runDirectory;
|
|
133
|
+
if (old === newRunDirectory)
|
|
134
|
+
return;
|
|
135
|
+
request.runDirectory = newRunDirectory;
|
|
136
|
+
rewritePrincipalSessionPaths(request.principal, old, newRunDirectory);
|
|
137
|
+
}
|
|
118
138
|
/** In-place rewrite of selected fields against a rewrite set. */
|
|
119
139
|
export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
|
|
120
140
|
for (const field of fields) {
|
|
@@ -163,7 +183,7 @@ export function rewriteAdmittedRoleRunPage(page, rewrites) {
|
|
|
163
183
|
rewriteSourceRunLocator(page.sourceRun, rewrites);
|
|
164
184
|
// #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
|
|
165
185
|
if (isRecord(page.principal)) {
|
|
166
|
-
rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal,
|
|
186
|
+
rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
189
|
/** Pre-#1161 `session/auditor-roles/<officer>.pointer.json` (migrator input only). */
|
|
@@ -301,7 +321,7 @@ async function rewriteSessionTranscriptBindings(path, rewrites) {
|
|
|
301
321
|
/**
|
|
302
322
|
* Package-owned nested seams under session/ only:
|
|
303
323
|
* sitian records.jsonl,
|
|
304
|
-
* session transcript typed parent bindings. Never walks
|
|
324
|
+
* session transcript typed parent bindings. Never walks caller file paths.
|
|
305
325
|
*/
|
|
306
326
|
async function rewriteNestedMachinePathPages(pagesDirectory, rewrites) {
|
|
307
327
|
const sessionRoot = join(pagesDirectory, "session");
|
|
@@ -355,7 +375,7 @@ export async function rewriteRoleRunDurablePages(input) {
|
|
|
355
375
|
runState: (page) => {
|
|
356
376
|
rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_FIELDS, rewrites);
|
|
357
377
|
if (isRecord(page.principal)) {
|
|
358
|
-
rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal,
|
|
378
|
+
rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
|
|
359
379
|
}
|
|
360
380
|
// Open court summons: source-run locators only; caller attach paths stay (#1165).
|
|
361
381
|
if (isRecord(page.currentCourt)) {
|
package/package.json
CHANGED
|
@@ -10,11 +10,14 @@ Material is data for the model, not a code contract. Do not invent package flags
|
|
|
10
10
|
|
|
11
11
|
## What goes into the prompt
|
|
12
12
|
|
|
13
|
-
The labor prompt carries **task + paths only**:
|
|
13
|
+
The labor prompt carries **task + caller paths only**:
|
|
14
14
|
|
|
15
15
|
- the task itself: goal, constraints, required output shape;
|
|
16
|
-
- paths the
|
|
17
|
-
|
|
16
|
+
- paths the caller already named (file-flag paths or paths written in the
|
|
17
|
+
dispatch text). The seat finds dossier records itself under the ticket
|
|
18
|
+
directory, or under `unbound/` when there is no ticket. Do not hand the
|
|
19
|
+
engine attachment copies, run-directory pointers, or `AK_ROLE_RUN_DIR`
|
|
20
|
+
(that variable is for the package subprocess only — not a pointer to pass on).
|
|
18
21
|
|
|
19
22
|
**Never paste material bodies into argv or the prompt** — no review bundles,
|
|
20
23
|
no distilled-evidence dumps, no receipt JSON, no full briefs copied out of the
|
|
@@ -39,11 +39,10 @@ returned body.
|
|
|
39
39
|
|
|
40
40
|
`--dangerously-skip-permissions` is required in headless labor: the CLI's
|
|
41
41
|
permission prompts cannot be answered without a TTY and are auto-denied. In
|
|
42
|
-
particular, reading any path outside the project root — such as
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
2026-08-28).
|
|
42
|
+
particular, reading any path outside the project root — such as a caller-given
|
|
43
|
+
absolute path under `~/.ak-roles/` — is refused without the flag ("The read was
|
|
44
|
+
not permitted — I don't have access to that file outside the current worktree")
|
|
45
|
+
and succeeds with it (host-verified 2026-08-28).
|
|
47
46
|
|
|
48
47
|
Prefer `claude --help` on the host over any remembered flag set. Do not wrap
|
|
49
48
|
this engine behind `ak-role` flags.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
本腿仍无票号。请调用 ak_report_ticket 申报,或在交卷里带上 ticketNumber。只问这一次。
|