@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

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 (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -9,10 +9,11 @@ import { tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
10
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
11
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
- import { ensureRoleRunDirectory, ensureRoleRunPlacement, isUnboundRunDirectory, listBookRunDirectories, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
12
+ import { ensureRoleRunDirectory, ensureRoleRunPlacement, formatRunLeaf, isUnboundRunDirectory, listBookRunDirectories, roleRunPlacement, } from "../role-run-placement.js";
13
13
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
- import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
14
+ import { isSafePositiveTicketNumber, parseTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
15
  import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
16
+ import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.js";
16
17
  import { loadDoctorCase, } from "../doctor-evidence.js";
17
18
  import { emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
18
19
  import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.js";
@@ -48,17 +49,16 @@ export function issueAdmissionPlacement(authority, request) {
48
49
  };
49
50
  }
50
51
  /**
51
- * Unique admitted-request.json persistence projection: top-level sessionDirectory/sessionFile
52
+ * Unique `admitted` section projection of current.json: top-level sessionDirectory/sessionFile
52
53
  * (base wire shape). Memory Admitted keeps only the opaque principal — never dual-carry.
53
54
  */
54
- async function writeAdmittedRequestPersistence(admittedRequestPath, body, coordinates) {
55
+ function writeAdmittedRequestPersistence(runDirectory, body, coordinates) {
55
56
  const { principal: _omitPrincipal, ...rest } = body;
56
- const projection = {
57
+ writeSectionSync(runDirectory, "admitted", {
57
58
  ...rest,
58
59
  sessionDirectory: coordinates.sessionDirectory,
59
60
  sessionFile: coordinates.sessionFile,
60
- };
61
- await writeFile(admittedRequestPath, `${JSON.stringify(projection, null, 2)}\n`, "utf8");
61
+ });
62
62
  }
63
63
  /** Project effective model onto ledger fields; absent thinking stays absent. */
64
64
  function effectiveModelLedgerFields(model) {
@@ -72,7 +72,7 @@ function effectiveModelLedgerFields(model) {
72
72
  }
73
73
  export { homeFromRunDirectory };
74
74
  /**
75
- * Persist one `invocation.json` identity page for the public run.
75
+ * Persist the `invocation` identity section of current.json for the public run.
76
76
  * Admission is the sole source for every field; this is the only identity
77
77
  * projection and callers never provide an independent ledger shape.
78
78
  * When an effective model is known at admission, provider/model (and thinking
@@ -91,7 +91,7 @@ async function writeRoleInvocationLedger(source, role, effectiveModel) {
91
91
  ...(source.ticketNumber === undefined ? {} : { ticketNumber: source.ticketNumber }),
92
92
  ...effectiveModelLedgerFields(effectiveModel),
93
93
  };
94
- await writeFile(join(source.runDirectory, "invocation.json"), `${JSON.stringify(identity, null, 2)}\n`, "utf8");
94
+ writeSectionSync(source.runDirectory, "invocation", identity);
95
95
  }
96
96
  /**
97
97
  * Merge the effective launch model / engine / host onto the existing invocation
@@ -104,7 +104,7 @@ async function writeRoleInvocationLedger(source, role, effectiveModel) {
104
104
  * Host stays write-if-present (`string` only).
105
105
  */
106
106
  export async function recordEffectiveInvocationModel(runDirectory, model, engine, host, engineModel) {
107
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => {
107
+ updateSectionSync(runDirectory, "invocation", (current) => {
108
108
  const next = { ...current };
109
109
  if (model !== undefined) {
110
110
  next.provider = model.provider;
@@ -134,33 +134,22 @@ export async function recordEffectiveInvocationModel(runDirectory, model, engine
134
134
  return next;
135
135
  });
136
136
  }
137
- /**
138
- * Read one durable JSON object, let the caller keep its field rules, and write
139
- * the page back. `undefined` means the caller decided not to change the page.
140
- */
141
- async function updateDurableJsonPage(path, update) {
142
- const current = JSON.parse(await readFile(path, "utf8"));
143
- const next = update(current);
144
- if (next === undefined)
145
- return;
146
- await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
147
- }
148
- /** Merge observed launch-time fields into the single existing invocation.json identity page. */
137
+ /** Merge observed launch-time fields into the single existing invocation section of current.json. */
149
138
  async function mergeInvocationIdentityPage(runDirectory, fields) {
150
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => ({
139
+ updateSectionSync(runDirectory, "invocation", (current) => ({
151
140
  ...current,
152
141
  ...fields,
153
142
  }));
154
143
  }
155
144
  /**
156
- * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
145
+ * Persist parent --source-run path onto the admitted section for officer resume lookup (#747).
157
146
  * Reuses the existing notary sourceRunPath key; does not invent a new field name.
158
147
  */
159
148
  export async function persistAdmittedSourceRunPath(admitted, sourceRunPath, auditorSubject) {
160
149
  if (sourceRunPath.trim() === "") {
161
150
  throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
162
151
  }
163
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
152
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
164
153
  if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
165
154
  throw new Error(`persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`);
166
155
  }
@@ -176,7 +165,7 @@ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath, audi
176
165
  }
177
166
  /**
178
167
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
179
- * object and both durable pages (invocation.json + admitted-request.json).
168
+ * object and both durable sections (invocation + admitted).
180
169
  * A later typed role receipt replaces an earlier assertion (#1025).
181
170
  */
182
171
  export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
@@ -188,7 +177,7 @@ export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
188
177
  /** Persist each direct caller observed while a retained run is resumed. */
189
178
  export async function recordAdmittedCorrelation(admitted, correlationId) {
190
179
  let correlationIds = [];
191
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
180
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
192
181
  const prior = [
193
182
  ...(Array.isArray(current.correlationIds)
194
183
  ? current.correlationIds.filter((value) => typeof value === "string" && value.trim() !== "")
@@ -225,12 +214,11 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
225
214
  // Keep that failure before the filesystem commit point.
226
215
  const principal = authority.seal(target);
227
216
  if (admitted.role !== "diarist") {
228
- const parentPage = JSON.parse(await readFile(admitted.admittedRequestPath, "utf8"));
229
- const childRunIds = parentPage.childDiaristRunIds;
217
+ const childRunIds = readPageSync(oldRunDirectory, "admitted")?.childDiaristRunIds;
230
218
  for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
231
219
  if (typeof childRunId !== "string")
232
220
  continue;
233
- const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
221
+ const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
234
222
  // A child that already filed under a ticket keeps its own assertion.
235
223
  if (!existsSync(childDirectory))
236
224
  continue;
@@ -245,6 +233,15 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
245
233
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
246
234
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
247
235
  await rename(childDirectory, childTarget.runDirectory);
236
+ // Finished child legs will not settle again; refresh derived host.original
237
+ // after the rename commit. A derived render fault must not undo or block
238
+ // the committed placement (#1161 L1) — note and keep the true cause.
239
+ try {
240
+ renderCurrentSync(childTarget.runDirectory);
241
+ }
242
+ catch (error) {
243
+ process.stderr.write(`[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`);
244
+ }
248
245
  }
249
246
  }
250
247
  // The first ticket assignment moves only an unbound diarist's own diary.
@@ -255,13 +252,13 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
255
252
  // outside the atomic directory move.
256
253
  // Persisted paths are resolved from typed run identity on read.
257
254
  await rename(oldRunDirectory, target.runDirectory);
258
- // rename moved the open lock inode with the directory. Transfer cleanup
259
- // ownership immediately after the commit.
255
+ // rename moved the open lock inode with the directory. Transfer cleanup and
256
+ // admitted path ownership immediately after the commit — before any derived
257
+ // render that may refuse (#1161 L1 / BASE order).
260
258
  heldLease?.relocate(target.runDirectory);
261
259
  const admittedRecord = admitted;
262
260
  rewriteRunDirectoryPathFields(admittedRecord, [
263
261
  "runDirectory",
264
- "admittedRequestPath",
265
262
  "taskPath",
266
263
  "packetPath",
267
264
  "prerequisitesPath",
@@ -272,11 +269,15 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
272
269
  attachment.frozenPath = rewriteRunDirectoryPathValue(attachment.frozenPath, oldRunDirectory, target.runDirectory);
273
270
  }
274
271
  admitted.principal = principal;
272
+ // current.json projects host.original from this run directory. Ownership is
273
+ // already transferred; a refuse still carries the true cause to the caller
274
+ // (#1161 L1) without leaving the leg under the unbound path.
275
+ renderCurrentSync(target.runDirectory);
275
276
  return { oldRunDirectory, newRunDirectory: target.runDirectory };
276
277
  }
277
278
  /** Persist the exact child identity; later parent binding never inspects siblings. */
278
279
  export async function recordChildDiaristRun(parent, childRunId) {
279
- await updateDurableJsonPage(parent.admittedRequestPath, (page) => {
280
+ updateSectionSync(parent.runDirectory, "admitted", (page) => {
280
281
  const existing = Array.isArray(page.childDiaristRunIds)
281
282
  ? page.childDiaristRunIds.filter((runId) => typeof runId === "string")
282
283
  : [];
@@ -291,9 +292,8 @@ export async function recordChildDiaristRun(parent, childRunId) {
291
292
  */
292
293
  export async function bindTicketNumberOnRunDirectory(runDirectory, ticketNumber) {
293
294
  requireSafePositiveTicketNumber(ticketNumber, "bindTicketNumberOnRunDirectory");
294
- const admittedPath = join(runDirectory, "admitted-request.json");
295
295
  let unchanged = false;
296
- await updateDurableJsonPage(admittedPath, (admitted) => {
296
+ updateSectionSync(runDirectory, "admitted", (admitted) => {
297
297
  if (admitted.ticketNumber === ticketNumber) {
298
298
  unchanged = true;
299
299
  return undefined;
@@ -334,17 +334,16 @@ export async function recordLaunchedRolePackageIdentity(runDirectory, identity)
334
334
  entryMode: identity.entryMode,
335
335
  });
336
336
  }
337
+ /** Trim, then the shared ticket spelling. Callers keep their own diagnostic text. */
338
+ function rejectUnlessTicketNumber(raw, message) {
339
+ const parsed = parseTicketNumber(raw.trim());
340
+ if (parsed === undefined)
341
+ throw new CliUsageError(message);
342
+ return parsed;
343
+ }
337
344
  /** Positive ticket number for analyst query-scope face (and shared integer parse). */
338
345
  export function parsePositiveTicketNumber(raw, flag) {
339
- const trimmed = raw.trim();
340
- if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
341
- throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
342
- }
343
- const value = Number(trimmed);
344
- if (!Number.isSafeInteger(value) || value < 1) {
345
- throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
346
- }
347
- return value;
346
+ return rejectUnlessTicketNumber(raw, `${flag} must be a positive integer, got ${raw}`);
348
347
  }
349
348
  /**
350
349
  * One token scan for public argv.
@@ -702,8 +701,13 @@ async function freezeRegularFileAttachment(sourcePath, destinationDir, index) {
702
701
  };
703
702
  }
704
703
  /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
705
- async function freezeAttachments(attachmentPaths, attachmentsDirectory) {
704
+ async function freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory) {
706
705
  const attachments = [];
706
+ // attachments/ appears only when something is frozen; created through the
707
+ // ledger's symlink-rejecting tree walk, never a plain mkdir.
708
+ if (attachmentPaths.length > 0) {
709
+ ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
710
+ }
707
711
  for (let i = 0; i < attachmentPaths.length; i += 1) {
708
712
  const frozen = await freezeRegularFileAttachment(attachmentPaths[i], attachmentsDirectory, i);
709
713
  attachments.push(frozen.attachment);
@@ -721,7 +725,7 @@ export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, su
721
725
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
722
726
  const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
723
727
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
724
- return freezeAttachments(attachmentPaths, attachmentsDirectory);
728
+ return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
725
729
  }
726
730
  /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
727
731
  export async function freezePreparedAttachmentsIntoRun(prepared, runDirectory, summonsKey) {
@@ -957,7 +961,7 @@ export async function admitPublicRole(role, parsed, env, override) {
957
961
  }
958
962
  let caseIdentity;
959
963
  try {
960
- const patient = await loadDoctorCase(caseRunsPath);
964
+ const patient = await loadDoctorCase(caseRunsPath, parsed.runs === undefined ? undefined : placed.projectRoot);
961
965
  if (patient.identity.issueNumber !== issueNumber) {
962
966
  throw new CliUsageError(`doctor case issue ${patient.identity.issueNumber} does not match --issue ${issueNumber}`);
963
967
  }
@@ -970,7 +974,7 @@ export async function admitPublicRole(role, parsed, env, override) {
970
974
  const detail = errorText(error);
971
975
  throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
972
976
  }
973
- frozenAttachments = await freezeAttachments(attachmentPaths, placed.attachmentsDirectory);
977
+ frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
974
978
  return {
975
979
  issueNumber,
976
980
  caseRunsPath,
@@ -1127,7 +1131,7 @@ async function placeRoleAdmission(options) {
1127
1131
  });
1128
1132
  const attachments = options.freezeAttachments === false
1129
1133
  ? []
1130
- : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1134
+ : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1131
1135
  return {
1132
1136
  runId,
1133
1137
  bookKey,
@@ -1152,8 +1156,7 @@ function persistedAttachmentRefs(attachments) {
1152
1156
  }));
1153
1157
  }
1154
1158
  async function persistPlacedAdmission(admitted, placed, model) {
1155
- const admittedRequestPath = join(placed.runDirectory, "admitted-request.json");
1156
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1159
+ writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
1157
1160
  sessionDirectory: placed.sessionDirectory,
1158
1161
  sessionFile: placed.sessionFile,
1159
1162
  });
@@ -1162,11 +1165,10 @@ async function persistPlacedAdmission(admitted, placed, model) {
1162
1165
  sessionDirectory: placed.sessionDirectory,
1163
1166
  sessionFile: placed.sessionFile,
1164
1167
  }, admitted.role, model);
1165
- return admittedRequestPath;
1166
1168
  }
1167
1169
  /**
1168
1170
  * Shared admission: project check, placement, attachment freeze,
1169
- * admitted-request and invocation ledger write.
1171
+ * admitted and invocation section write.
1170
1172
  * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1171
1173
  * before materializeCountersignInvocation writes the page.
1172
1174
  * Seats whose extra facts are known before placement pass them as admittedFields.
@@ -1215,9 +1217,8 @@ async function admitStandardMaterialInvocation(role, options) {
1215
1217
  attachments: persistedAttachmentRefs(placed.attachments),
1216
1218
  ...placed.ticketFields,
1217
1219
  };
1218
- const admittedRequestPath = defer
1219
- ? join(placed.runDirectory, "admitted-request.json")
1220
- : await persistPlacedAdmission(admitted, placed, options.model);
1220
+ if (!defer)
1221
+ await persistPlacedAdmission(admitted, placed, options.model);
1221
1222
  return {
1222
1223
  role,
1223
1224
  runId: placed.runId,
@@ -1228,7 +1229,6 @@ async function admitStandardMaterialInvocation(role, options) {
1228
1229
  attachments: placed.attachments,
1229
1230
  runDirectory: placed.runDirectory,
1230
1231
  principal: placed.principal,
1231
- admittedRequestPath,
1232
1232
  ...correlationFields,
1233
1233
  ...placed.ticketFields,
1234
1234
  ...admittedFields,
@@ -1305,7 +1305,6 @@ export async function materializeCountersignInvocation(admitted, options) {
1305
1305
  });
1306
1306
  const placement = placed;
1307
1307
  admitted.runDirectory = placement.runDirectory;
1308
- admitted.admittedRequestPath = join(placement.runDirectory, "admitted-request.json");
1309
1308
  admitted.principal = placement.principal;
1310
1309
  const ticketFields = ticketAdmissionFields(options.ticketNumber);
1311
1310
  if (ticketFields.ticketNumber !== undefined) {
@@ -1313,19 +1312,18 @@ export async function materializeCountersignInvocation(admitted, options) {
1313
1312
  }
1314
1313
  const attachments = await freezePreparedAttachmentsIntoRun(options.preparedAttachments, admitted.runDirectory);
1315
1314
  admitted.attachments = attachments;
1316
- await writeAdmittedRequestPersistence(admitted.admittedRequestPath, admitted, {
1315
+ writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
1317
1316
  sessionDirectory: placement.sessionDirectory,
1318
1317
  sessionFile: placement.sessionFile,
1319
1318
  });
1320
1319
  await writeRoleInvocationLedger({ ...admitted, sessionDirectory: placement.sessionDirectory, sessionFile: placement.sessionFile }, admitted.role, options.model);
1321
1320
  }
1322
- /** Load admitted-request.json written at admission (Navigator work-context seam). */
1321
+ /** Load the admitted section written at admission (Navigator work-context seam). */
1323
1322
  export async function loadAdmittedJudgeRequest(runDirectory) {
1324
1323
  try {
1325
- const raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
1326
- if (!isRecord(raw))
1324
+ const record = readPageSync(runDirectory, "admitted");
1325
+ if (record === undefined)
1327
1326
  return undefined;
1328
- const record = raw;
1329
1327
  if (!packagedPublicInstructionSubject(record.role))
1330
1328
  return undefined;
1331
1329
  if (typeof record.instruction !== "string")
@@ -1344,10 +1342,6 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
1344
1342
  return undefined;
1345
1343
  }
1346
1344
  }
1347
- export async function ensureRunArtifactsDir(runDirectory) {
1348
- const directory = roleRunArtifactsDirectory(runDirectory);
1349
- return ensureRoleRunDirectory(resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)), directory);
1350
- }
1351
1345
  function parsePositivePrOption(raw) {
1352
1346
  if (raw === undefined || raw.trim() === "")
1353
1347
  throw new CliUsageError("--pr requires a positive pull request number");
@@ -1411,26 +1405,17 @@ function isGitRemoteMissing(error) {
1411
1405
  const status = error.status;
1412
1406
  return status === 2;
1413
1407
  }
1414
- /** Positive Issue number grammar shared with Doctor case path identity. */
1415
- const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
1416
- /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
1417
- /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
1418
- const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
1419
1408
  /**
1420
1409
  * Parse a positive Issue number for public Doctor admission.
1421
1410
  * Leading zeros and non-integers are structural rejects.
1422
1411
  */
1423
1412
  export function parseDoctorIssueNumber(raw) {
1424
- const trimmed = raw.trim();
1425
- if (!DOCTOR_ISSUE_NUMBER_PATTERN.test(trimmed)) {
1426
- throw new CliUsageError(`doctor --issue must be a positive integer, got ${raw}`);
1427
- }
1428
- return Number(trimmed);
1413
+ return rejectUnlessTicketNumber(raw, `doctor --issue must be a positive integer, got ${raw}`);
1429
1414
  }
1430
1415
  /**
1431
1416
  * Resolve the retained Doctor case runs root from Issue identity.
1432
- * Default is the #78 book locator; optional --runs must stay project-confined
1433
- * and match Doctor case grammar for the same issue number.
1417
+ * Default is the #78 book locator; optional --runs must stay project-confined.
1418
+ * loadDoctorCase owns retained-root grammar at IO; admission matches its case identity.
1434
1419
  */
1435
1420
  export async function resolveDoctorCaseRunsPath(options) {
1436
1421
  const ledgerHome = resolveActivationLedgerHome(options.home);
@@ -1460,14 +1445,6 @@ export async function resolveDoctorCaseRunsPath(options) {
1460
1445
  const detail = errorText(error);
1461
1446
  throw new CliUsageError(`doctor --runs is not a readable retained runs root: ${detail}`, { cause: error });
1462
1447
  }
1463
- const normalized = real.split(sep).join("/");
1464
- const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
1465
- if (!match) {
1466
- throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory");
1467
- }
1468
- if (Number(match[1]) !== options.issueNumber) {
1469
- throw new CliUsageError(`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`);
1470
- }
1471
1448
  return real;
1472
1449
  }
1473
1450
  /** Frozen Skill arg projection shared by initial and resume (never reverse-parsed). */
@@ -1501,23 +1478,13 @@ export async function deriveMergerEnvelopeFromActiveMerge(projectRoot, gitState
1501
1478
  resolutionScope,
1502
1479
  });
1503
1480
  }
1504
- const ANALYST_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
1505
1481
  /**
1506
1482
  * Parse a positive ticket / issue number for public analyst admission.
1507
1483
  * Leading zeros and non-integers are structural rejects (same face as #176).
1508
1484
  * `flag` names the actual argv face in diagnostics (cohort group lists reuse this).
1509
1485
  */
1510
1486
  export function parseAnalystTicketNumber(raw, flag = "--ticket") {
1511
- const trimmed = raw.trim();
1512
- if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
1513
- throw new CliUsageError(`analyst ${flag} must be a positive integer, got ${raw}`);
1514
- }
1515
- const value = Number(trimmed);
1516
- // Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
1517
- if (!Number.isSafeInteger(value) || value < 1) {
1518
- throw new CliUsageError(`analyst ${flag} must be a positive integer, got ${raw}`);
1519
- }
1520
- return value;
1487
+ return rejectUnlessTicketNumber(raw, `analyst ${flag} must be a positive integer, got ${raw}`);
1521
1488
  }
1522
1489
  /**
1523
1490
  * Parse one cohort issue token: bare positive integer or `book:N`.
@@ -1532,7 +1499,7 @@ export function parseAnalystCohortIssueToken(raw, flag) {
1532
1499
  const sep = trimmed.lastIndexOf(":");
1533
1500
  if (sep > 0) {
1534
1501
  const rhs = trimmed.slice(sep + 1);
1535
- if (ANALYST_TICKET_NUMBER_PATTERN.test(rhs)) {
1502
+ if (parseTicketNumber(rhs) !== undefined) {
1536
1503
  const bookKey = trimmed.slice(0, sep);
1537
1504
  if (bookKey.trim() === "") {
1538
1505
  throw new CliUsageError(`${flag} book:N requires a non-empty book key, got ${raw}`);