@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193

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.
Files changed (108) hide show
  1. package/dist/acp-host/production-host.js +2368 -1155
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-auditor.js +1 -1
  7. package/dist/doctor-evidence.js +3 -2
  8. package/dist/dossier-resolution.js +21 -8
  9. package/dist/gatekeeper-role.js +34 -27
  10. package/dist/headless-host/description.js +7 -8
  11. package/dist/headless-host/production-host.js +2375 -1149
  12. package/dist/host-contracts.js +16 -0
  13. package/dist/inspector-contracts.js +8 -0
  14. package/dist/migrate-book-topology.js +278917 -0
  15. package/dist/navigator-public-session.js +22 -25
  16. package/dist/notary-source-run.js +7 -4
  17. package/dist/packaged-role-registry.js +1 -1
  18. package/dist/pi/durable-principal.js +9 -6
  19. package/dist/pi/role-turn-host.js +43 -13
  20. package/dist/public-cli/auto-resume.js +2 -5
  21. package/dist/public-cli/case-dossier-delivery.js +60 -1
  22. package/dist/public-cli/diarist-run.js +27 -25
  23. package/dist/public-cli/inspector-run.js +17 -3
  24. package/dist/public-cli/instruction-seat-run.js +17 -17
  25. package/dist/public-cli/invocation.js +127 -63
  26. package/dist/public-cli/main.js +1685 -696
  27. package/dist/public-cli/notary-run.js +7 -4
  28. package/dist/public-cli/option-definitions.js +2 -2
  29. package/dist/public-cli/post-admission.js +113 -71
  30. package/dist/public-cli/run-lifecycle.js +209 -58
  31. package/dist/public-cli/seat-ticket-binding.js +3 -6
  32. package/dist/public-cli/settlement.js +29 -10
  33. package/dist/public-role-summons.js +8 -0
  34. package/dist/role-run-placement.js +84 -0
  35. package/dist/role-run-relocation.js +358 -0
  36. package/dist/run-terminal-artifacts.js +2 -1
  37. package/dist/run-ticket-number.js +51 -32
  38. package/dist/sitian-appender.js +11 -26
  39. package/dist/submission-ledger.js +106 -25
  40. package/dist/ticket-provenance.js +3 -1
  41. package/dist/user-dialogue-stdin.js +33 -0
  42. package/extensions/role-runtime.ts +1 -0
  43. package/package.json +3 -2
  44. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  45. package/scripts/build-package.mjs +34 -2
  46. package/souls/fixer.md +3 -2
  47. package/src/analyst-ledger.ts +14 -15
  48. package/src/archivist-record-entry.ts +285 -38
  49. package/src/archivist-record-topology.ts +43 -18
  50. package/src/auditor-dossier-tool.ts +2 -1
  51. package/src/book-topology-migration-placement.ts +493 -0
  52. package/src/book-topology-migration.ts +220 -0
  53. package/src/book-topology-migrators-t11.ts +856 -0
  54. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  55. package/src/book-topology-partition-migrators.ts +25 -0
  56. package/src/book-topology-record-class-migrators.ts +779 -0
  57. package/src/book-topology-runs-migrator.ts +244 -0
  58. package/src/diarist-contracts.ts +116 -0
  59. package/src/diarist-role.ts +6 -0
  60. package/src/diarist.ts +2 -0
  61. package/src/doctor-auditor.ts +1 -1
  62. package/src/doctor-evidence.ts +4 -1
  63. package/src/doctor-role.ts +1 -1
  64. package/src/dossier-resolution.ts +26 -8
  65. package/src/external-host-turn-loop.ts +9 -0
  66. package/src/gatekeeper-pass-envelope.ts +6 -0
  67. package/src/gatekeeper-role.ts +47 -28
  68. package/src/headless-host/description.ts +8 -11
  69. package/src/headless-host/role-turn-host.ts +20 -5
  70. package/src/host-contracts.ts +22 -3
  71. package/src/inspector-contracts.ts +7 -0
  72. package/src/judge-role.ts +4 -0
  73. package/src/navigator-public-session.ts +33 -30
  74. package/src/navigator-work-context.ts +3 -4
  75. package/src/notary-role.ts +1 -0
  76. package/src/notary-source-run.ts +9 -4
  77. package/src/packaged-role-registry.ts +1 -1
  78. package/src/pi/adapter.ts +19 -4
  79. package/src/pi/durable-principal.ts +9 -13
  80. package/src/pi/role-turn-host.ts +48 -14
  81. package/src/public-cli/auto-resume.ts +2 -6
  82. package/src/public-cli/case-dossier-delivery.ts +86 -1
  83. package/src/public-cli/countersign-run.ts +182 -32
  84. package/src/public-cli/diarist-run.ts +34 -26
  85. package/src/public-cli/inspector-run.ts +26 -2
  86. package/src/public-cli/instruction-seat-run.ts +24 -20
  87. package/src/public-cli/invocation.ts +194 -58
  88. package/src/public-cli/notary-run.ts +13 -4
  89. package/src/public-cli/option-definitions.ts +2 -2
  90. package/src/public-cli/post-admission.ts +147 -78
  91. package/src/public-cli/run-lifecycle.ts +258 -65
  92. package/src/public-cli/seat-ticket-binding.ts +3 -6
  93. package/src/public-cli/settlement.ts +38 -10
  94. package/src/public-role-summons.ts +31 -7
  95. package/src/role-envelope.ts +9 -30
  96. package/src/role-run-placement.ts +128 -0
  97. package/src/role-run-relocation.ts +491 -0
  98. package/src/role-runtime-dependencies.ts +1 -0
  99. package/src/role-runtime.ts +106 -24
  100. package/src/run-terminal-artifacts.ts +3 -1
  101. package/src/run-ticket-number.ts +80 -20
  102. package/src/sitian-appender.ts +25 -28
  103. package/src/submission-ledger.ts +148 -30
  104. package/src/ticket-provenance.ts +4 -1
  105. package/src/ticket-trajectory.ts +22 -35
  106. package/src/user-dialogue-stdin.ts +37 -0
  107. package/src/worker-role.ts +8 -4
  108. package/src/worker-submission-gates.ts +6 -6
