@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca

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 (109) hide show
  1. package/README.md +129 -39
  2. package/README.zh-CN.md +128 -39
  3. package/dist/activation-ledger-topology.js +1 -1
  4. package/dist/archivist-record-entry.js +47 -26
  5. package/dist/auditor-dossier-tool.js +3 -1
  6. package/dist/compliance-transport.js +17 -26
  7. package/dist/doctor-contracts.js +1 -2
  8. package/dist/dossier-resolution.js +4 -8
  9. package/dist/engine-detour-tool.js +33 -8
  10. package/dist/engine-detour.js +1 -0
  11. package/dist/evidence-child-executor.js +277 -424
  12. package/dist/host-contracts.js +22 -0
  13. package/dist/institutional-resolution.js +88 -0
  14. package/dist/merger-contracts.js +1 -2
  15. package/dist/navigator-attendance.js +16 -0
  16. package/dist/notary-contracts.js +2 -3
  17. package/dist/package-contracts/fixer-output.js +1 -2
  18. package/dist/packaged-role-registry.js +102 -9
  19. package/dist/pi/in-process-session.js +671 -0
  20. package/dist/public-cli/config.js +7 -5
  21. package/dist/public-cli/main.js +3767 -4113
  22. package/dist/reviewer-child-executor.js +6 -1
  23. package/dist/session-opening-materials.js +10 -38
  24. package/dist/sitian-appender.js +146 -0
  25. package/dist/sitian-contracts.js +21 -0
  26. package/dist/sitian-facade.js +15 -0
  27. package/dist/sitian-reader.js +52 -0
  28. package/extensions/role-runtime.ts +9 -7
  29. package/package.json +5 -5
  30. package/resources/engines/cursor.md +12 -26
  31. package/resources/engines/hermes.md +2 -23
  32. package/resources/engines/opus.md +1 -16
  33. package/resources/navigator-route-playbook.md +2 -2
  34. package/scripts/build-package.mjs +0 -1
  35. package/souls/quality-law.md +1 -1
  36. package/src/activation-ledger-topology.ts +1 -1
  37. package/src/archivist-record-entry.ts +57 -45
  38. package/src/auditor-dossier-tool.ts +4 -2
  39. package/src/canonical-skill-binding.ts +37 -27
  40. package/src/collector-role.ts +11 -20
  41. package/src/collector-tool-schemas.ts +1 -4
  42. package/src/compliance-transport.ts +19 -44
  43. package/src/doctor-auditor.ts +1 -5
  44. package/src/doctor-contracts.ts +1 -4
  45. package/src/doctor-role.ts +31 -8
  46. package/src/dossier-resolution.ts +8 -8
  47. package/src/engine-detour-tool.ts +48 -19
  48. package/src/engine-detour.ts +3 -0
  49. package/src/evidence-child-executor.ts +277 -485
  50. package/src/factory-board.ts +3 -3
  51. package/src/gatekeeper-role.ts +21 -10
  52. package/src/host-contracts.ts +386 -0
  53. package/src/in-process-session.ts +1 -115
  54. package/src/institutional-resolution.ts +135 -0
  55. package/src/judge-auditor.ts +1 -5
  56. package/src/judge-role.ts +37 -51
  57. package/src/merger-contracts.ts +1 -4
  58. package/src/merger-role.ts +7 -11
  59. package/src/navigator-attendance.ts +20 -1
  60. package/src/notary-contracts.ts +8 -11
  61. package/src/notary-role.ts +7 -23
  62. package/src/notary-source-run.ts +4 -3
  63. package/src/package-contracts/fixer-output.ts +1 -4
  64. package/src/package-resources/method-skill-binding.ts +10 -34
  65. package/src/packaged-role-registry.ts +122 -12
  66. package/src/pi/adapter.ts +192 -0
  67. package/src/pi/durable-principal.ts +98 -0
  68. package/src/pi/in-process-session.ts +819 -0
  69. package/src/pi/known-failure.ts +77 -0
  70. package/src/pi/pi-normalization.ts +284 -0
  71. package/src/pi/role-turn-host.ts +459 -0
  72. package/src/public-cli/auto-resume.ts +42 -44
  73. package/src/public-cli/cli.ts +171 -257
  74. package/src/public-cli/coder-run.ts +115 -433
  75. package/src/public-cli/collector-run.ts +62 -314
  76. package/src/public-cli/config.ts +7 -5
  77. package/src/public-cli/doctor-run.ts +41 -52
  78. package/src/public-cli/fixer-run.ts +122 -427
  79. package/src/public-cli/invocation.ts +287 -96
  80. package/src/public-cli/judge-run.ts +92 -422
  81. package/src/public-cli/merger-run.ts +167 -429
  82. package/src/public-cli/notary-run.ts +41 -47
  83. package/src/public-cli/option-definitions.ts +136 -3
  84. package/src/public-cli/post-admission.ts +497 -0
  85. package/src/public-cli/public-run-credentials.ts +4 -4
  86. package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
  87. package/src/public-cli/reviewer-run.ts +132 -449
  88. package/src/public-cli/run-lifecycle.ts +339 -327
  89. package/src/public-cli/settlement.ts +251 -134
  90. package/src/public-cli/terminal.ts +3 -9
  91. package/src/public-cli/turn-request.ts +87 -0
  92. package/src/reviewer-child-executor.ts +9 -1
  93. package/src/reviewer-role.ts +11 -20
  94. package/src/role-runtime.ts +129 -78
  95. package/src/session-opening-materials.ts +25 -39
  96. package/src/sitian-appender.ts +171 -0
  97. package/src/sitian-contracts.ts +106 -0
  98. package/src/sitian-facade.ts +18 -0
  99. package/src/sitian-reader.ts +62 -0
  100. package/src/worker-role.ts +42 -40
  101. package/src/worker-submission-gates.ts +33 -0
  102. package/dist/archivist-role-run-coordinates.js +0 -28
  103. package/dist/package-contracts/terminating-infrastructure.js +0 -90
  104. package/resources/engines/sonnet.md +0 -27
  105. package/resources/engines/zcode.md +0 -63
  106. package/src/archivist-role-run-coordinates.ts +0 -45
  107. package/src/package-contracts/terminating-infrastructure.ts +0 -132
  108. package/src/public-cli/explicit-internal.ts +0 -427
  109. package/src/public-cli/one-shot-dispatch.ts +0 -300
