@akagilnc/pi-workflow-roles 0.1.5679 → 0.1.5698

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.
@@ -9,6 +9,7 @@ import { runDirectoryFromHostContext } from "./host-contracts.js";
9
9
  import { piDurablePrincipalAuthority } from "./pi/durable-principal.js";
10
10
  import { bindAdmittedTicketNumber, relocateAdmittedRunToTicket, } from "./public-cli/invocation.js";
11
11
  import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf } from "./role-run-placement.js";
12
+ import { rewriteRunDirectoryPathValue } from "./role-run-path-rewrite.js";
12
13
  import { readPageSync } from "./run-dossier.js";
13
14
  import { readDeclaredTicketNumber } from "./run-ticket-number.js";
14
15
  import { isRecord } from "./unknown-value.js";
@@ -89,6 +90,11 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
89
90
  // Live handles belong with rename ownership — before derived renderCurrentSync
90
91
  // (#1171 F4-R2). Reuse relocate's heldLease.relocate seam; keep raw I/O throw.
91
92
  const applyLiveHandles = (nextDirectory) => {
93
+ // Capture live session handle before ownership transfer. After relocate the
94
+ // write handle must share host-authority coords with the principal (#1183):
95
+ // project the prior path onto the new leaf — never rebuild default
96
+ // session.jsonl over a non-default seal (and do not trust a setter rebuild).
97
+ const priorSessionFile = context.sessionManager.getSessionFile?.();
92
98
  context.runDirectory = nextDirectory;
93
99
  // Same-leg spawn env only (#1171 F1): never overwrite another leg's ambient
94
100
  // AK_ROLE_RUN_DIR in a shared process. HostContext is the per-turn authority
@@ -96,8 +102,12 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
96
102
  if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
97
103
  process.env.AK_ROLE_RUN_DIR = nextDirectory;
98
104
  }
99
- // Native host rebind only — SessionManager has no setSessionDir; appends follow sessionFile.
100
- context.sessionManager.setSessionFile?.(sessionFileOf(nextDirectory));
105
+ const projectedSessionFile = typeof priorSessionFile === "string" && priorSessionFile.trim() !== ""
106
+ ? rewriteRunDirectoryPathValue(priorSessionFile, runDirectory, nextDirectory)
107
+ : sessionFileOf(nextDirectory);
108
+ if (typeof projectedSessionFile === "string") {
109
+ context.sessionManager.setSessionFile?.(projectedSessionFile);
110
+ }
101
111
  };
102
112
  const relocation = alreadyPlaced