@@ -0,0 +1,244 @@
1
+ /**
2
+ * #865 T9: migrate retained runs into ticket-scoped or unbound placement.
3
+ * Inventory covers legacy flat `runs/`, existing `<ticket>/runs/`, and
4
+ * `unbound/runs/`. Attribution reuses the shared migrating-run helper;
5
+ * already-canonical trees copy as-is.
6
+ */
7
+ import { cp, mkdir, stat, writeFile } from "node:fs/promises";
8
+ import { basename, dirname, join, relative, sep } from "node:path";
9
+
10
+ import {
11
+ destinationRunDirectory,
12
+ isMigrationEnoent,
13
+ isTicketNumberString,
14
+ listBackupRunLeaves,
15
+ listMigrationBookKeys,
16
+ resolveMigratingRunTicket,
17
+ } from "./book-topology-migration-placement.ts";
18
+ import {
19
+ reconcileMigrationPartition,
20
+ type BookTopologyMigrationContext,
21
+ type BookTopologyPartitionMigrator,
22
+ type MigrationItemOutcome,
23
+ } from "./book-topology-migration.ts";
24
+ import {
25
+ rewriteRoleRunDurablePages,
26
+ type RunDirectoryPathRewrite,
27
+ } from "./role-run-relocation.ts";
28
+ import { MIGRATION_TICKET_DERIVATION_PAGE } from "./run-ticket-number.ts";
29
+
30
+ const RUNS_PARTITION = "runs";
31
+
32
+ const RUN_LEAF =
33
+ /^([A-Za-z0-9][A-Za-z0-9._-]*)@([A-Za-z][A-Za-z0-9_-]*)$/;
34
+
35
+ type RunLeaf = { readonly runId: string; readonly role: string };
36
+
37
+ type PlannedRunMove = {
38
+ readonly sourcePath: string;
39
+ readonly sourceIdentity: string;
40
+ readonly isDirectory: boolean;
41
+ readonly disposition: "placed" | "unbound";
42
+ readonly targetPath: string;
43
+ readonly historicalRunDirectory: string | undefined;
44
+ readonly derivation:
45
+ | { readonly ticketNumber: number; readonly projectRoot: string; readonly sourcePage: string }
46
+ | undefined;
47
+ };
48
+
49
+ function parseRunLeaf(name: string): RunLeaf | undefined {
50
+ const match = RUN_LEAF.exec(name);
51
+ if (match === null) return undefined;
52
+ return { runId: match[1]!, role: match[2]! };
53
+ }
54
+
55
+ async function pathExists(path: string): Promise<boolean> {
56
+ try {
57
+ await stat(path);
58
+ return true;
59
+ } catch (error) {
60
+ if (isMigrationEnoent(error)) return false;
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ async function writeDerivationPage(
66
+ targetRunDirectory: string,
67
+ derivation: {
68
+ readonly ticketNumber: number;
69
+ readonly projectRoot: string;
70
+ readonly sourcePage: string;
71
+ },
72
+ ): Promise<void> {
73
+ const page = {
74
+ ticketNumber: derivation.ticketNumber,
75
+ derivation: "worktree-path-basename" as const,
76
+ source: {
77
+ page: derivation.sourcePage,
78
+ field: "projectRoot",
79
+ path: derivation.projectRoot,
80
+ basename: basename(derivation.projectRoot),
81
+ },
82
+ };
83
+ await writeFile(
84
+ join(targetRunDirectory, MIGRATION_TICKET_DERIVATION_PAGE),
85
+ `${JSON.stringify(page, null, 2)}\n`,
86
+ "utf8",
87
+ );
88
+ }
89
+
90
+ async function copyRunTree(
91
+ source: string,
92
+ target: string,
93
+ isDirectory: boolean,
94
+ ): Promise<void> {
95
+ await mkdir(dirname(target), { recursive: true });
96
+ if (isDirectory) {
97
+ await cp(source, target, { recursive: true, preserveTimestamps: true });
98
+ return;
99
+ }
100
+ await cp(source, target, { preserveTimestamps: true });
101
+ }
102
+
103
+ async function planBookMoves(
104
+ backupBooksDirectory: string,
105
+ booksDirectory: string,
106
+ bookKey: string,
107
+ ): Promise<readonly PlannedRunMove[]> {
108
+ const backupBook = join(backupBooksDirectory, bookKey);
109
+ const claimed = new Map<string, string>();
110
+ const planned: PlannedRunMove[] = [];
111
+
112
+ for (const leaf of await listBackupRunLeaves(backupBook)) {
113
+ const sourceIdentity = relative(backupBooksDirectory, leaf.sourcePath)
114
+ .split(sep)
115
+ .join("/");
116
+ const parsed = leaf.isDirectory ? parseRunLeaf(leaf.leafName) : undefined;
117
+
118
+ let targetPath: string;
119
+ let disposition: "placed" | "unbound";
120
+ let historicalRunDirectory: string | undefined;
121
+ let derivation: PlannedRunMove["derivation"];
122
+
123
+ if (leaf.layout === "issues") {
124
+ // issues/<ticket>/runs/<leaf> → <ticket>/runs/<leaf>; conflict refuses below.
125
+ const parts = leaf.relativePath.replaceAll("\\", "/").split("/");
126
+ const ticketStr = parts[1];
127
+ if (ticketStr !== undefined && isTicketNumberString(ticketStr)) {
128
+ targetPath = join(booksDirectory, bookKey, ticketStr, "runs", leaf.leafName);
129
+ disposition = "placed";
130
+ historicalRunDirectory = join(
131
+ booksDirectory,
132
+ bookKey,
133
+ "issues",
134
+ ticketStr,
135
+ "runs",
136
+ leaf.leafName,
137
+ );
138
+ } else {
139
+ targetPath = join(booksDirectory, bookKey, "unbound", "runs", leaf.leafName);
140
+ disposition = "unbound";
141
+ historicalRunDirectory = join(booksDirectory, bookKey, leaf.relativePath);
142
+ }
143
+ derivation = undefined;
144
+ } else if (leaf.layout !== "flat") {
145
+ targetPath = join(booksDirectory, bookKey, leaf.relativePath);
146
+ disposition = leaf.layout === "unbound" ? "unbound" : "placed";
147
+ historicalRunDirectory = undefined;
148
+ derivation = undefined;
149
+ } else if (parsed === undefined) {
150
+ targetPath = join(booksDirectory, bookKey, "unbound", "runs", leaf.leafName);
151
+ disposition = "unbound";
152
+ historicalRunDirectory = join(booksDirectory, bookKey, "runs", leaf.leafName);
153
+ derivation = undefined;
154
+ } else {
155
+ const ticket = await resolveMigratingRunTicket(leaf.sourcePath);
156
+ targetPath = destinationRunDirectory(
157
+ booksDirectory,
158
+ bookKey,
159
+ ticket.ticketNumber,
160
+ parsed.runId,
161
+ parsed.role,
162
+ );
163
+ disposition = ticket.ticketNumber === undefined ? "unbound" : "placed";
164
+ historicalRunDirectory = join(booksDirectory, bookKey, "runs", leaf.leafName);
165
+ derivation =
166
+ ticket.derivation?.method === "project-root-basename"
167
+ && ticket.ticketNumber !== undefined
168
+ ? {
169
+ ticketNumber: ticket.ticketNumber,
170
+ projectRoot: ticket.derivation.source,
171
+ sourcePage: ticket.derivation.sourcePage,
172
+ }
173
+ : undefined;
174
+ }
175
+
176
+ const prior = claimed.get(targetPath);
177
+ if (prior !== undefined) {
178
+ throw new Error(
179
+ `book topology migration refuses to overwrite ${targetPath} (already claimed by ${prior}) with ${leaf.sourcePath}`,
180
+ );
181
+ }
182
+ claimed.set(targetPath, leaf.sourcePath);
183
+ planned.push({
184
+ sourcePath: leaf.sourcePath,
185
+ sourceIdentity,
186
+ isDirectory: leaf.isDirectory,
187
+ disposition,
188
+ targetPath,
189
+ historicalRunDirectory,
190
+ derivation,
191
+ });
192
+ }
193
+ return planned;
194
+ }
195
+
196
+ export const bookTopologyRunsMigrator: BookTopologyPartitionMigrator = {
197
+ partition: RUNS_PARTITION,
198
+ async migrate(context: BookTopologyMigrationContext) {
199
+ const outcomes: MigrationItemOutcome[] = [];
200
+ const { backupBooksDirectory, booksDirectory } = context;
201
+
202
+ for (const bookKey of await listMigrationBookKeys(backupBooksDirectory)) {
203
+ const planned = await planBookMoves(
204
+ backupBooksDirectory,
205
+ booksDirectory,
206
+ bookKey,
207
+ );
208
+
209
+ const crossRunRewrites: RunDirectoryPathRewrite[] = planned
210
+ .filter((move) => move.isDirectory && move.historicalRunDirectory !== undefined)
211
+ .map((move) => ({
212
+ oldRunDirectory: move.historicalRunDirectory!,
213
+ newRunDirectory: move.targetPath,
214
+ }));
215
+
216
+ for (const move of planned) {
217
+ if (await pathExists(move.targetPath)) {
218
+ throw new Error(
219
+ `book topology migration refuses to overwrite ${move.targetPath} with ${move.sourcePath}`,
220
+ );
221
+ }
222
+ await copyRunTree(move.sourcePath, move.targetPath, move.isDirectory);
223
+ if (move.isDirectory) {
224
+ await rewriteRoleRunDurablePages({
225
+ pagesDirectory: move.targetPath,
226
+ oldRunDirectory: move.historicalRunDirectory ?? move.targetPath,
227
+ newRunDirectory: move.targetPath,
228
+ crossRunRewrites,
229
+ });
230
+ }
231
+ if (move.isDirectory && move.derivation !== undefined) {
232
+ await writeDerivationPage(move.targetPath, move.derivation);
233
+ }
234
+
235
+ outcomes.push({
236
+ disposition: move.disposition,
237
+ source: move.sourceIdentity,
238
+ });
239
+ }
240
+ }
241
+
242
+ return reconcileMigrationPartition(RUNS_PARTITION, "entries", outcomes);
243
+ },
244
+ };
@@ -12,6 +12,10 @@ import type {
12
12
  TicketProvenanceSourceKind,
13
13
  TicketProvenanceSourceRef,
14
14
  } from "./ticket-provenance-contracts.ts";
15
+ import {
16
+ isSafePositiveTicketNumber,
17
+ requireSafePositiveTicketNumber,
18
+ } from "./run-ticket-number.ts";
15
19
 
16
20
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
17
21
  export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
@@ -39,6 +43,15 @@ export type DiaristOutput =
39
43
  * LLM owns recognition; mechanical layer does not re-judge the number.
40
44
  */
41
45
  readonly ticketNumber?: number | null;
46
+ /**
47
+ * #871 typed co-review set (ADR 0075 `diarist-resolves-ticket-llm-layer` narrow extension).
48
+ * Positive integers = tickets that should each receive a diary this court (includes main).
49
+ * Field absent → no new set this turn (resume keeps stored run fact; first court defaults to [main]).
50
+ * Explicit empty array → single-ticket face [main] whole-set replace.
51
+ * Non-empty with zero lawful members after type projection is not a new set (call site).
52
+ * Mechanical layer only type-projects/dedupes and guarantees principal membership when applying a set.
53
+ */
54
+ readonly courtTicketNumbers?: readonly number[] | null;
42
55
  /** Whole blocks to append under the asserted ticket. Empty list is lawful. */
43
56
  readonly entries?: readonly DiaristEntrySubmission[];
44
57
  }
@@ -48,6 +61,109 @@ export type DiaristOutput =
48
61
  readonly reason: string;
49
62
  };
