@akagilnc/pi-workflow-roles 0.1.5504 → 0.1.5575

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 (89) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +5965 -5994
  4. package/dist/analyst-gate-cycles-read.js +23 -55
  5. package/dist/analyst-ledger.js +21 -35
  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-pointer.js +38 -30
  9. package/dist/auditor-dossier-tool.js +4 -3
  10. package/dist/auditor-soul.js +3 -11
  11. package/dist/doctor-evidence.js +3 -3
  12. package/dist/engine-detour-usage.js +6 -14
  13. package/dist/headless-host/production-host.js +5316 -5324
  14. package/dist/host-descriptions.js +0 -4
  15. package/dist/host-session-record.js +266 -267
  16. package/dist/migrate-book-topology.js +1314 -696
  17. package/dist/notary-source-run.js +1 -1
  18. package/dist/packaged-role-registry.js +5 -44
  19. package/dist/pi/role-turn-host.js +53 -3
  20. package/dist/public-cli/auto-resume.js +8 -47
  21. package/dist/public-cli/instruction-seat-run.js +5 -3
  22. package/dist/public-cli/invocation.js +51 -56
  23. package/dist/public-cli/main.js +1916 -1748
  24. package/dist/public-cli/post-admission.js +96 -55
  25. package/dist/public-cli/run-lifecycle.js +27 -53
  26. package/dist/public-cli/settlement.js +157 -302
  27. package/dist/role-run-placement.js +1 -11
  28. package/dist/role-run-relocation.js +72 -50
  29. package/dist/run-dossier-files.js +30 -0
  30. package/dist/run-dossier.js +322 -0
  31. package/dist/run-terminal-artifacts.js +68 -197
  32. package/dist/run-ticket-number.js +16 -3
  33. package/dist/session-identity.js +49 -18
  34. package/dist/sitian-appender.js +13 -16
  35. package/dist/sitian-facade.js +11 -0
  36. package/dist/sitian-reader.js +44 -29
  37. package/dist/submission-gate.js +1 -1
  38. package/dist/ticket-provenance.js +22 -7
  39. package/package.json +1 -1
  40. package/scripts/build-package.mjs +2 -0
  41. package/src/acp-host/production-host.ts +1 -1
  42. package/src/acp-host/role-turn-host.ts +9 -2
  43. package/src/analyst-gate-cycles-read.ts +26 -60
  44. package/src/analyst-ledger.ts +27 -42
  45. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  46. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  47. package/src/analyst-metric-families/round-timeline.ts +2 -5
  48. package/src/analyst-page.ts +3 -3
  49. package/src/archivist-record-entry.ts +2 -2
  50. package/src/archivist-record-pointer.ts +50 -36
  51. package/src/auditor-dossier-tool.ts +7 -5
  52. package/src/auditor-soul.ts +3 -9
  53. package/src/book-topology-migration-placement.ts +16 -3
  54. package/src/book-topology-record-class-migrators.ts +4 -2
  55. package/src/book-topology-runs-migrator.ts +6 -3
  56. package/src/doctor-contracts.ts +1 -1
  57. package/src/doctor-evidence.ts +2 -2
  58. package/src/engine-detour-tool.ts +1 -1
  59. package/src/engine-detour-usage.ts +6 -16
  60. package/src/external-host-turn-loop.ts +25 -0
  61. package/src/headless-host/production-host.ts +1 -4
  62. package/src/headless-host/role-turn-host.ts +54 -20
  63. package/src/host-descriptions.ts +0 -6
  64. package/src/host-session-record.ts +224 -140
  65. package/src/notary-source-run.ts +1 -1
  66. package/src/packaged-role-registry.ts +1 -50
  67. package/src/pi/adapter.ts +50 -7
  68. package/src/pi/role-turn-host.ts +51 -3
  69. package/src/public-cli/auto-resume.ts +7 -43
  70. package/src/public-cli/instruction-seat-run.ts +9 -2
  71. package/src/public-cli/invocation.ts +61 -87
  72. package/src/public-cli/post-admission.ts +111 -72
  73. package/src/public-cli/run-lifecycle.ts +27 -72
  74. package/src/public-cli/settlement.ts +193 -418
  75. package/src/public-cli/terminal.ts +1 -1
  76. package/src/role-run-placement.ts +2 -13
  77. package/src/role-run-relocation.ts +71 -68
  78. package/src/role-runtime.ts +6 -10
  79. package/src/run-dossier-files.ts +33 -0
  80. package/src/run-dossier.ts +357 -0
  81. package/src/run-terminal-artifacts.ts +83 -237
  82. package/src/run-ticket-number.ts +41 -11
  83. package/src/session-identity.ts +52 -20
  84. package/src/sitian-appender.ts +17 -18
  85. package/src/sitian-facade.ts +22 -0
  86. package/src/sitian-reader.ts +62 -32
  87. package/src/submission-gate.ts +1 -1
  88. package/src/ticket-provenance.ts +20 -7
  89. package/src/ticket-trajectory.ts +10 -10