103
113
  ? undefined
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Sole pure authority for typed run-directory path rewrite / projection.
3
+ * No dossier I/O — run-dossier render and role-run-relocation durable walks
4
+ * both consume this module (avoids a cycle through page readers).
5
+ *
6
+ * Scope is typed machine-consumed path fields only — never free text, never
7
+ * caller file-flag path strings.
8
+ */
9
+ import { sep } from "node:path";
10
+ import { isRecord } from "./unknown-value.js";
11
+ export const ADMITTED_PAGE_PATH_FIELDS = [
12
+ "runDirectory",
13
+ "admittedRequestPath", // pre-#1161 pages only
14
+ "sessionDirectory",
15
+ "sessionFile",
16
+ // #1165/#1168: caller file-flag paths stay as given.
17
+ "mergerInputPath",
18
+ "sourceRunPath",
19
+ ];
20
+ export const INVOCATION_PAGE_PATH_FIELDS = [
21
+ "runDirectory",
22
+ "sessionDirectory",
23
+ "sessionFile",
24
+ ];
25
+ export const RUN_STATE_PAGE_PATH_FIELDS = [
26
+ "runDirectory",
27
+ "admittedRequestPath", // pre-#1161 pages only
28
+ "sessionDirectory",
29
+ "sessionFile",
30
+ ];
31
+ /** Typed principal session coordinates rewritten when the owning run leaf moves. */
32
+ export const PRINCIPAL_SESSION_PATH_FIELDS = [
33
+ "sessionDirectory",
34
+ "sessionFile",
35
+ ];
36
+ const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
37
+ const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
38
+ /** Rewrite one path value that points at or under oldRunDirectory. */
39
+ export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirectory) {
40
+ if (typeof value !== "string")
41
+ return value;
42
+ if (value === oldRunDirectory)
43
+ return newRunDirectory;
44
+ const prefix = `${oldRunDirectory}${sep}`;
45
+ if (value.startsWith(prefix)) {
46
+ return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
47
+ }
48
+ return value;
49
+ }
50
+ /**
51
+ * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
52
+ * nested run path is not partially claimed by a shorter sibling prefix.
53
+ */
54
+ export function rewriteRunDirectoryPathValueAgainstRewrites(value, rewrites) {
55
+ if (typeof value !== "string" || rewrites.length === 0)
56
+ return value;
57
+ let best;
58
+ for (const rewrite of rewrites) {
59
+ if (value === rewrite.oldRunDirectory ||
60
+ value.startsWith(`${rewrite.oldRunDirectory}${sep}`)) {
61
+ if (best === undefined ||
62
+ rewrite.oldRunDirectory.length > best.oldRunDirectory.length) {
63
+ best = rewrite;
64
+ }
65
+ }
66
+ }
67
+ if (best === undefined)
68
+ return value;
69
+ return rewriteRunDirectoryPathValue(value, best.oldRunDirectory, best.newRunDirectory);
70
+ }
71
+ /** In-place rewrite of selected fields on one record (single pair). */
72
+ export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, newRunDirectory) {
73
+ rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
74
+ { oldRunDirectory, newRunDirectory },
75
+ ]);
76
+ }
77
+ /** In-place rewrite of selected fields against a rewrite set. */
78
+ export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
79
+ for (const field of fields) {
80
+ if (field in record) {
81
+ record[field] = rewriteRunDirectoryPathValueAgainstRewrites(record[field], rewrites);
82
+ }
83
+ }
84
+ }
85
+ /** Principal session paths only — shared live + durable projection after a run move. */
86
+ export function rewritePrincipalSessionPaths(principal, oldRunDirectory, newRunDirectory) {
87
+ rewriteRunDirectoryPathFields(principal, PRINCIPAL_SESSION_PATH_FIELDS, oldRunDirectory, newRunDirectory);
88
+ }
89
+ /**
90
+ * Live turn request: move runDirectory and keep principal session paths on the
91
+ * new leaf. Sole path = host authority decode → rewrite coords copy → seal
92
+ * (#1183 / #636). No shape-based in-place rewrite and no silent skip.
93
+ */
94
+ export function projectTurnRequestLiveRunDirectory(request, newRunDirectory, authority) {
95
+ const old = request.runDirectory;
96
+ if (old === newRunDirectory)
97
+ return;
98
+ const coords = { ...authority.decode(request.principal) };
99
+ rewritePrincipalSessionPaths(coords, old, newRunDirectory);
100
+ const sealed = authority.seal(coords);
101
+ request.runDirectory = newRunDirectory;
102
+ request.principal = sealed;
103
+ }
104
+ /** Typed notary/source-run locator: only runDirectory is a path. */
105
+ export function rewriteSourceRunLocator(value, rewrites) {
106
+ if (!isRecord(value))
107
+ return;
108
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SOURCE_RUN_LOCATOR_FIELDS, rewrites);
109
+ }
110
+ /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
111
+ export function rewriteSummonsMaterials(value, rewrites) {
112
+ if (!isRecord(value))
113
+ return;
114
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SUMMONS_PATH_FIELDS, rewrites);
115
+ rewriteSourceRunLocator(value.sourceRun, rewrites);
116
+ // #1165: summons.attachmentPaths are caller paths — do not rewrite.
117
+ }
118
+ /** Project one admitted page in memory after its containing run moved. */
119
+ export function rewriteAdmittedRoleRunPage(page, rewrites) {
120
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_PATH_FIELDS, rewrites);
121
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
122
+ // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
123
+ if (isRecord(page.principal)) {
124
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
125
+ }
126
+ }
127
+ /**
128
+ * #1183: current.json location fields follow the directory being rendered.
129
+ * Fact rows stay as written (append-only); only the rendering projects stored→actual.
130
+ * Returns the same object when already aligned. Caller file-flag paths stay opaque.
131
+ */
132
+ export function projectRenderedPagePaths(page, actualRunDirectory) {
133
+ const stored = typeof page.runDirectory === "string" && page.runDirectory.trim() !== ""
134
+ ? page.runDirectory
135
+ : undefined;
136
+ if (stored === undefined || stored === actualRunDirectory)
137
+ return page;
138
+ const projected = structuredClone(page);
139
+ const rewrites = [
140
+ { oldRunDirectory: stored, newRunDirectory: actualRunDirectory },
141
+ ];
142
+ // Admitted field set is the location-field superset of invocation; runState adds
143
+ // principal (covered) and optional currentCourt summons under this run.
144
+ rewriteAdmittedRoleRunPage(projected, rewrites);
145
+ if (isRecord(projected.currentCourt)) {
146
+ rewriteSummonsMaterials(projected.currentCourt.summons, rewrites);
147
+ }
148
+ return projected;
149
+ }
@@ -1,64 +1,27 @@
1
1
  /**
2
- * Single authority for rewriting durable run pages after a run directory moves.
3
- * Live callers project typed fields; only historical migrators walk durable pages.
2
+ * Durable run-page path rewrite after a run directory moves, plus retained-path
3
+ * matching for live lookup. Pure path rewrite rules live in role-run-path-rewrite.ts
4
+ * (sole authority); this module owns I/O walks that need dossier page readers.
4
5
  *
5
6
  * Scope is typed machine-consumed path fields only — never free text, never
6
7
  * caller file-flag path strings. Nested walk is confined to package-owned
7
8
  * `session/` seams.
8
9
  */
