@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
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Station resolution (four-layer fallback):
8
8
  * 1) terminating tool name inside the session (ak_<role>_output)
9
- * 2) invocation.json role
9
+ * 2) current.json invocation role
10
10
  * 3) run-directory name heuristic
11
11
  * 4) unknown (still listed; never dropped)
12
12
  *
@@ -18,19 +18,27 @@
18
18
  * write does not advertise refresh. Regeneration faults surface the original
19
19
  * cause via handle.closed / stop(). Caller stops the handle.
20
20
  */
21
- import { randomUUID } from "node:crypto";
22
- import { lstat, mkdir, readdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
21
+ import { readSectionSync } from "./run-dossier.ts";
22
+ import { lstat, mkdir, readdir, readFile, realpath } from "node:fs/promises";
23
23
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
24
24
  import { pathToFileURL } from "node:url";
25
25
 
26
+ import { writeFileAtomically } from "./atomic-write.ts";
27
+ import {
28
+ errnoCode,
29
+ pathContainedIn,
30
+ physicalPathIdentity,
31
+ } from "./activation-ledger-topology.ts";
32
+
26
33
  import {
27
34
  formatDurationZh,
28
35
  formatLocalDateTime,
29
36
  formatTokensCompact,
30
37
  formatUsdPrecise,
31
38
  } from "./human-format.ts";
32
- import { listBookRunDirectories } from "./role-run-placement.ts";
33
- import { readRunTicketNumber } from "./run-ticket-number.ts";
39
+ import { listBookRunDirectories, sessionDirectoryOf } from "./role-run-placement.ts";
40
+ import { sitianVolumeDirectory } from "./sitian-appender.ts";
41
+ import { isPositiveTicketNumber, readRunTicketNumber } from "./run-ticket-number.ts";
34
42
  import {
35
43
  extractSessionTimestampSpan,
36
44
  readLedgerSessionJsonl,
@@ -279,9 +287,8 @@ type InvocationInfo = {
279
287
 
280
288
  async function readInvocation(runDir: string): Promise<InvocationInfo | undefined> {
281
289
  try {
282
- const raw = await readFile(join(runDir, "invocation.json"), "utf8");
283
- const parsed: unknown = JSON.parse(raw);
284
- if (!isRecord(parsed)) return undefined;
290
+ const parsed = readSectionSync(runDir, "invocation");
291
+ if (parsed === undefined) return undefined;
285
292
  const info: InvocationInfo = {};
286
293
  if (typeof parsed.role === "string" && parsed.role.trim()) info.role = parsed.role.trim();
287
294
  if (typeof parsed.thinking === "string" && parsed.thinking.trim()) info.thinking = parsed.thinking.trim();
@@ -350,7 +357,7 @@ async function listSessionFiles(sessionDir: string): Promise<string[]> {
350
357
 
351
358
  /** Reviewer parallel axis-leg sessions live under session/reviewer-legs/. */
352
359
  async function listAxisLegSessionFiles(sessionDir: string): Promise<string[]> {
353
- return listSessionFiles(join(sessionDir, "reviewer-legs"));
360
+ return listSessionFiles(sitianVolumeDirectory(sessionDir, "reviewer-legs"));
354
361
  }
355
362
 
356
363
  async function maxMtimeMs(paths: readonly string[]): Promise<number> {
@@ -372,7 +379,7 @@ async function parseRunDirectory(runDir: string, ledgerCoord: string): Promise<P
372
379
  const runId = basename(runDir);
373
380
  const evidenceTarget = await realpathOrLexicalIfMissing(runDir);
374
381
  const evidenceHref = pathToFileURL(evidenceTarget).href;
375
- const sessionDir = join(runDir, "session");
382
+ const sessionDir = sessionDirectoryOf(runDir);
376
383
  const sessionFiles = await listSessionFiles(sessionDir);
377
384
  const axisLegFiles = await listAxisLegSessionFiles(sessionDir);
378
385
  const rows: SessionRow[] = [];
@@ -425,7 +432,7 @@ async function parseRunDirectory(runDir: string, ledgerCoord: string): Promise<P
425
432
  ...(invocation?.role !== undefined ? { invocationRole: invocation.role } : {}),
426
433
  });
427
434
 
428
- // Session mechanical fields win; invocation.json fills gaps only.
435
+ // Session mechanical fields win; the invocation section fills gaps only.
429
436
  let { model, provider, thinking } = models;
430
437
  if (invocation) {
431
438
  if (!thinking && invocation.thinking) thinking = invocation.thinking;
@@ -653,12 +660,12 @@ ${stationBlocks || "<p data-empty=\"true\">no runs</p>"}
653
660
  * Factory board reuses this — no parallel receipt parser.
654
661
  *
655
662
  * Book-run attribution (#176 / #859): read typed `ticketNumber` from each run's
656
- * invocation.json under listBookRunDirectories (flat legacy `runs/` + subject-tree
663
+ * current.json invocation section under listBookRunDirectories (flat legacy `runs/` + subject-tree
657
664
  * `<ticket|unbound>/runs/`). Legacy `issues/<N>/runs` remains a read-only
658
665
  * compatibility entrance. Unbound runs never join a ticket.
659
666
  */
660
667
 
661
- /** One book run's typed binding projection from invocation.json. */
668
+ /** One book run's typed binding projection from the invocation section. */
662
669
  export type FlatRunTicketBinding = {
663
670
  readonly runDir: string;
664
671
  readonly runFolder: string;
@@ -671,10 +678,10 @@ export type FlatRunTicketBinding = {
671
678
 
672
679
  /**
673
680
  * Book-level run index built once per lane/render and reused per ticket.
674
- * Pure derived view of book runs' invocation.json files — not a parallel ledger.
681
+ * Pure derived view of book runs' current.json invocation sections — not a parallel ledger.
675
682
  */
676
683
  export type TicketTrajectoryBookIndex = {
677
- /** ticket number → runs bound via invocation.json ticketNumber. */
684
+ /** ticket number → runs bound via invocation ticketNumber. */
678
685
  readonly runsByTicket: ReadonlyMap<number, readonly FlatRunTicketBinding[]>;
679
686
  /** Runs with no typed ticketNumber (unknown/unbound seam). */
680
687
  readonly unboundRuns: readonly FlatRunTicketBinding[];
@@ -749,7 +756,7 @@ export async function loadTicketTrajectoryRuns(
749
756
  issueNumber: number,
750
757
  bookIndex?: TicketTrajectoryBookIndex,
751
758
  ): Promise<TicketTrajectoryRun[]> {
752
- if (!Number.isInteger(issueNumber) || issueNumber < 1) {
759
+ if (!isPositiveTicketNumber(issueNumber)) {
753
760
  throw new Error("issueNumber must be a positive integer");
754
761
  }
755
762
  const root = resolve(ledgerDir);
@@ -767,7 +774,7 @@ export async function loadTicketTrajectoryRuns(
767
774
  runs.push(parsed);
768
775
  }
769
776
 
770
- // Book runs (flat + subject-tree) bound by typed invocation.json ticketNumber.
777
+ // Book runs (flat + subject-tree) bound by typed invocation ticketNumber.
771
778
  const flatBindings = index.runsByTicket.get(issueNumber) ?? [];
772
779
  for (const binding of flatBindings) {
773
780
  const resolvedRunDir = resolve(binding.runDir);
@@ -789,7 +796,7 @@ export async function renderTicketTrajectoryHtml(
789
796
  now: Date,
790
797
  options?: { refreshBoundarySeconds?: number },
791
798
  ): Promise<string> {
792
- if (!isRecord(ticketSnapshot) || typeof ticketSnapshot.issueNumber !== "number" || !Number.isInteger(ticketSnapshot.issueNumber) || ticketSnapshot.issueNumber < 1) {
799
+ if (!isRecord(ticketSnapshot) || !isPositiveTicketNumber(ticketSnapshot.issueNumber)) {
793
800
  throw new Error("ticketSnapshot.issueNumber must be a positive integer");
794
801
  }
795
802
  const issueNumber = ticketSnapshot.issueNumber;
@@ -807,118 +814,96 @@ export async function renderTicketTrajectoryHtml(
807
814
  });
808
815
  }
809
816
 
810
- function isPathInside(parent: string, child: string): boolean {
811
- const rel = relative(parent, child);
812
- return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !rel.startsWith(".."));
817
+ function landsInLedger(ledgerRoot: string, candidate: string): boolean {
818
+ // mkdir may have materialized a previously dangling ledger alias since the first gate.
819
+ const physicalRoot = physicalPathIdentity(ledgerRoot);
820
+ const lexical = resolve(candidate);
821
+ const physical = physicalPathIdentity(candidate);
822
+ const inside = (id: string) => physicalRoot === id || pathContainedIn(physicalRoot, id);
823
+ return inside(lexical) || inside(physical);
813
824
  }
814
825
 
815
826
  /**
816
- * Resolve the prospective on-disk target of outputPath and refuse any landing
817
- * inside the ledger — including when the path, a parent, or a trailing segment
818
- * is a symlink into the ledger tree.
827
+ * Prospective output must stay outside every ledger root.
828
+ * Equality counts as inside. Symlink parents are followed by physical identity.
819
829
  */
820
- export async function assertTrajectoryOutputOutsideLedger(
821
- ledgerDir: string,
830
+ export async function assertOutputOutsideLedgers(
831
+ ledgerDirs: readonly string[],
822
832
  outputPath: string,
823
- ): Promise<{ ledgerRoot: string; outputAbsolute: string; prospectiveReal: string }> {
824
- const ledgerResolved = resolve(ledgerDir);
825
- let ledgerRoot: string;
826
- try {
827
- ledgerRoot = await realpath(ledgerResolved);
828
- } catch (error) {
829
- if (!isMissingPathError(error)) throw error;
830
- ledgerRoot = ledgerResolved;
831
- }
833
+ refusalMessage: string,
834
+ ): Promise<{ ledgerRoots: string[]; outputAbsolute: string; prospectiveReal: string }> {
832
835
  const outputAbsolute = resolve(outputPath);
833
-
834
- // Walk up until an existing filesystem node is found; realpath that prefix
835
- // and rejoin the missing tail so symlink parents are fully followed.
836
- const missingTail: string[] = [];
837
- let cursor = outputAbsolute;
838
- for (;;) {
836
+ const prospectiveReal = physicalPathIdentity(outputAbsolute);
837
+ const ledgerRoots: string[] = [];
838
+ for (const ledgerDir of ledgerDirs) {
839
+ const ledgerResolved = resolve(ledgerDir);
840
+ let ledgerRoot: string;
839
841
  try {
840
- await lstat(cursor);
841
- break;
842
+ ledgerRoot = await realpath(ledgerResolved);
842
843
  } catch (error) {
843
844
  if (!isMissingPathError(error)) throw error;
844
- const parent = dirname(cursor);
845
- if (parent === cursor) break;
846
- missingTail.push(basename(cursor));
847
- cursor = parent;
845
+ ledgerRoot = physicalPathIdentity(ledgerResolved);
846
+ }
847
+ ledgerRoots.push(ledgerRoot);
848
+ if (landsInLedger(ledgerRoot, prospectiveReal) || landsInLedger(ledgerRoot, outputAbsolute)) {
849
+ throw new Error(refusalMessage);
848
850
  }
849
851
  }
850
-
851
- let realPrefix: string;
852
- try {
853
- realPrefix = await realpath(cursor);
854
- } catch (error) {
855
- if (!isMissingPathError(error)) throw error;
856
- realPrefix = resolve(cursor);
857
- }
858
-
859
- const prospectiveReal =
860
- missingTail.length === 0 ? realPrefix : resolve(realPrefix, ...missingTail.reverse());
861
-
862
- if (isPathInside(ledgerRoot, prospectiveReal) || isPathInside(ledgerRoot, realPrefix)) {
863
- throw new Error("ticket trajectory outputPath must be outside the ledger directory");
864
- }
865
-
866
- // Lexical absolute path must also stay outside (defense in depth before mkdir).
867
- if (isPathInside(ledgerRoot, outputAbsolute)) {
868
- throw new Error("ticket trajectory outputPath must be outside the ledger directory");
869
- }
870
-
871
- return { ledgerRoot, outputAbsolute, prospectiveReal };
852
+ return { ledgerRoots, outputAbsolute, prospectiveReal };
872
853
  }
873
854
 
874
855
  /**
875
- * Write HTML to an explicit path outside the ledger without following an
876
- * existing destination inode (hard link / prior file). Temp file + rename
877
- * replaces the directory entry so a hard-linked ledger twin keeps its bytes.
856
+ * Write HTML outside the ledgers. Temp+rename does not open an existing inode.
857
+ * An existing symlink whose target is inside a ledger is refused.
878
858
  */
879
- async function writeHtmlAtomicallyOutsideLedger(input: {
880
- ledgerRoot: string;
881
- outputAbsolute: string;
882
- html: string;
883
- }): Promise<string> {
884
- const parent = dirname(input.outputAbsolute);
859
+ export async function writePageOutsideLedgers(
860
+ ledgerRoots: readonly string[],
861
+ outputAbsolute: string,
862
+ html: string,
863
+ refusalMessage: string,
864
+ ): Promise<string> {
865
+ const parent = dirname(outputAbsolute);
885
866
  await mkdir(parent, { recursive: true });
886
-
887
- // Re-resolve after mkdir: a race or symlink parent must still land outside.
888
867
  const parentReal = await realpath(parent);
889
- if (isPathInside(input.ledgerRoot, parentReal)) {
890
- throw new Error("ticket trajectory outputPath must be outside the ledger directory");
891
- }
892
-
893
- const destinationReal = resolve(parentReal, basename(input.outputAbsolute));
894
- if (isPathInside(input.ledgerRoot, destinationReal)) {
895
- throw new Error("ticket trajectory outputPath must be outside the ledger directory");
868
+ const destinationReal = resolve(parentReal, basename(outputAbsolute));
869
+ for (const root of ledgerRoots) {
870
+ if (landsInLedger(root, parentReal) || landsInLedger(root, destinationReal)) {
871
+ throw new Error(refusalMessage);
872
+ }
896
873
  }
897
-
898
- // Refuse to write through an existing symlink whose target is inside the ledger.
899
874
  try {
900
- const existing = await lstat(input.outputAbsolute);
875
+ const existing = await lstat(outputAbsolute);
901
876
  if (existing.isSymbolicLink()) {
902
- const target = await realpath(input.outputAbsolute);
903
- if (isPathInside(input.ledgerRoot, target)) {
904
- throw new Error("ticket trajectory outputPath must be outside the ledger directory");
877
+ const target = await realpath(outputAbsolute);
878
+ for (const root of ledgerRoots) {
879
+ if (landsInLedger(root, target)) throw new Error(refusalMessage);
905
880
  }
906
881
  }
907
882
  } catch (error) {
908
- if (!isMissingPathError(error)) throw error;
883
+ if (errnoCode(error) !== "ENOENT" && errnoCode(error) !== "ENOTDIR") throw error;
909
884
  }
885
+ await writeFileAtomically(outputAbsolute, html);
886
+ return realpath(outputAbsolute);
887
+ }
910
888
 
911
- // Same-directory temp + rename: does not open/truncate an existing inode, so a
912
- // hard link from outputPath into the ledger cannot smuggle writes back home.
913
- const temporary = join(parent, `.ticket-trajectory-${randomUUID()}.html.tmp`);
914
- try {
915
- await writeFile(temporary, input.html, "utf8");
916
- await rename(temporary, input.outputAbsolute);
917
- } catch (error) {
918
- await rm(temporary, { force: true }).catch(() => undefined);
919
- throw error;
920
- }
921
- return realpath(input.outputAbsolute);
889
+ const TRAJECTORY_OUTPUT_REFUSAL =
890
+ "ticket trajectory outputPath must be outside the ledger directory";
891
+
892
+ /**
893
+ * Resolve the prospective on-disk target of outputPath and refuse any landing
894
+ * inside the ledger — including when the path, a parent, or a trailing segment
895
+ * is a symlink into the ledger tree.
896
+ */
897
+ export async function assertTrajectoryOutputOutsideLedger(
898
+ ledgerDir: string,
899
+ outputPath: string,
900
+ ): Promise<{ ledgerRoot: string; outputAbsolute: string; prospectiveReal: string }> {
901
+ const gate = await assertOutputOutsideLedgers([ledgerDir], outputPath, TRAJECTORY_OUTPUT_REFUSAL);
902
+ return {
903
+ ledgerRoot: gate.ledgerRoots[0]!,
904
+ outputAbsolute: gate.outputAbsolute,
905
+ prospectiveReal: gate.prospectiveReal,
906
+ };
922
907
  }
923
908
 
924
909
  /**
@@ -943,11 +928,12 @@ export async function writeTicketTrajectoryPage(input: {
943
928
  : undefined,
944
929
  );
945
930
 
946
- const outputPath = await writeHtmlAtomicallyOutsideLedger({
947
- ledgerRoot: gate.ledgerRoot,
948
- outputAbsolute: gate.outputAbsolute,
931
+ const outputPath = await writePageOutsideLedgers(
932
+ [gate.ledgerRoot],
933
+ gate.outputAbsolute,
949
934
  html,
950
- });
935
+ TRAJECTORY_OUTPUT_REFUSAL,
936
+ );
951
937
  return { outputPath, html };
952
938
  }
953
939