@@ -58,7 +58,7 @@ async function requireRunDirectory(candidate, display) {
58
58
  * - absolute/relative path that realpath's to that same book runs slot
59
59
  *
60
60
  * Rejects project-tree projections and any path outside the project's ledger book runs.
61
- * Retained `run-state.json` must match basename identity and book binding.
61
+ * Retained state.jsonl run-state must match basename identity and book binding.
62
62
  */
63
63
  export async function resolveNotarySourceRunLocator(options) {
64
64
  const raw = options.sourceRun.trim();
@@ -88,14 +88,7 @@ const PUBLIC_ROLE_RECORDS = [
88
88
  outputTool: FIXER_OUTPUT_TOOL_NAME,
89
89
  settlement: "sealed",
90
90
  artifactFace: {
91
- reportPhase: true,
92
- evidenceRole: true,
93
- leaves: [
94
- { key: "phase" },
95
- { key: "packetPath" },
96
- { key: "prerequisitesPath", omitUndefined: true },
97
- { key: "prerequisites" }
98
- ]
91
+ reportPhase: true
99
92
  },
100
93
  boardPlacement: "marshal",
101
94
  activationFlags: [
@@ -126,12 +119,7 @@ const PUBLIC_ROLE_RECORDS = [
126
119
  outputTool: CODER_OUTPUT_TOOL_NAME,
127
120
  settlement: "sealed",
128
121
  artifactFace: {
129
- reportPhase: true,
130
- evidenceRole: true,
131
- leaves: [
132
- { key: "phase" },
133
- { key: "taskPath" }
134
- ]
122
+ reportPhase: true
135
123
  },
136
124
  activationFlags: [
137
125
  { field: "taskPath", flag: "ak-coder-task", binds: "input" },
@@ -165,15 +153,7 @@ const PUBLIC_ROLE_RECORDS = [
165
153
  settlement: "sealed",
166
154
  /** Publish only an accepted ledger outcome; audit escalation stays unsettled here. */
167
155
  sealedAcceptedOnly: true,
168
- artifactFace: {
169
- evidenceRole: true,
170
- leaves: [
171
- { key: "baseRevision" },
172
- { key: "lens" },
173
- { key: "authorityRefs", copyArray: true },
174
- { key: "callerProvenance", callerProvenance: true }
175
- ]
176
- },
156
+ artifactFace: {},
177
157
  /** Frozen base/lens/authority become the initial prompt; instruction follows. */
178
158
  transportPrompt: "skill-args",
179
159
  boardPlacement: "marshal",
@@ -204,14 +184,7 @@ const PUBLIC_ROLE_RECORDS = [
204
184
  bareCommand: false,
205
185
  outputTool: COLLECTOR_OUTPUT_TOOL,
206
186
  settlement: "sealed",
207
- artifactFace: {
208
- evidenceRole: true,
209
- leaves: [
210
- { key: "prNumber", omitUndefined: true },
211
- { key: "repository", from: "repository.canonical" },
212
- { key: "manifestDigest" }
213
- ]
214
- },
187
+ artifactFace: {},
215
188
  boardPlacement: "collector",
216
189
  /** Taishi acceptance is a typed groups array, not a status leaf. */
217
190
  analystTerminal: "groups",
@@ -240,12 +213,6 @@ const PUBLIC_ROLE_RECORDS = [
240
213
  auditTool: DOCTOR_AUDIT_TOOL_NAME,
241
214
  settlement: "sealed",
242
215
  artifactFace: {
243
- evidenceRole: true,
244
- leaves: [
245
- { key: "issueNumber" },
246
- { key: "caseRunsPath" },
247
- { key: "caseIdentity" }
248
- ],
249
216
  doctorReportFacts: true
250
217
  },
251
218
  /** Navigator subject is the doctor case, not the case-path file bytes. */
@@ -271,13 +238,7 @@ const PUBLIC_ROLE_RECORDS = [
271
238
  phases: [null],
272
239
  outputTool: MERGER_OUTPUT_TOOL_NAME,
273
240
  settlement: "sealed",
274
- artifactFace: {
275
- evidenceRole: true,
276
- leaves: [
277
- { key: "mergerInputPath" },
278
- { key: "derived" }
279
- ]
280
- },
241
+ artifactFace: {},
281
242
  activationFlags: [
282
243
  { field: "inputPath", from: "mergerInputPath", flag: "ak-merger-input", binds: "input" }
283
244
  ],
@@ -14,10 +14,13 @@ import { ExplicitInternalActivationError } from "../host-contracts.js";
14
14
  import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.js";
15
15
  import { projectActivationFlags } from "../role-activation-flags.js";
16
16
  import { encodeUserDialogueStdin } from "../user-dialogue-stdin.js";
17
- import { readStrictPiSessionJsonl } from "../ledger-session-read.js";
17
+ import { readLedgerSessionJsonlLines, readStrictPiSessionJsonl } from "../ledger-session-read.js";
18
+ import { copyAndRecordHostDossier } from "../host-session-record.js";
18
19
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.js";
19
20
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.js";
20
21
  import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.js";
22
+ import { createSessionIdentityAuthority } from "../session-identity.js";
23
+ import { sitianReport } from "../sitian-facade.js";
21
24
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
22
25
  const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
23
26
  export function resolveInternalRoleEntrypoint(packageRoot) {
@@ -351,17 +354,64 @@ export function createPiRoleTurnHost(config) {
351
354
  await config.recordLaunchedRolePackageIdentity(request.runDirectory, await config.observeLaunchedRolePackageIdentity(config.packageRoot, roleEntry));
352
355
  }
353
356
  const timeoutMs = request.timeoutMs ?? config.timeoutMs;
354
- return await spawnRunner(args, {
357
+ const result = await spawnRunner(args, {
355
358
  cwd: request.cwd,
356
359
  env,
357
360
  stdin,
358
361
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
359
362
  ...(request.signal === undefined ? {} : { signal: request.signal }),
360
363
  });
364
+ // Deliver already-written Pi host facts. Pi lands at session/session.jsonl;
365
+ // bind projects host.sessions; copy records the landing. Host adapters do
366
+ // not write current.json — public bind/settlement seams own that render.
367
+ try {
368
+ const { sessionFile, sessionDirectory } = config.principalAuthority.decode(request.principal);
369
+ const sessionId = await readPiSessionHeaderId(sessionFile);
370
+ if (sessionId !== undefined) {
371
+ await createSessionIdentityAuthority(config.principalAuthority, "pi").bind(request.principal, sessionId);
372
+ copyAndRecordHostDossier({
373
+ host: "pi",
374
+ sessionId,
375
+ cwd: request.cwd,
376
+ sessionDirectory,
377
+ sessionParent: sessionFile,
378
+ ...(request.home !== undefined ? { home: request.home } : {}),
379
+ });
380
+ }
381
+ }
382
+ catch (error) {
383
+ await retainPackageFault({
384
+ runDirectory: request.runDirectory,
385
+ diagnostic: `pi host dossier record failed beside host terminal: ${describeErrorIdentity(error)}`,
386
+ error,
387
+ });
388
+ }
389
+ return result;
361
390
  },
362
391
  };
363
392
  }
364
- import { sitianReport } from "../sitian-facade.js";
393
+ /**
394
+ * Session header id from the Pi volume the child (or fixture) already wrote.
395
+ * Reuses the ledger line kernel: a later malformed line must not erase a valid
396
+ * header already present (ADR 0086 originals are not validated whole-file).
397
+ */
398
+ async function readPiSessionHeaderId(sessionFile) {
399
+ try {
400
+ for (const line of await readLedgerSessionJsonlLines(sessionFile)) {
401
+ const entry = line.row;
402
+ if (entry === undefined || entry.type !== "session")
403
+ continue;
404
+ if (typeof entry.id === "string" && entry.id.trim() !== "")
405
+ return entry.id;
406
+ }
407
+ }
408
+ catch (error) {
409
+ if (error.code === "ENOENT")
410
+ return undefined;
411
+ throw error;
412
+ }
413
+ return undefined;
414
+ }
365
415
  /** Last non-session entry id. Session headers are not parents of custom lines. */
366
416
  export function piSessionCustomParentId(entries) {
367
417
  let parentId = null;
@@ -1,27 +1,10 @@
1
- /**
2
- * Single generic auto-resume loop for #416 (owner scope = single LLM call).
3
- * Call-local retries, at most the effective autoResumeLimit times (injected once
4
- * per call by the caller, #422 — never re-read from disk inside the loop),
5
- * in-place (same runId/session).
6
- * Unifies presentation: intermediate attempts use dummyIo, only final Terminal is presented.
7
- *
8
- * Owner 2026-08-23: a dispatch that exits by throwing used to bypass the entire
9
- * retry mechanism (the throw escaped the while-loop before the count check ever
10
- * ran). Every exception is now retained whole, in place, and the ordinary retry
11
- * path continues: same budget, same call-local count semantics. No failure-type
12
- * classification — every thrown value is treated identically.
13
- */
14
- import { constants as fsConstants } from "node:fs";
15
- import { randomUUID } from "node:crypto";
16
- import { open } from "node:fs/promises";
17
- import { join } from "node:path";
18
1
  import { AUTO_RESUME_LIMIT, describeErrorIdentity, acquireRunWriterLease, markRunTerminal, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
19
2
  import { parseAutoResumeLimit } from "./config.js";
20
3
  import { processCancelSignalName } from "./process-cancel.js";
21
4
  import { isLawfulTypedTerminalOutcome, formatTerminalResult } from "./terminal.js";
22
- import { attachRecordedSubmissions, ensureRealArtifactsDirectory, presentFailureTerminal, retainPackageFault, } from "./settlement.js";
23
- export { ensureRealArtifactsDirectory };
5
+ import { attachRecordedSubmissions, presentFailureTerminal, retainPackageFault, } from "./settlement.js";
24
6
  import { serializeThrownValue } from "../serialize-thrown-value.js";
7
+ import { reportRunRecord } from "../sitian-facade.js";
25
8
  import { errorText } from "../unknown-value.js";
26
9
  const dummyIo = { stdout: () => { }, stderr: () => { } };
27
10
  /**
@@ -105,41 +88,19 @@ function jsonSafeReplacer() {
105
88
  };
106
89
  }
107
90
  /**
108
- * Retain one throwing dispatch attempt's complete exception as an independent
109
- * per-attempt file under the run's artifacts directory, then leave an
110
- * addressable pointer in the session principal (custom entry). Exclusive-create
111
- * open (O_EXCL) with a per-attempt unique name enforces 史必追加 (#419): a later
112
- * attempt can never overwrite an earlier attempt's file.
91
+ * Retain one throwing dispatch attempt's complete exception as a line in the
92
+ * run's log.jsonl, then leave an addressable pointer in the session principal
93
+ * (custom entry). The log only appends (史必追加, #419): a later attempt can
94
+ * never overwrite an earlier attempt's record.
113
95
  */
114
- /**
115
- * Hardened create-once JSON write shared by every durable artifact this loop
116
- * retains directly (dispatch-error dumps, lawful-persist-failure error/
117
- * evidence records): O_EXCL (fail loud on a colliding name, never overwrite)
118
- * + O_NOFOLLOW where the platform provides it (a planted symlink is never
119
- * followed) — mirrors settlement.ts's own hardened artifact writers.
120
- */
121
- export async function writeHardenedArtifactFile(artifactsDir, namePrefix, payload) {
122
- const filePath = join(artifactsDir, `${namePrefix}-${randomUUID()}.json`);
123
- const body = `${JSON.stringify(payload, jsonSafeReplacer(), 2)}\n`;
124
- const noFollowFlag = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
125
- const handle = await open(filePath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag, 0o600);
126
- try {
127
- await handle.writeFile(body, "utf8");
128
- }
129
- finally {
130
- await handle.close();
131
- }
132
- return filePath;
133
- }
134
96
  async function retainDispatchError(admitted, principalAuthority, sessionAppender, attempt, error) {
135
- const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
136
97
  // Whole-object dump: everything the thrown value carries, nothing picked.
137
- const filePath = await writeHardenedArtifactFile(artifactsDir, `dispatch-error-attempt-${attempt}`, {
98
+ const filePath = reportRunRecord(admitted.runDirectory, "dispatch-exception", JSON.parse(JSON.stringify({
138
99
  version: 1,
139
100
  attempt,
140
101
  recordedAt: new Date().toISOString(),
141
102
  error: serializeThrownValue(error),
142
- });
103
+ }, jsonSafeReplacer())), "auto-resume").recordFile;
143
104
  // Addressable pointer in the dossier (卷宗): Pi session custom-entry codec
144
105
  // (appendPiSessionCustomEntry). Lease still owned here with run-writer.
145
106
  let pointerLease;
@@ -29,7 +29,7 @@ import { packagedAdmitsCountersign, packagedBindsBoardTicket, packagedRebindSour
29
29
  import { readAuditorResumeBinding } from "../auditor-soul.js";
30
30
  import { CliUsageError } from "./cli-errors.js";
31
31
  import { admitPublicRole, bindAdmittedTicketNumber, buildInstructionTransportPrompt, materializeCountersignInvocation, persistAdmittedSourceRunPath, recordAdmittedCorrelation, recordChildDiaristRun, relocateAdmittedRunToTicket, withPreparedAttachments, } from "./invocation.js";
32
- import { presentControlledFailure, packageFaultNoteFor, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
32
+ import { bindDiaristAssertedTicketNumber, presentControlledFailure, packageFaultNoteFor, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
33
33
  import { presentTerminal } from "./auto-resume.js";
34
34
  import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, readCurrentCourt, } from "./run-lifecycle.js";
35
35
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
@@ -184,6 +184,8 @@ function isBoardTicketSeat(seat) {
184
184
  return packagedBindsBoardTicket(seat.role);
185
185
  }
186
186
  async function bindAndRelocateDiarist(admitted, authority, lease) {
187
+ // The diarist's asserted ticketNumber binds here, in the public call's process.
188
+ await bindDiaristAssertedTicketNumber(admitted);
187
189
  const boardTicket = await readBoardTicketNumber(admitted.runDirectory);
188
190
  if (boardTicket === undefined)
189
191
  return undefined;
@@ -856,7 +858,7 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
856
858
  if (stopped.exitCode === 0
857
859
  && heldUnreadableTerminal(stopped.terminal)
858
860
  && stopped.terminal !== undefined) {
859
- const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
861
+ const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, { ...await readCurrentCourt(admitted.runDirectory), ...packageFaultScope(admitted, env, io) });
860
862
  if (parentTerminal?.roleOutcome.kind === "accepted"
861
863
  || parentTerminal?.roleOutcome.kind === "audit_escalation") {
862
864
  const terminal = await presentUnsettledAudit(parentTerminal, stopped.terminal, io, admitted.runDirectory);
@@ -870,7 +872,7 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
870
872
  }
871
873
  if (resolved !== undefined && (resolved.status === "continue" || resolved.status === "converged")) {
872
874
  if (AUDITED_ROLES.has(admitted.role) && resolved.status === "converged") {
873
- const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, packageFaultScope(admitted, env, io));
875
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, { ...await readCurrentCourt(admitted.runDirectory), ...packageFaultScope(admitted, env, io) });
874
876
  if (terminal?.roleOutcome.kind !== "accepted")
875
877
  throw new Error("pending submission is not recorded");
876
878
  return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
@@ -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, formatRunLeaf, 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
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,8 +214,7 @@ 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;
@@ -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;
@@ -701,8 +701,13 @@ async function freezeRegularFileAttachment(sourcePath, destinationDir, index) {
701
701
  };
702
702
  }
703
703
  /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
704
- async function freezeAttachments(attachmentPaths, attachmentsDirectory) {
704
+ async function freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory) {
705
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
+ }
706
711
  for (let i = 0; i < attachmentPaths.length; i += 1) {
707
712
  const frozen = await freezeRegularFileAttachment(attachmentPaths[i], attachmentsDirectory, i);
708
713
  attachments.push(frozen.attachment);
@@ -720,7 +725,7 @@ export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, su
720
725
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
721
726
  const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
722
727
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
723
- return freezeAttachments(attachmentPaths, attachmentsDirectory);
728
+ return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
724
729
  }
725
730
  /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
726
731
  export async function freezePreparedAttachmentsIntoRun(prepared, runDirectory, summonsKey) {
@@ -969,7 +974,7 @@ export async function admitPublicRole(role, parsed, env, override) {
969
974
  const detail = errorText(error);
970
975
  throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
971
976
  }
972
- frozenAttachments = await freezeAttachments(attachmentPaths, placed.attachmentsDirectory);
977
+ frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
973
978
  return {
974
979
  issueNumber,
975
980
  caseRunsPath,
@@ -1126,7 +1131,7 @@ async function placeRoleAdmission(options) {
1126
1131
  });
1127
1132
  const attachments = options.freezeAttachments === false
1128
1133
  ? []
1129
- : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1134
+ : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1130
1135
  return {
1131
1136
  runId,
1132
1137
  bookKey,
@@ -1151,8 +1156,7 @@ function persistedAttachmentRefs(attachments) {
1151
1156
  }));
1152
1157
  }
1153
1158
  async function persistPlacedAdmission(admitted, placed, model) {
1154
- const admittedRequestPath = join(placed.runDirectory, "admitted-request.json");
1155
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1159
+ writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
1156
1160
  sessionDirectory: placed.sessionDirectory,
1157
1161
  sessionFile: placed.sessionFile,
1158
1162
  });
@@ -1161,11 +1165,10 @@ async function persistPlacedAdmission(admitted, placed, model) {
1161
1165
  sessionDirectory: placed.sessionDirectory,
1162
1166
  sessionFile: placed.sessionFile,
1163
1167
  }, admitted.role, model);
1164
- return admittedRequestPath;
1165
1168
  }
1166
1169
  /**
1167
1170
  * Shared admission: project check, placement, attachment freeze,
1168
- * admitted-request and invocation ledger write.
1171
+ * admitted and invocation section write.
1169
1172
  * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1170
1173
  * before materializeCountersignInvocation writes the page.
1171
1174
  * Seats whose extra facts are known before placement pass them as admittedFields.
@@ -1214,9 +1217,8 @@ async function admitStandardMaterialInvocation(role, options) {
1214
1217
  attachments: persistedAttachmentRefs(placed.attachments),
1215
1218
  ...placed.ticketFields,
1216
1219
  };
1217
- const admittedRequestPath = defer
1218
- ? join(placed.runDirectory, "admitted-request.json")
1219
- : await persistPlacedAdmission(admitted, placed, options.model);
1220
+ if (!defer)
1221
+ await persistPlacedAdmission(admitted, placed, options.model);
1220
1222
  return {
1221
1223
  role,
1222
1224
  runId: placed.runId,
@@ -1227,7 +1229,6 @@ async function admitStandardMaterialInvocation(role, options) {
1227
1229
  attachments: placed.attachments,
1228
1230
  runDirectory: placed.runDirectory,
1229
1231
  principal: placed.principal,
1230
- admittedRequestPath,
1231
1232
  ...correlationFields,
1232
1233
  ...placed.ticketFields,
1233
1234
  ...admittedFields,
@@ -1304,7 +1305,6 @@ export async function materializeCountersignInvocation(admitted, options) {
1304
1305
  });
1305
1306
  const placement = placed;
1306
1307
  admitted.runDirectory = placement.runDirectory;
1307
- admitted.admittedRequestPath = join(placement.runDirectory, "admitted-request.json");
1308
1308
  admitted.principal = placement.principal;
1309
1309
  const ticketFields = ticketAdmissionFields(options.ticketNumber);
1310
1310
  if (ticketFields.ticketNumber !== undefined) {
@@ -1312,19 +1312,18 @@ export async function materializeCountersignInvocation(admitted, options) {
1312
1312
  }
1313
1313
  const attachments = await freezePreparedAttachmentsIntoRun(options.preparedAttachments, admitted.runDirectory);
1314
1314
  admitted.attachments = attachments;
1315
- await writeAdmittedRequestPersistence(admitted.admittedRequestPath, admitted, {
1315
+ writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
1316
1316
  sessionDirectory: placement.sessionDirectory,
1317
1317
  sessionFile: placement.sessionFile,
1318
1318
  });
1319
1319
  await writeRoleInvocationLedger({ ...admitted, sessionDirectory: placement.sessionDirectory, sessionFile: placement.sessionFile }, admitted.role, options.model);
1320
1320
  }
1321
- /** Load admitted-request.json written at admission (Navigator work-context seam). */
1321
+ /** Load the admitted section written at admission (Navigator work-context seam). */
1322
1322
  export async function loadAdmittedJudgeRequest(runDirectory) {
1323
1323
  try {
1324
- const raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
1325
- if (!isRecord(raw))
1324
+ const record = readPageSync(runDirectory, "admitted");
1325
+ if (record === undefined)
1326
1326
  return undefined;
1327
- const record = raw;
1328
1327
  if (!packagedPublicInstructionSubject(record.role))
1329
1328
  return undefined;
1330
1329
  if (typeof record.instruction !== "string")
@@ -1343,10 +1342,6 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
1343
1342
  return undefined;
1344
1343
  }
1345
1344
  }
1346
- export async function ensureRunArtifactsDir(runDirectory) {
1347
- const directory = roleRunArtifactsDirectory(runDirectory);
1348
- return ensureRoleRunDirectory(resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)), directory);
1349
- }
1350
1345
  function parsePositivePrOption(raw) {
1351
1346
  if (raw === undefined || raw.trim() === "")
1352
1347
  throw new CliUsageError("--pr requires a positive pull request number");