9
10
  import { readdir, readFile, writeFile } from "node:fs/promises";
10
- import { dirname, join, sep } from "node:path";
11
+ import { dirname, join } from "node:path";
11
12
  import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.js";
13
+ import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
12
14
  import { readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
13
15
  import { findRoleRunDirectory, roleRunPlacement } from "./role-run-placement.js";
14
- import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
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";
15
17
  import { readPageSync, updateSectionSync } from "./run-dossier.js";
16
- import { isRecord, isEnoent } from "./unknown-value.js";
17
- const ADMITTED_PAGE_FIELDS = [
18
- "runDirectory",
19
- "admittedRequestPath", // pre-#1161 pages only
20
- "sessionDirectory",
21
- "sessionFile",
22
- // #1165/#1168: caller file-flag paths (requestManifestPath, prerequisitesPath)
23
- // stay as given. Obsolete copy fields (taskPath/packetPath) are not rewritten —
24
- // stock volumes keep their bytes; live relocate no longer maintains them.
25
- "mergerInputPath",
26
- "sourceRunPath",
27
- ];
28
- const INVOCATION_PAGE_FIELDS = [
29
- "runDirectory",
30
- "sessionDirectory",
31
- "sessionFile",
32
- ];
33
- const RUN_STATE_PAGE_FIELDS = [
34
- "runDirectory",
35
- "admittedRequestPath", // pre-#1161 pages only
36
- "sessionDirectory",
37
- "sessionFile",
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
- ];
44
- const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
45
- const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
18
+ import { isEnoent, isRecord } from "./unknown-value.js";
19
+ // Re-export the pure path API so existing import sites stay on this module path.
20
+ export { PRINCIPAL_SESSION_PATH_FIELDS, projectTurnRequestLiveRunDirectory, rewriteAdmittedRoleRunPage, rewritePrincipalSessionPaths, rewriteRunDirectoryPathFields, rewriteRunDirectoryPathFieldsAgainstRewrites, rewriteRunDirectoryPathValue, rewriteRunDirectoryPathValueAgainstRewrites, } from "./role-run-path-rewrite.js";
46
21
  const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"];
47
22
  const SITIAN_RECORD_FIELDS = ["sessionParent"];
48
23
  const SESSION_HEADER_FIELDS = ["parentSession"];
49
24
  const BINDING_PARENT_FIELDS = ["sessionFile"];
50
- /** Rewrite one path value that points at or under oldRunDirectory. */
51
- export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirectory) {
52
- if (typeof value !== "string")
53
- return value;
54
- if (value === oldRunDirectory)
55
- return newRunDirectory;
56
- const prefix = `${oldRunDirectory}${sep}`;
57
- if (value.startsWith(prefix)) {
58
- return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
59
- }
60
- return value;
61
- }
62
25
  /** Match physical aliases or a normal unbound→ticket bind, never historical migration layouts. */