@@ -1,4 +1,4 @@
1
- /** The single package-owned source for role attendance and settlement metadata. */
1
+ /** Composition-root unique authoritative public-role records (#509 / #524). */
2
2
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
3
3
  import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.ts";
4
4
  import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.ts";
@@ -7,20 +7,130 @@ import { DOCTOR_OUTPUT_TOOL_NAME } from "./doctor-contracts.ts";
7
7
  import { MERGER_OUTPUT_TOOL_NAME } from "./merger-contracts.ts";
8
8
  import { NOTARY_OUTPUT_TOOL_NAME } from "./notary-contracts.ts";
9
9
 
10
- export const PACKAGED_ROLE_REGISTRY = [
11
- { role: "judge", phases: [null], outputTool: JUDGE_OUTPUT_TOOL_NAME, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
12
- { role: "fixer", phases: ["plan", "apply"], outputTool: FIXER_OUTPUT_TOOL_NAME, inputFlag: "ak-fix-packet", phaseFlag: "ak-fixer-phase", activationStage: "load-and-install" },
13
- { role: "coder", phases: ["plan", "apply"], outputTool: CODER_OUTPUT_TOOL_NAME, inputFlag: "ak-coder-task", phaseFlag: "ak-coder-phase", activationStage: "load-and-install" },
14
- { role: "reviewer", phases: [null], outputTool: REVIEWER_OUTPUT_TOOL_NAME, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
10
+ /** Shared by public notary and gatekeeper-province notary. */
11
+ export const NOTARY_SESSION_MATERIALS = [
12
+ "CLAUDE.md",
13
+ "souls/notary.md",
14
+ "souls/gate-output-guide.md",
15
+ ] as const;
16
+
17
+ /**
18
+ * One record per public callable role. Navigator is absent (name-only materials
19
+ * live on the session-opening projection).
20
+ */
21
+ export const PUBLIC_ROLE_RECORDS = [
22
+ {
23
+ role: "judge",
24
+ phases: [null],
25
+ outputTool: JUDGE_OUTPUT_TOOL_NAME,
26
+ inputFlag: undefined,
27
+ phaseFlag: undefined,
28
+ activationStage: "load-and-install",
29
+ sessionMaterials: [
30
+ "CLAUDE.md",
31
+ "souls/judge.md",
32
+ "souls/audit-law.md",
33
+ "souls/quality-law.md",
34
+ "souls/judge-output-guide.md",
35
+ ],
36
+ },
37
+ {
38
+ role: "fixer",
39
+ phases: ["plan", "apply"],
40
+ outputTool: FIXER_OUTPUT_TOOL_NAME,
41
+ inputFlag: "ak-fix-packet",
42
+ phaseFlag: "ak-fixer-phase",
43
+ activationStage: "load-and-install",
44
+ sessionMaterials: [
45
+ "CLAUDE.md",
46
+ "souls/fixer.md",
47
+ "souls/quality-law.md",
48
+ "souls/fixer-output-guide.md",
49
+ ],
50
+ },
51
+ {
52
+ role: "coder",
53
+ phases: ["plan", "apply"],
54
+ outputTool: CODER_OUTPUT_TOOL_NAME,
55
+ inputFlag: "ak-coder-task",
56
+ phaseFlag: "ak-coder-phase",
57
+ activationStage: "load-and-install",
58
+ sessionMaterials: [
59
+ "CLAUDE.md",
60
+ "souls/coder.md",
61
+ "souls/quality-law.md",
62
+ "souls/coder-output-guide.md",
63
+ ],
64
+ },
65
+ {
66
+ role: "reviewer",
67
+ phases: [null],
68
+ outputTool: REVIEWER_OUTPUT_TOOL_NAME,
69
+ inputFlag: undefined,
70
+ phaseFlag: undefined,
71
+ activationStage: "load-and-install",
72
+ sessionMaterials: [
73
+ "CLAUDE.md",
74
+ "souls/reviewer.md",
75
+ "souls/audit-law.md",
76
+ "souls/quality-law.md",
77
+ ],
78
+ },
15
79
  // ak-collector-repo is GitHub owner/repo identity, not a local material path (#438).
16
- { role: "collector", phases: [null], outputTool: COLLECTOR_OUTPUT_TOOL, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
17
- { role: "doctor", phases: [null], outputTool: DOCTOR_OUTPUT_TOOL_NAME, inputFlag: "ak-doctor-case", phaseFlag: undefined, activationStage: "load-and-install" },
18
- { role: "merger", phases: [null], outputTool: MERGER_OUTPUT_TOOL_NAME, inputFlag: "ak-merger-input", phaseFlag: undefined, activationStage: "prepare-git-and-install" },
19
- { role: "notary", phases: [null], outputTool: NOTARY_OUTPUT_TOOL_NAME, inputFlag: "ak-notary-source-run", phaseFlag: undefined, activationStage: "load-and-install" },
80
+ {
81
+ role: "collector",
82
+ phases: [null],
83
+ outputTool: COLLECTOR_OUTPUT_TOOL,
84
+ inputFlag: undefined,
85
+ phaseFlag: undefined,
86
+ activationStage: "load-and-install",
87
+ sessionMaterials: ["CLAUDE.md", "souls/collector.md"],
88
+ },
89
+ {
90
+ role: "doctor",
91
+ phases: [null],
92
+ outputTool: DOCTOR_OUTPUT_TOOL_NAME,
93
+ inputFlag: "ak-doctor-case",
94
+ phaseFlag: undefined,
95
+ activationStage: "load-and-install",
96
+ sessionMaterials: ["CLAUDE.md", "souls/doctor.md"],
97
+ },
98
+ {
99
+ role: "merger",
100
+ phases: [null],
101
+ outputTool: MERGER_OUTPUT_TOOL_NAME,
102
+ inputFlag: "ak-merger-input",
103
+ phaseFlag: undefined,
104
+ activationStage: "prepare-git-and-install",
105
+ sessionMaterials: ["CLAUDE.md", "souls/merger.md"],
106
+ },
107
+ {
108
+ role: "notary",
109
+ phases: [null],
110
+ outputTool: NOTARY_OUTPUT_TOOL_NAME,
111
+ inputFlag: "ak-notary-source-run",
112
+ phaseFlag: undefined,
113
+ activationStage: "load-and-install",
114
+ sessionMaterials: NOTARY_SESSION_MATERIALS,
115
+ },
20
116
  ] as const;
21
117
 
22
- export type PackagedRole = (typeof PACKAGED_ROLE_REGISTRY)[number]["role"];
23
- export type PackagedRoleMetadata = (typeof PACKAGED_ROLE_REGISTRY)[number];
118
+ export type PublicRoleRecord = (typeof PUBLIC_ROLE_RECORDS)[number];
119
+ export type PackagedRole = PublicRoleRecord["role"];
120
+
121
+ /**
122
+ * Read-only metadata projection (no sessionMaterials).
123
+ * Distributed per PublicRoleRecord member so role↔field associations stay intact.
124
+ */
125
+ export type PackagedRoleMetadata = PublicRoleRecord extends infer R
126
+ ? R extends PublicRoleRecord
127
+ ? Omit<R, "sessionMaterials">
128
+ : never
129
+ : never;
130
+
131
+ /** Historical symbol — derived from PUBLIC_ROLE_RECORDS. */
132
+ export const PACKAGED_ROLE_REGISTRY: readonly PackagedRoleMetadata[] =
133
+ PUBLIC_ROLE_RECORDS.map(({ sessionMaterials: _omit, ...metadata }) => metadata);
24
134
 
25
135
  export function packagedRoleMetadata(role: string): PackagedRoleMetadata | undefined {
26
136
  return PACKAGED_ROLE_REGISTRY.find((entry) => entry.role === role);
@@ -0,0 +1,192 @@
1
+ import {
2
+ parseSkillBlock,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ type SessionManager,
6
+ type ToolDefinition,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import type { Static, TSchema } from "typebox";
9
+ import { requireGatekeeperPass, type GatekeeperNonPassResult, type GatekeeperSubject } from "../gatekeeper-role.ts";
10
+ import type {
11
+ HostContext,
12
+ HostEventRegistration,
13
+ HostGatekeeperActions,
14
+ HostToolDefinition,
15
+ HostToolResult,
16
+ RoleHost,
17
+ } from "../host-contracts.ts";
18
+
19
+ export type PiRoleHostAdapter = {
20
+ readonly host: RoleHost;
21
+ };
22
+
23
+ const piContexts = new WeakMap<HostContext, ExtensionContext>();
24
+
25
+ /** Project Pi's activation context onto the package-owned host contract. */
26
+ function projectPiContext(context: ExtensionContext, transcriptFromContext?: (context: ExtensionContext) => string): HostContext {
27
+ const sessionManager = context.sessionManager as SessionManager;
28
+ const host: HostContext = {
29
+ cwd: context.cwd,
30
+ mode: context.mode,
31
+ model: context.model === undefined ? undefined : { provider: context.model.provider },
32
+ sessionManager: {
33
+ getLeafEntry: () => context.sessionManager.getLeafEntry(),
34
+ getLeafId: () => context.sessionManager.getLeafId(),
35
+ getEntries: () => context.sessionManager.getEntries(),
36
+ getSessionDir: () => context.sessionManager.getSessionDir(),
37
+ getSessionFile: () => context.sessionManager.getSessionFile(),
38
+ getHeader: () => context.sessionManager.getHeader(),
39
+ setSessionFile: (path) => sessionManager.setSessionFile(path),
40
+ appendCustomEntry: (customType, data) => sessionManager.appendCustomEntry(customType, data),
41
+ },
42
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
43
+ ...(context.ui === undefined ? {} : { ui: { notify: (message, type) => context.ui.notify(message, type) } }),
44
+ ...(transcriptFromContext === undefined ? {} : { transcript: () => transcriptFromContext(context) }),
45
+ abort: () => context.abort(),
46
+ };
47
+ piContexts.set(host, context);
48
+ return host;
49
+ }
50
+
51
+ /** Recover the Pi context only at the Pi composition boundary. */
52
+ export function toPiContext(context: HostContext): ExtensionContext {
53
+ const piContext = piContexts.get(context);
54
+ if (piContext === undefined) throw new Error("Host context is not backed by the Pi adapter");
55
+ return piContext;
56
+ }
57
+
58
+ /** Gatekeeper receives HostContext directly (#518 §1③) without recovering raw ExtensionContext via WeakMap. */
59
+ function requirePiGatekeeperPass(options: {
60
+ context: HostContext;
61
+ subject: GatekeeperSubject;
62
+ signal?: AbortSignal;
63
+ hostActions: HostGatekeeperActions;
64
+ toolCallId: string;
65
+ }): Promise<void> {
66
+ return requireGatekeeperPass({
67
+ context: options.context,
68
+ subject: options.subject,
69
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
70
+ hostActions: {
71
+ failInfrastructure(error, _context, toolCallId) {
72
+ options.hostActions.failInfrastructure(error, options.context, toolCallId);
73
+ },
74
+ bindSubmissionNonPass: options.hostActions.bindSubmissionNonPass,
75
+ },
76
+ toolCallId: options.toolCallId,
77
+ });
78
+ }
79
+
80
+ function toPiResult<D>(result: HostToolResult<D>): HostToolResult<D> {
81
+ return result;
82
+ }
83
+
84
+ function toPiToolDefinition<S extends TSchema, D>(
85
+ tool: HostToolDefinition<S, D>,
86
+ projectContext: (context: ExtensionContext) => HostContext,
87
+ ): ToolDefinition<S, D> {
88
+ return {
89
+ name: tool.name,
90
+ label: tool.label,
91
+ description: tool.description,
92
+ ...(tool.promptSnippet === undefined ? {} : { promptSnippet: tool.promptSnippet }),
93
+ parameters: tool.parameters,
94
+ execute: async (toolCallId, params, signal, update, context) =>
95
+ toPiResult(await tool.execute(
96
+ toolCallId,
97
+ params as Static<S>,
98
+ signal,
99
+ update === undefined ? undefined : (result) => update(toPiResult(result)),
100
+ projectContext(context),
101
+ )),
102
+ };
103
+ }
104
+
105
+ /** Pi composition boundary. Each consumed capability is adapted explicitly. */
106
+ export function createPiRoleHostAdapter(
107
+ pi: ExtensionAPI,
108
+ options: { transcriptFromContext?: (context: ExtensionContext) => string } = {},
109
+ ): PiRoleHostAdapter {
110
+ const host: RoleHost = {
111
+ capabilities: {
112
+ skillExpansion(prompt) {
113
+ const parsed = parseSkillBlock(prompt);
114
+ if (parsed == null) return undefined;
115
+ const userMessage = parsed.userMessage ?? "";
116
+ return Object.freeze({
117
+ name: parsed.name,
118
+ location: parsed.location,
119
+ content: parsed.content,
120
+ userMessage,
121
+ });
122
+ },
123
+ },
124
+ registerFlag: (name, definition) => pi.registerFlag(name, definition),
125
+ getFlag: (name) => pi.getFlag(name),
126
+ registerTool: (tool) => pi.registerTool(toPiToolDefinition(
127
+ tool,
128
+ (context) => projectPiContext(context, options.transcriptFromContext),
129
+ )),
130
+ getAllTools: () => pi.getAllTools().map(({ name, sourceInfo }) => ({
131
+ name,
132
+ ...(sourceInfo?.path === undefined ? {} : { sourceInfo: { path: sourceInfo.path } }),
133
+ })),
134
+ setActiveTools: (names) => pi.setActiveTools(names),
135
+ getActiveTools: () => pi.getActiveTools(),
136
+ requireGatekeeperPass: requirePiGatekeeperPass,
137
+ on(...registration: HostEventRegistration) {
138
+ const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
139
+ if (registration[0] === "before_agent_start") {
140
+ const [, handler] = registration;
141
+ pi.on("before_agent_start", (value, ctx) => handler({ prompt: value.prompt, systemPrompt: value.systemPrompt, systemPromptOptions: value.systemPromptOptions }, context(ctx)));
142
+ } else if (registration[0] === "input") {
143
+ const [, handler] = registration;
144
+ pi.on("input", (value, ctx) => handler({ text: value.text, ...(value.images === undefined ? {} : { images: value.images }), source: value.source }, context(ctx)));
145
+ } else if (registration[0] === "tool_call") {
146
+ const [, handler] = registration;
147
+ pi.on("tool_call", (value, ctx) => handler({ toolName: value.toolName, toolCallId: value.toolCallId, input: value.input }, context(ctx)));
148
+ } else if (registration[0] === "tool_result") {
149
+ const [, handler] = registration;
150
+ pi.on("tool_result", (value, ctx) => handler({ toolName: value.toolName, toolCallId: value.toolCallId, isError: value.isError, content: (value.content ?? []).map((part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", data: part.data, mimeType: part.mimeType }), details: value.details }, context(ctx)));
151
+ } else if (registration[0] === "session_start") {
152
+ const [, handler] = registration;
153
+ pi.on("session_start", (value, ctx) => handler({ reason: value.reason }, context(ctx)));
154
+ } else if (registration[0] === "session_shutdown") {
155
+ const [, handler] = registration;
156
+ pi.on("session_shutdown", (_value, ctx) => handler({}, context(ctx)));
157
+ } else if (registration[0] === "after_provider_response") {
158
+ const [, handler] = registration;
159
+ pi.on("after_provider_response", (value, ctx) => handler({ status: value.status }, context(ctx)));
160
+ } else if (registration[0] === "agent_end") {
161
+ const [, handler] = registration;
162
+ pi.on("agent_end", (value, ctx) => handler({
163
+ messages: value.messages.map((message) => ({
164
+ role: message.role,
165
+ content: "content" in message && Array.isArray(message.content) ? message.content.map((part) => part.type === "text"
166
+ ? { type: "text", text: part.text }
167
+ : part.type === "toolCall"
168
+ ? { type: "toolCall", id: part.id, name: part.name, arguments: part.arguments }
169
+ : { type: part.type }) : [],
170
+ ...("toolName" in message && typeof message.toolName === "string" ? { toolName: message.toolName } : {}),
171
+ ...("isError" in message && typeof message.isError === "boolean" ? { isError: message.isError } : {}),
172
+ ...("stopReason" in message && typeof message.stopReason === "string" ? { stopReason: message.stopReason } : {}),
173
+ })),
174
+ }, context(ctx)));
175
+ } else if (registration[0] === "agent_settled") {
176
+ const [, handler] = registration;
177
+ pi.on("agent_settled", (_value, ctx) => handler({}, context(ctx)));
178
+ } else if (registration[0] === "tool_execution_start") {
179
+ const [, handler] = registration;
180
+ pi.on("tool_execution_start", (value, ctx) => handler({ toolName: value.toolName, toolCallId: value.toolCallId }, context(ctx)));
181
+ } else if (registration[0] === "tool_execution_update") {
182
+ const [, handler] = registration;
183
+ pi.on("tool_execution_update", (value, ctx) => handler({ toolName: value.toolName, toolCallId: value.toolCallId, partialResult: value.partialResult }, context(ctx)));
184
+ } else {
185
+ const [, handler] = registration;
186
+ pi.on("tool_execution_end", (value, ctx) => handler({ toolName: value.toolName, toolCallId: value.toolCallId, isError: value.isError }, context(ctx)));
187
+ }
188
+ },
189
+ ...(typeof pi.getCommands === "function" ? { getCommands: () => pi.getCommands().map(({ name }) => ({ name })) } : {}),
190
+ };
191
+ return { host };
192
+ }
@@ -0,0 +1,98 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import type {
5
+ DurablePrincipal,
6
+ DurablePrincipalAuthority,
7
+ DurablePrincipalCoordinates,
8
+ NewDurablePrincipalRequest,
9
+ } from "../host-contracts.ts";
10
+ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
11
+ import {
12
+ activationBookDirectory,
13
+ resolveActivationLedgerHome,
14
+ } from "../activation-ledger-topology.ts";
15
+
16
+ type PiDurablePrincipal = DurablePrincipal & {
17
+ readonly sessionDirectory: string;
18
+ readonly sessionFile: string;
19
+ };
20
+
21
+ function encode(coordinates: DurablePrincipalCoordinates): PiDurablePrincipal {
22
+ return coordinates as PiDurablePrincipal;
23
+ }
24
+
25
+ export function issuePiDurablePrincipalCoordinates(
26
+ request: NewDurablePrincipalRequest,
27
+ ): DurablePrincipalCoordinates & {
28
+ readonly ledgerHome: string;
29
+ readonly bookKey: string;
30
+ readonly runDirectory: string;
31
+ } {
32
+ const ledgerHome = resolveActivationLedgerHome(
33
+ request.home === undefined ? undefined : () => request.home!,
34
+ );
35
+ const bookKey = resolveBookKeyFromGit(request.cwd);
36
+ const runDirectory = join(
37
+ activationBookDirectory(ledgerHome, bookKey),
38
+ "runs",
39
+ `${request.runId}@${request.role}`,
40
+ );
41
+ const sessionDirectory = join(runDirectory, "session");
42
+ return {
43
+ ledgerHome,
44
+ bookKey,
45
+ runDirectory,
46
+ sessionDirectory,
47
+ sessionFile: join(sessionDirectory, "session.jsonl"),
48
+ };
49
+ }
50
+
51
+ /** Pi's durable-principal codec and availability authority. */
52
+ export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
53
+ issue(request: NewDurablePrincipalRequest): DurablePrincipal {
54
+ const coordinates = issuePiDurablePrincipalCoordinates(request);
55
+ return encode({
56
+ sessionDirectory: coordinates.sessionDirectory,
57
+ sessionFile: coordinates.sessionFile,
58
+ });
59
+ },
60
+ decode(value: unknown): DurablePrincipalCoordinates {
61
+ const record = value as Partial<DurablePrincipalCoordinates> | null;
62
+ if (record === null || typeof record !== "object") {
63
+ throw new Error("durable principal payload is missing");
64
+ }
65
+ if (typeof record.sessionDirectory !== "string" || record.sessionDirectory.trim() === "") {
66
+ throw new Error("durable principal session directory is missing");
67
+ }
68
+ return {
69
+ sessionDirectory: record.sessionDirectory,
70
+ sessionFile:
71
+ typeof record.sessionFile === "string" && record.sessionFile.trim() !== ""
72
+ ? record.sessionFile
73
+ : join(record.sessionDirectory, "session.jsonl"),
74
+ };
75
+ },
76
+ async isAvailable(principal: DurablePrincipal): Promise<boolean> {
77
+ const { sessionFile } = this.decode(principal);
78
+ try {
79
+ const stat = await lstat(sessionFile);
80
+ return stat.isFile() && !stat.isSymbolicLink();
81
+ } catch {
82
+ return false;
83
+ }
84
+ },
85
+ };
86
+
87
+ /**
88
+ * Transitional coordinate accessor.
89
+ * S1b-2 (#526) removed 13 per-role builder accessors and auto-resume decode
90
+ * consumption. Remaining consumers: admission persistence projection and
91
+ * settlement/inspect (S4 #519 retires this seam).
92
+ */
93
+ export function decodePiDurablePrincipal(
94
+ authority: DurablePrincipalAuthority,
95
+ principal: DurablePrincipal,
96
+ ): DurablePrincipalCoordinates {
97
+ return authority.decode(principal);
98
+ }