50
63
 
64
+ /**
65
+ * Sole shape projection for a typed co-review ticket set (#871).
66
+ * Positive safe integers, first-seen order, duplicates dropped. Not content judgment.
67
+ * Returns null when `raw` is not an array (callers classify absent vs durable damage).
68
+ * Returns a possibly empty list when `raw` is an array (unlawful members skipped).
69
+ * When `principalTicket` is set, that ticket is guaranteed as a member (prepended if missing).
70
+ */
71
+ export function projectCourtTicketNumbers(
72
+ raw: unknown,
73
+ options?: { readonly principalTicket?: number },
74
+ ): readonly number[] | null {
75
+ if (!Array.isArray(raw)) return null;
76
+ const out: number[] = [];
77
+ const seen = new Set<number>();
78
+ for (const item of raw) {
79
+ if (!isSafePositiveTicketNumber(item)) continue;
80
+ if (seen.has(item)) continue;
81
+ seen.add(item);
82
+ out.push(item);
83
+ }
84
+ const principal = options?.principalTicket;
85
+ if (principal === undefined) return out;
86
+ requireSafePositiveTicketNumber(principal, "projectCourtTicketNumbers principalTicket");
87
+ if (seen.has(principal)) return out;
88
+ return [principal, ...out];
89
+ }
90
+
91
+ /**
92
+ * Durable-field interpreter for #871 co-review sets.
93
+ * Absent key (old-format run) is not damage. Present but empty / non-array /
94
+ * partially unlawful / missing principal is damage — callers must fail closed,
95
+ * never wash into main-only.
96
+ */
97
+ export function interpretDurableCourtTicketNumbers(
98
+ record: Record<string, unknown>,
99
+ options?: { readonly principalTicket?: number },
100
+ ):
101
+ | { readonly kind: "absent" }
102
+ | { readonly kind: "ok"; readonly tickets: readonly number[] }
103
+ | { readonly kind: "damage"; readonly reason: string } {
104
+ if (!Object.hasOwn(record, "courtTicketNumbers")) {
105
+ return { kind: "absent" };
106
+ }
107
+ const raw = record.courtTicketNumbers;
108
+ if (!Array.isArray(raw)) {
109
+ return {
110
+ kind: "damage",
111
+ reason: "courtTicketNumbers is present but not an array",
112
+ };
113
+ }
114
+ if (raw.length === 0) {
115
+ return {
116
+ kind: "damage",
117
+ reason: "courtTicketNumbers is present but empty",
118
+ };
119
+ }
120
+ for (let i = 0; i < raw.length; i += 1) {
121
+ const item = raw[i];
122
+ if (!isSafePositiveTicketNumber(item)) {
123
+ return {
124
+ kind: "damage",
125
+ reason: `courtTicketNumbers[${i}] is not a safe positive integer`,
126
+ };
127
+ }
128
+ }
129
+ // Members are all lawful numbers; sole projection owns dedupe/order.
130
+ const tickets = projectCourtTicketNumbers(raw);
131
+ if (tickets === null || tickets.length === 0) {
132
+ return {
133
+ kind: "damage",
134
+ reason: "courtTicketNumbers projected empty after lawful members",
135
+ };
136
+ }
137
+ const principal = options?.principalTicket;
138
+ if (principal !== undefined) {
139
+ if (!isSafePositiveTicketNumber(principal)) {
140
+ return {
141
+ kind: "damage",
142
+ reason: "courtTicketNumbers principal ticket is not a safe positive integer",
143
+ };
144
+ }
145
+ if (!tickets.includes(principal)) {
146
+ return {
147
+ kind: "damage",
148
+ reason: `courtTicketNumbers is missing principal ticket #${principal}`,
149
+ };
150
+ }
151
+ }
152
+ return { kind: "ok", tickets: Object.freeze([...tickets]) };
153
+ }
154
+
155
+ /** True when two durable #871 sets are byte-identical sequences. */
156
+ export function sameCourtTicketNumbers(
157
+ left: readonly number[],
158
+ right: readonly number[],
159
+ ): boolean {
160
+ if (left.length !== right.length) return false;
161
+ for (let i = 0; i < left.length; i += 1) {
162
+ if (left[i] !== right[i]) return false;
163
+ }
164
+ return true;
165
+ }
166
+
51
167
  export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