63
26
  export async function retainedRunPathsMatch(recordedPath, currentPath, currentRunDirectory) {
64
27
  if (recordedPath === currentPath)
@@ -93,56 +56,6 @@ export async function retainedRunPathsMatch(recordedPath, currentPath, currentRu
93
56
  }
94
57
  return false;
95
58
  }
96
- /**
97
- * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
98
- * nested run path is not partially claimed by a shorter sibling prefix.
99
- */
100
- export function rewriteRunDirectoryPathValueAgainstRewrites(value, rewrites) {
101
- if (typeof value !== "string" || rewrites.length === 0)
102
- return value;
103
- let best;
104
- for (const rewrite of rewrites) {
105
- if (value === rewrite.oldRunDirectory ||
106
- value.startsWith(`${rewrite.oldRunDirectory}${sep}`)) {
107
- if (best === undefined ||
108
- rewrite.oldRunDirectory.length > best.oldRunDirectory.length) {
109
- best = rewrite;
110
- }
111
- }
112
- }
113
- if (best === undefined)
114
- return value;
115
- return rewriteRunDirectoryPathValue(value, best.oldRunDirectory, best.newRunDirectory);
116
- }
117
- /** In-place rewrite of selected fields on one record (single pair). */
118
- export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, newRunDirectory) {
119
- rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
120
- { oldRunDirectory, newRunDirectory },
121
- ]);
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
- }
138
- /** In-place rewrite of selected fields against a rewrite set. */
139
- export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
140
- for (const field of fields) {
141
- if (field in record) {
142
- record[field] = rewriteRunDirectoryPathValueAgainstRewrites(record[field], rewrites);
143
- }
144
- }
145
- }
146
59
  function collectRewrites(input) {
147
60
  const own = {
148
61
  oldRunDirectory: input.oldRunDirectory,
@@ -163,29 +76,6 @@ function collectRewrites(input) {
163
76
  }
164
77
  return out;
165
78
  }
166
- /** Typed notary/source-run locator: only runDirectory is a path. */
167
- function rewriteSourceRunLocator(value, rewrites) {
168
- if (!isRecord(value))
169
- return;
170
- rewriteRunDirectoryPathFieldsAgainstRewrites(value, SOURCE_RUN_LOCATOR_FIELDS, rewrites);
171
- }
172
- /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
173
- function rewriteSummonsMaterials(value, rewrites) {
174
- if (!isRecord(value))
175
- return;
176
- rewriteRunDirectoryPathFieldsAgainstRewrites(value, SUMMONS_PATH_FIELDS, rewrites);
177
- rewriteSourceRunLocator(value.sourceRun, rewrites);
178
- // #1165: summons.attachmentPaths are caller paths — do not rewrite.
179
- }
180
- /** Project one admitted page in memory after its containing run moved. */
181
- export function rewriteAdmittedRoleRunPage(page, rewrites) {
182
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
183
- rewriteSourceRunLocator(page.sourceRun, rewrites);
184
- // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
185
- if (isRecord(page.principal)) {
186
- rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
187
- }
188
- }
189
79
  /** Pre-#1161 `session/auditor-roles/<officer>.pointer.json` (migrator input only). */
190
80
  async function rewriteLegacyOfficerPointerFile(path, rewrites) {
191
81
  const page = JSON.parse(await readFile(path, "utf8"));
@@ -371,9 +261,9 @@ export async function rewriteRoleRunDurablePages(input) {
371
261
  // One rewriter per page kind; each keeps its own field list.
372
262
  const pageRewriters = {
373
263
  admitted: (page) => rewriteAdmittedRoleRunPage(page, rewrites),
374
- invocation: (page) => rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_FIELDS, rewrites),
264
+ invocation: (page) => rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_PATH_FIELDS, rewrites),
375
265
  runState: (page) => {
376
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_FIELDS, rewrites);
266
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_PATH_FIELDS, rewrites);
377
267
  if (isRecord(page.principal)) {
378
268
  rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
379
269
  }
@@ -20,6 +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
  import { sessionFileOf } from "./role-run-placement.js";
23
+ import { projectRenderedPagePaths } from "./role-run-path-rewrite.js";
23
24
  import { RUN_CURRENT_FILE, RUN_HISTORY_FILE, RUN_LOG_FILE, RUN_STATE_FILE, OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.js";
24
25
  import { appendSitianRecord } from "./sitian-appender.js";
25
26
  import { parseSitianRecordText } from "./sitian-reader.js";
@@ -144,8 +145,16 @@ function render(runDirectory, history, state, log, unreadable) {
144
145
  const whole = {};
145
146
  for (const section of ["invocation", "admitted", "runState", "terminal"]) {
146
147
  const page = lastPayloadOfKind(state, PAGE_ROW_KIND[section]);
147
- if (page !== undefined)
148
+ if (page === undefined)
149
+ continue;
150
+ // #1183: location fields track the directory being rendered; fact rows stay put.
151
+ if ((section === "invocation" || section === "admitted" || section === "runState")
152
+ && isRecord(page)) {
153
+ whole[section] = projectRenderedPagePaths(page, runDirectory);
154
+ }
155
+ else {
148
156
  whole[section] = page;
157
+ }
149
158
  }
150
159
  let latest;
151
160
  for (const row of history) {
@@ -16,6 +16,7 @@ export const HOST_SESSION_ID_RECORD_KIND = "host-session-id";
16
16
  export function createSessionIdentityAuthority(authority, hostName) {
17
17
  const runDirectoryOf = (principal) => dirname(authority.decode(principal).sessionDirectory);
18
18
  return {
19
+ principalAuthority: authority,
19
20
  resolveSessionFile(principal) {
20
21
  return authority.decode(principal).sessionFile;
21
22
  },
@@ -6,7 +6,9 @@ import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from
6
6
  import { findRunDirectoryById } from "./public-cli/run-lifecycle.js";
7
7
  import { isCorrectableExecuteError } from "./submission-correctable-error.js";
8
8
  import { failOnInfrastructureFailureDeclaration, infrastructureFailureDiagnostic } from "./package-contracts/terminating-infrastructure.js";
9
+ import { reportTicketFromHostContext } from "./report-ticket-tool.js";
9
10
  import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.js";
11
+ import { readDeclaredTicketNumber } from "./run-ticket-number.js";
10
12
  import { isRecord, errorText } from "./unknown-value.js";
11
13
  /**
12
14
  * Admitted run identity for the ledger subject.
@@ -374,6 +376,15 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
374
376
  host.registerTool({
375
377
  ...tool,
376
378
  async execute(toolCallId, params, signal, update, context) {
379
+ // #1183: valid typed ticket on the submission is identity acquisition —
380
+ // bind/relocate once here, before reask / ledger restore / seal outcomes.
381
+ // Host return must not be the first placement (dossier-topology).
382
+ const typedTicket = isRecord(params)
383
+ ? readDeclaredTicketNumber(params.ticketNumber)
384
+ : undefined;
385
+ if (typedTicket !== undefined) {
386
+ await reportTicketFromHostContext(context, typedTicket);
387
+ }
377
388
  const runId = runIdentity(context);
378
389
  const attemptId = attemptIdentity(context, runId);
379
390
  const state = await stateFor(context, runId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5679",
3
+ "version": "0.1.5698",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,13 +1,6 @@
1
- ## 到场阶段(#1160 / ADR 0073)
1
+ ## 备答
2
2
 
3
- 阶段行为说明住本手册。代码不写阶段句,也不把本手册再拼进用户消息。
4
-
5
- - **备答**:父衙门一开始即并行准备。自行判断当前进度——有票时读票卷宗里实际发生的角色运行,必要时看票面;不把过去建议或建议送达当作已经走过的站。按主衙门可能的结局 status 预写建议,经 `byStatus` 提交(键为 status 字面量,值为建议正文)。无状态分叉时可用单条 `prose`。
6
- - **取用**:父衙门交卷后,代码只按实际 status 取对应原文,不再起模型轮次。没有对应建议则如实无建议。主衙门结束后最多再等十秒;到时停止等待,不扣押主结果。
7
-
8
- ## 宿主轴(与角色命令面)
9
-
10
- 调用与 host 解析见 `ak-role help`、`ak-role help navigator`;机构边界见 [ADR 0082](https://github.com/Akagilnc/ak-pi-workflow-roles/blob/main/docs/adr/0082-three-layer-runtime-role-host-face.md),席位解析见 [config 实现](../src/public-cli/config.ts)。
3
+ 父衙门一开始即并行准备。有票时读票卷宗里实际发生的角色运行,必要时看票面;不读过去的建议。按主衙门可能的结局 status 预写建议,经 `byStatus` 提交(键为 status 字面量,值为建议正文)。无状态分叉时可用单条 `prose`。
11
4
 
12
5
  ## 常用交付线
13
6
 
@@ -32,12 +25,9 @@ collector(收齐 current-head 材料)
32
25
  ├─ 有问题 → 修内司 → 大理寺
33
26
  └─ 收敛 → 调用者合并、关票
34
27
 
35
- 既往大理寺收敛不替代其后新一轮线上材料的裁决;缺失 reviewer 腿只是 degraded coverage,不改变 collector 后到大理寺的接力。
36
-
37
28
  ## 未完交棒(unfinished)
38
29
 
39
30
  修内司 / 将作监 apply 以 unfinished 交棒时,工作仍未结清:
40
31
  → 续修(通常仍是同一 worker 的 apply),不要送大理寺
41
32
 
42
33
  refused / partially_completed 是已结清结算,送大理寺审计仍合法。
43
- unfinished 不是失败清理信号,也不豁免任何验收。
@@ -1,7 +1,5 @@
1
1
  # Navigator Soul
2
2
 
3
- 根据工作大小与控制性 authority,判断一条最低成本且安全的后续角色路线。保留必要的角色/phase顺序,避免重复站点;路线是建议,不是授权、执行或约束。
3
+ 按当前结算判断最低成本且安全的下一站:不跳必要站,不加多余站。只是建议,不授权、不执行。
4
4
 
5
- 建议针对**当前角色刚完成的结算**之后的下一步。既有 settlement 历史是背景,不能用更早的收敛或收口替代本轮刚产出的材料/劳动;合并收口只在大理寺对本轮材料收敛之后才合适。
6
-
7
- 当前 plan 阶段停用。
5
+ 一句话,20 字以内。plan 阶段停用。
@@ -42,6 +42,7 @@ function createComposedAcpRoleTurnHost(
42
42
  request,
43
43
  dependencies: config.roleRuntimeDependencies,
44
44
  sessionFile: config.sessionIdentity.resolveSessionFile(request.principal),
45
+ principalAuthority: config.sessionIdentity.principalAuthority,
45
46
  socketPath: config.socketPath?.(request) ?? `/tmp/ak-acp-mcp-${randomUUID()}.sock`,
46
47
  }),
47
48
  });
@@ -382,7 +382,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
382
382
  // MCP advertises tool.parameters; prepared.jsonSchema is the headless
383
383
  // draft-07 stamp of that clone — record the delivered parameters face.
384
384
  const { $schema: _headlessStamp, ...deliveredSchema } = prepared.jsonSchema;
385
- await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
385
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority), {
386
386
  systemPrompt: systemPromptOverride,
387
387
  outputSchema: deliveredSchema,
388
388
  }, "acp-host");
@@ -503,7 +503,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
503
503
  outcome = { ...facts, ...(knownFailure === undefined ? {} : { knownFailure }) };
504
504
  }
505
505
  if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
506
- const live = await trySyncTurnRequestLivePlacement(request);
506
+ const live = await trySyncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority);
507
507
  if ("runDirectory" in live) {
508
508
  try {
509
509
  copyAndRecordHostDossier({
@@ -7,6 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { basename } from "node:path";
8
8
 
9
9
  import type {
10
+ DurablePrincipalAuthority,
10
11
  RoleTurnHost,
11
12
  RoleTurnKnownFailure,
12
13
  RoleTurnRequest,
@@ -46,10 +47,12 @@ export async function resolveLiveRunDirectoryPath(
46
47
  /**
47
48
  * Prefer the request path when it still exists; otherwise re-locate by run id
48
49
  * (resolveLiveRunDirectoryPath). Mutates the live request so later turn-record /
49
- * exit-copy / fault notes stay on the new leaf.
50
+ * exit-copy / fault notes stay on the new leaf. Authority is required so opaque
51
+ * principal wire is resealed — never shape-guessed in place (#1183).
50
52
  */
51
53
  export async function syncTurnRequestLivePlacement(
52
54
  request: RoleTurnRequest,
55
+ authority: DurablePrincipalAuthority,
53
56
  ): Promise<string> {
54
57
  const live = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
55
58
  if (live === undefined) {
@@ -59,13 +62,7 @@ export async function syncTurnRequestLivePlacement(
59
62
  );
60
63
  }
61
64
  if (live !== request.runDirectory) {
62
- projectTurnRequestLiveRunDirectory(
63
- request as {
64
- runDirectory: string;
65
- principal: { sessionDirectory?: string; sessionFile?: string };
66
- },
67
- live,
68
- );
65
+ projectTurnRequestLiveRunDirectory(request, live, authority);
69
66
  }
70
67
  return live;
71
68
  }
@@ -76,37 +73,54 @@ export async function syncTurnRequestLivePlacement(
76
73
  */
77
74
  export async function trySyncTurnRequestLivePlacement(
78
75
  request: RoleTurnRequest,
76
+ authority: DurablePrincipalAuthority,
79
77
  ): Promise<{ readonly runDirectory: string } | { readonly resolveError: unknown }> {
80
78
  try {
81
- return { runDirectory: await syncTurnRequestLivePlacement(request) };
79
+ return { runDirectory: await syncTurnRequestLivePlacement(request, authority) };
82
80
  } catch (resolveError) {
83
81
  return { resolveError };
84
82
  }
85
83
  }
86
84
 
87
85
  /**
88
- * Retain a package fault on the live leaf when locatable. Lookup failure must not
89
- * replace the original fault or block later cleanup — reuse retainPackageFault when
90
- * the leaf exists; otherwise present both causes on stderr (Pi transport path shape).
86
+ * Retain a package fault on the live leaf when locatable. Directory lookup only —
87
+ * does not mutate live principal identity (#1183). Lookup failure must not replace
88
+ * the original fault or block later cleanup.
91
89
  */
92
90
  export async function retainPackageFaultBesideLivePlacement(
93
91
  request: RoleTurnRequest,
94
92
  input: { readonly diagnostic: string; readonly error?: unknown },
95
93
  ): Promise<void> {
96
- const live = await trySyncTurnRequestLivePlacement(request);
97
- if ("resolveError" in live) {
98
- const resolveDiagnostic =
99
- `live run path resolve failed beside fault: ${describeErrorIdentity(live.resolveError)}`;
94
+ let runDirectory = request.runDirectory;
95
+ if (!existsSync(runDirectory)) {
100
96
  try {
101
- process.stderr.write(`${input.diagnostic}\n`);
102
- process.stderr.write(`${resolveDiagnostic}\n`);
103
- } catch {
104
- // Best-effort presentation beside an already-chosen terminal.
97
+ const found = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
98
+ if (found === undefined) {
99
+ const resolveDiagnostic =
100
+ `live run path resolve failed beside fault: admitted run missing under ${request.home}`;
101
+ try {
102
+ process.stderr.write(`${input.diagnostic}\n`);
103
+ process.stderr.write(`${resolveDiagnostic}\n`);
104
+ } catch {
105
+ // Best-effort presentation beside an already-chosen terminal.
106
+ }
107
+ return;
108
+ }
109
+ runDirectory = found;
110
+ } catch (resolveError) {
111
+ const resolveDiagnostic =
112
+ `live run path resolve failed beside fault: ${describeErrorIdentity(resolveError)}`;
113
+ try {
114
+ process.stderr.write(`${input.diagnostic}\n`);
115
+ process.stderr.write(`${resolveDiagnostic}\n`);
116
+ } catch {
117
+ // Best-effort presentation beside an already-chosen terminal.
118
+ }
119
+ return;
105
120
  }
106
- return;
107
121
  }
108
122
  await retainPackageFault({
109
- runDirectory: live.runDirectory,
123
+ runDirectory,
110
124
  diagnostic: input.diagnostic,
111
125
  ...(Object.hasOwn(input, "error") ? { error: input.error } : {}),
112
126
  });
@@ -54,6 +54,7 @@ export function createProductionHeadlessRoleTurnHost(
54
54
  request,
55
55
  dependencies: roleRuntimeDependencies,
56
56
  sessionFile: sessionIdentity.resolveSessionFile(request.principal),
57
+ principalAuthority,
57
58
  // Same MCP relay as ACP so intermediate AK tools stay reachable;
58
59
  // headless adapter projects the row into --mcp-config.
59
60
  socketPath: `/tmp/ak-headless-mcp-${randomUUID()}.sock`,
@@ -564,7 +564,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
564
564
  }
565
565
 
566
566
  // What this CLI start was given: one history record per start.
567
- await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
567
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority), {
568
568
  systemPrompt,
569
569
  ...(startedWithSchema === undefined ? {} : { outputSchema: startedWithSchema }),
570
570
  }, "headless-host");
@@ -748,7 +748,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
748
748
  } finally {
749
749
  // Cleanup must not depend on live-path sync: lookup failure stays beside
750
750
  // the original cleanup/copy work and never skips rm(inputsDirectory).
751
- const live = await trySyncTurnRequestLivePlacement(request);
751
+ const live = await trySyncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority);
752
752
  const liveRunDirectory = "runDirectory" in live ? live.runDirectory : undefined;
753
753
  if (liveRunDirectory === undefined && "resolveError" in live) {
754
754
  await retainPackageFaultBesideLivePlacement(request, {
@@ -500,7 +500,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
500
500
  // bind projects host.sessions; copy records the landing. Host adapters do
501
501
  // not write current.json — public bind/settlement seams own that render.
502
502
  try {
503
- await syncTurnRequestLivePlacement(request);
503
+ await syncTurnRequestLivePlacement(request, config.principalAuthority);
504
504
  const { sessionFile, sessionDirectory } = config.principalAuthority.decode(request.principal);
505
505
  const sessionId = await readPiSessionHeaderId(sessionFile);
506
506
  if (sessionId !== undefined) {
@@ -75,4 +75,6 @@ export type SessionIdentityAuthority = Readonly<{
75
75
  bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
76
76
  /** Durable principal session path for layout ownership — not a rebuild source (#617 DK-4). */
77
77
  resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
78
+ /** Host authority that sealed the principal — mid-turn relocate reseals through it (#1183). */
79
+ readonly principalAuthority: import("./host-contracts.ts").DurablePrincipalAuthority;
78
80
  }>;