52
168
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
53
169
  throw new Error("Diarist output has no execution discriminator");
@@ -35,6 +35,12 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(
35
35
  "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
36
36
  }),
37
37
  ),
38
+ courtTicketNumbers: Type.Optional(
39
+ Type.Unknown({
40
+ description:
41
+ "#871 本次合审应各自成录的票号集合(正整数数组,含主票);单票可省略。机械层只做类型/去重投影,不从散文猜票。",
42
+ }),
43
+ ),
38
44
  reason: Type.Optional(
39
45
  Type.String({
40
46
  description: "status 为 escalate 时:认不出本庭对象的原因",
package/src/diarist.ts CHANGED
@@ -42,6 +42,7 @@ export type DiaristCommitFacts = {
42
42
  export async function commitDiaristEntries(input: {
43
43
  readonly ticketNumber: number;
44
44
  readonly cwd: string;
45
+ readonly sessionParent: string;
45
46
  readonly home?: string;
46
47
  readonly entries: readonly unknown[];
47
48
  }): Promise<DiaristCommitFacts> {
@@ -59,6 +60,7 @@ export async function commitDiaristEntries(input: {
59
60
  appendTicketProvenanceEntry({
60
61
  ticketNumber,
61
62
  cwd,
63
+ sessionParent: input.sessionParent,
62
64
  ...homeOpt,
63
65
  payload: submitted,
64
66
  source: "diarist",
@@ -26,7 +26,7 @@ export type DoctorAuditOptions = {
26
26
  */
27
27
  export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
28
28
  return async (options) => {
29
- const dossier = resolveAuditDossier();
29
+ const dossier = resolveAuditDossier(options.context);
30
30
  requireAuditMaterials(dossier);
31
31
  const subjects = readDoctorAuditSubjects(options.context);
32
32
  requireAuditMaterials(subjects);
@@ -37,7 +37,10 @@ function deriveSession(content: string, id: string): SessionDerivation {
37
37
 
38
38
  /** Read Pi's retained session directory as the sole raw material for one case. */
39
39
  export async function loadDoctorCase(runsPath: string): Promise<DoctorCase> {
40
- const root = await realpath(runsPath); const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/); if (!match) throw new Error("Doctor case must be an .ak-roles/books/<book>/issues/<n>/runs directory");
40
+ const root = await realpath(runsPath);
41
+ // Canonical <book>/<ticket>/runs; legacy issues/<n>/runs remains read-only compatible.
42
+ const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/);
43
+ if (!match) throw new Error("Doctor case must be an .ak-roles/books/<book>/<n>/runs directory");
41
44
  const evidence: DoctorEvidenceEntry[] = [], sessions: DoctorCaseCost["sessions"] = [], statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
42
45
  const turns: DoctorCount = { count: 0, sources: [] }, calls: DoctorCount = { count: 0, sources: [] }, tokens: DoctorCount = { count: 0, sources: [] };
43
46
  for (const path of await discoverCaseFiles(root)) { const id = relative(root, path).split(sep).join("/"); const bytes = await readFile(path); const content = bytes.toString("utf8"); const kind = id.endsWith(".jsonl") ? "session" : "stderr"; evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content }); if (kind === "stderr") continue; const result = deriveSession(content, id); sessions.push(result.session); statuses.push(...result.statuses); commits.push(...result.commits); accumulate(turns, result.turns, id); accumulate(calls, result.calls, id); accumulate(tokens, result.tokens, id); }
@@ -8,7 +8,7 @@ import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVI
8
8
  import { sitianReport } from "./sitian-facade.ts";
9
9
 
10
10
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
11
- export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/issues/<n>/runs directory", type: "string" as const } } as const;
11
+ export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/<n>/runs directory", type: "string" as const } } as const;
12
12
  export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context: HostContext; signal?: AbortSignal }): Promise<ComplianceDecision> };
13
13
  function appendCandidate(ctx: HostContext, data: unknown): void {
14
14
  try {
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Unique dossier-resolution seam for 审刑院 (#233).
3
- * Machine pointers only: cwd + AK_ROLE_RUN_DIR. No latest-run / mtime / global scan.
3
+ * Machine pointers only: cwd + per-turn HostContext (Pi child env fallback).
4
+ * No latest-run / mtime / global scan.
4
5
  */
5
6
  import { existsSync, statSync } from "node:fs";
6
7
  import { resolve } from "node:path";
7
8
 
9
+ import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
10
+
8
11
  export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR" as const;
9
12
  export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate" as const;
10
13
 
@@ -33,18 +36,33 @@ export type SubjectIncomplete = {
33
36
  };
34
37
  export type SubjectResolution = SubjectOk | SubjectIncomplete;
35
38
 
39
+ function isHostContext(value: object): value is HostContext {
40
+ return "sessionManager" in value;
41
+ }
42
+
43
+ function dossierPointerFrom(source?: HostContext | NodeJS.ProcessEnv): string | undefined {
44
+ if (source !== undefined && isHostContext(source)) {
45
+ return runDirectoryFromHostContext(source);
46
+ }
47
+ const env = source ?? process.env;
48
+ const raw = env[AUDIT_RUN_DIR_ENV];
49
+ return typeof raw === "string" && raw.trim() !== "" ? raw : undefined;
50
+ }
51
+
36
52
  /**
37
- * Resolve the per-run dossier pointer injected by the public CLI.
53
+ * Resolve the per-run dossier pointer.
38
54
  *
55
+ * ACP/headless: typed HostContext.runDirectory.
56
+ * Pi child: HostContext projected from env, or env fallback when no context.
39
57
  * Absent pointer = bare Pi internal seam (ADR 0052): audit proceeds; the model
40
- * self-locates the dossier from its own fall-volume position per soul. Public CLI
41
- * always injects the pointer — only then does the machine validate the path.
42
- * Concurrent runs stay isolated because a present pointer is per-process.
58
+ * self-locates the dossier from its own fall-volume position per soul.
43
59
  */
44
- export function resolveAuditDossier(env: NodeJS.ProcessEnv = process.env): DossierResolution {
45
- const raw = env[AUDIT_RUN_DIR_ENV];
60
+ export function resolveAuditDossier(
61
+ source?: HostContext | NodeJS.ProcessEnv,
62
+ ): DossierResolution {
63
+ const raw = dossierPointerFrom(source);
46
64
  // Bare Pi activation seam: no machine gate when the pointer was never injected.
47
- if (typeof raw !== "string" || raw.trim() === "") {
65
+ if (raw === undefined) {
48
66
  return { status: "ok" };
49
67
  }
50
68
  const runDirectory = resolve(raw);
@@ -9,6 +9,7 @@ import type {
9
9
  RoleTurnRequest,
10
10
  RoleTurnResult,
11
11
  } from "./host-contracts.ts";
12
+ import { isOfficerReviewSeat } from "./host-contracts.ts";
12
13
 
13
14
  export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
14
15
 
@@ -54,8 +55,16 @@ export function mergeRoleTurnAbortSignals(
54
55
  return AbortSignal.any([prepared, request]);
55
56
  }
56
57
 
58
+ /**
59
+ * Host-transition prior-native paths are machine handoff material (#617 DK-4).
60
+ * #879: station-child officer dialogue must not receive those paths spliced into
61
+ * the review prompt — officers already hold --source-run / binding pointers.
62
+ */
57
63
  export function promptWithPriorNativePaths(basePrompt: string, request: RoleTurnRequest): string {
58
64
  if (request.continuation.kind !== "resume") return basePrompt;
65
+ if (request.stationChild === true && isOfficerReviewSeat(request.activation.role)) {
66
+ return basePrompt;
67
+ }
59
68
  const paths = request.hostTransition?.priorNativePaths;
60
69
  if (paths === undefined || paths.length === 0) return basePrompt;
61
70
  return `${basePrompt}\n${paths.join("\n")}`;
@@ -74,6 +74,11 @@ export async function requireGatekeeperPass(options: {
74
74
  readonly signal?: AbortSignal;
75
75
  readonly hostActions: GatekeeperPassHostActions;
76
76
  readonly toolCallId: string;
77
+ /**
78
+ * In-flight parent typed payload for this gate turn (#879). Relayed verbatim
79
+ * as officer dialogue content; binding pointer stays the parent run directory.
80
+ */
81
+ readonly submission?: unknown;
77
82
  /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
78
83
  readonly summonOfficer?: GateOfficerSummon;
79
84
  }): Promise<void> {
@@ -85,6 +90,7 @@ export async function requireGatekeeperPass(options: {
85
90
  subject: options.subject,
86
91
  ...(options.signal === undefined ? {} : { signal: options.signal }),
87
92
  ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
93
+ ...(options.submission === undefined ? {} : { submission: options.submission }),
88
94
  ...(reask === undefined ? {} : { reask }),
89
95
  });
90
96
  const gatekeeper = projected.result;