@akagilnc/pi-workflow-roles 0.1.3648 → 0.1.3652

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 (66) hide show
  1. package/dist/acp-host/production-host.js +2919 -1750
  2. package/dist/activation-ledger-session.js +105 -0
  3. package/dist/activation-ledger.js +139 -0
  4. package/dist/activation-trace.js +33 -0
  5. package/dist/agent-start-materials.js +11 -0
  6. package/dist/auditor-role.js +12 -0
  7. package/dist/canonical-skill-binding.js +64 -0
  8. package/dist/collector-identity.js +151 -0
  9. package/dist/collector-receipt.js +95 -0
  10. package/dist/collector-role.js +441 -0
  11. package/dist/countersign-role.js +28 -0
  12. package/dist/diarist-role.js +29 -0
  13. package/dist/doctor-role.js +74 -0
  14. package/dist/fixer-bash-seatbelt.js +13 -0
  15. package/dist/gatekeeper-pass-envelope.js +72 -0
  16. package/dist/gleaner-left-role.js +26 -0
  17. package/dist/inspector-role.js +29 -0
  18. package/dist/judge-role.js +106 -0
  19. package/dist/merger-role.js +66 -0
  20. package/dist/navigator-role.js +12 -0
  21. package/dist/notary-role.js +137 -0
  22. package/dist/oauth-keepalive.js +158 -0
  23. package/dist/package-contracts/terminating-tools.js +2 -16
  24. package/dist/packaged-role-registry.js +1 -18
  25. package/dist/pi/adapter.js +211 -0
  26. package/dist/pi/role-turn-host.js +0 -2
  27. package/dist/public-cli/instruction-seat-run.js +2 -6
  28. package/dist/public-cli/invocation.js +0 -7
  29. package/dist/public-cli/main.js +94 -191
  30. package/dist/public-cli/option-definitions.js +0 -14
  31. package/dist/public-cli/registry.js +1 -2
  32. package/dist/public-cli/run-lifecycle.js +1 -3
  33. package/dist/public-cli/settlement.js +0 -10
  34. package/dist/public-role-summons.js +0 -14
  35. package/dist/reviewer-child-executor.js +163 -70
  36. package/dist/reviewer-construction.js +2 -2
  37. package/dist/reviewer-role.js +159 -0
  38. package/dist/reviewer-settlement.js +66 -0
  39. package/dist/role-runtime.js +1394 -0
  40. package/dist/stderr-jsonl.js +26 -0
  41. package/dist/tool-execution-observation.js +141 -0
  42. package/dist/worker-role.js +343 -0
  43. package/dist/worker-submission-gates.js +305 -0
  44. package/extensions/role-runtime.ts +1 -2
  45. package/package.json +1 -1
  46. package/src/acp-host/production-host.ts +0 -1
  47. package/src/host-contracts.ts +0 -2
  48. package/src/package-contracts/terminating-tools.ts +1 -19
  49. package/src/packaged-role-registry.ts +1 -19
  50. package/src/pi/role-turn-host.ts +0 -2
  51. package/src/public-cli/cli.ts +1 -5
  52. package/src/public-cli/instruction-seat-run.ts +5 -14
  53. package/src/public-cli/invocation.ts +3 -21
  54. package/src/public-cli/option-definitions.ts +0 -16
  55. package/src/public-cli/registry.ts +1 -2
  56. package/src/public-cli/run-lifecycle.ts +0 -6
  57. package/src/public-cli/settlement.ts +0 -32
  58. package/src/public-cli/terminal.ts +0 -1
  59. package/src/public-role-summons.ts +0 -15
  60. package/src/reviewer-child-executor.ts +199 -98
  61. package/src/reviewer-construction.ts +3 -3
  62. package/src/role-runtime.ts +1 -33
  63. package/dist/package-contracts/evidence-child-output.js +0 -31
  64. package/souls/evidence-child.md +0 -1
  65. package/src/evidence-child-role.ts +0 -22
  66. package/src/package-contracts/evidence-child-output.ts +0 -46
@@ -0,0 +1,1394 @@
1
+ import { writeSync } from "node:fs";
2
+ import { ExplicitInternalActivationError, } from "./host-contracts.js";
3
+ import { recordReviewerDispatchRejectionSync } from "./public-cli/reviewer-dispatch-rejection.js";
4
+ import { Value } from "typebox/value";
5
+ import { sitianReport } from "./sitian-facade.js";
6
+ import { createSubmissionLedgerHost } from "./submission-ledger.js";
7
+ import { createCollectorLedger } from "./collector-ledger.js";
8
+ import { activationTraceRecordSchema, namedActivationCause } from "./activation-trace.js";
9
+ import { resolveActivationLedgerHomeForPath } from "./activation-ledger-topology.js";
10
+ import { appendAcceptedActivationToBook, buildAcceptedActivationFact, correlationIdentityFromEnv, durableSessionPointer, resolveActivationLedgerHome, resolveBookKeyFromGit, } from "./activation-ledger.js";
11
+ import { writeStderrJsonlRecord } from "./stderr-jsonl.js";
12
+ import { createToolExecutionObservationFace, systemToolExecutionObservationMonoNow, writeToolExecutionObservationRecord, } from "./tool-execution-observation.js";
13
+ import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.js";
14
+ import { registerEngineDetourTool } from "./engine-detour-tool.js";
15
+ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
16
+ import { COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, createCollectorRoleRuntime, } from "./collector-role.js";
17
+ import { createDoctorRoleRuntime } from "./doctor-role.js";
18
+ import { createNotaryRoleRuntime, NOTARY_SESSION_BOUND_ENTRY, projectNotaryBoundFromFlags, readNotaryTicketFlag, } from "./notary-role.js";
19
+ import { NOTARY_TICKET_FLAG } from "./notary-contracts.js";
20
+ import { COUNTERSIGN_TOOL_SPEC, } from "./countersign-role.js";
21
+ import { COUNTERSIGN_ACCEPTED_TEXT } from "./countersign-contracts.js";
22
+ import { GLEANER_LEFT_TOOL_SPEC, } from "./gleaner-left-role.js";
23
+ import { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_BASE_FLAG } from "./gleaner-left-contracts.js";
24
+ import { INSPECTOR_TOOL_SPEC, } from "./inspector-role.js";
25
+ import { INSPECTOR_ACCEPTED_TEXT } from "./inspector-contracts.js";
26
+ import { DIARIST_TOOL_SPEC, } from "./diarist-role.js";
27
+ import { DIARIST_ACCEPTED_TEXT, DIARIST_SOURCES_FLAG, projectDiaristSelections, } from "./diarist-contracts.js";
28
+ import { commitDiaristSelections, loadDiaristSourceCatalog, } from "./diarist.js";
29
+ import { GATEKEEPER_TOOL_SPEC, } from "./gatekeeper-role.js";
30
+ import { NAVIGATOR_TOOL_SPEC, } from "./navigator-role.js";
31
+ import { AUDITOR_TOOL_SPEC, } from "./auditor-role.js";
32
+ import { AUDITOR_ACCEPTED_TEXT } from "./package-contracts/auditor-output.js";
33
+ import { GATEKEEPER_ACCEPTED_TEXT } from "./package-contracts/gatekeeper-output.js";
34
+ import { NAVIGATOR_ACCEPTED_TEXT } from "./package-contracts/navigator-output.js";
35
+ import { decorateSettlementWithNavigation, formatNavigatorReport, NAVIGATOR_EVENT_TYPE, navigatorSubjectKey, navigatorUnavailableError, subjectPath } from "./navigator-attendance.js";
36
+ import { buildNavigatorInfrastructureFailureFact, classifyPackagedRoleTerminalResult, extractInfrastructureFailureEvidence, NAVIGATOR_INVOCATION_ENTRY, resolveLifecycleInvocationPrincipal, } from "./navigator-invocation-identity.js";
37
+ import { recordTypedProviderHttpStatus } from "./typed-provider-http.js";
38
+ import { NAVIGATOR_POST_ROLE_GRACE_MS, raceNavigatorGrace } from "./public-cli/settlement.js";
39
+ import { PACKAGED_ROLE_REGISTRY, packagedRoleMetadata, packagedRoleOutputTool, packagedRolePhaseFlag } from "./packaged-role-registry.js";
40
+ import { isAuditEscalationProjection } from "./audit-escalation.js";
41
+ import { createJudgeRoleRuntime, } from "./judge-role.js";
42
+ import { createReviewerRoleRuntime, } from "./reviewer-role.js";
43
+ import {} from "./reviewer-construction.js";
44
+ /**
45
+ * Private transport flag names/definitions for Reviewer admitted inputs.
46
+ * Shared activation envelope owns registration and decoding (ADR 0018).
47
+ */
48
+ const REVIEWER_TRANSPORT_FLAGS = Object.freeze([
49
+ Object.freeze({
50
+ name: "ak-review-base",
51
+ definition: Object.freeze({
52
+ description: "Fixed base revision for the pinned review target",
53
+ type: "string",
54
+ }),
55
+ }),
56
+ Object.freeze({
57
+ name: "ak-review-scope-keys",
58
+ definition: Object.freeze({
59
+ description: "Optional comma-separated exact class keys limiting Reviewer scope",
60
+ type: "string",
61
+ }),
62
+ }),
63
+ Object.freeze({
64
+ name: "ak-review-authority-refs",
65
+ definition: Object.freeze({
66
+ description: "JSON array of durable authority references for Spec-axis material only",
67
+ type: "string",
68
+ }),
69
+ }),
70
+ Object.freeze({
71
+ name: "ak-review-ticket-number",
72
+ definition: Object.freeze({
73
+ description: "Typed ticketNumber for Spec self-fetch primary path",
74
+ type: "string",
75
+ }),
76
+ }),
77
+ ]);
78
+ /** Notary private transport: optional court-diary ticket (ADR 0018 / 0075 — envelope-owned). */
79
+ const NOTARY_TRANSPORT_FLAGS = Object.freeze([
80
+ Object.freeze({
81
+ name: NOTARY_TICKET_FLAG.name,
82
+ definition: NOTARY_TICKET_FLAG.definition,
83
+ }),
84
+ ]);
85
+ /** Gleaner-left private transport: comparison-base revision (ADR 0018 / #502). */
86
+ const GLEANER_LEFT_TRANSPORT_FLAGS = Object.freeze([
87
+ Object.freeze({
88
+ name: GLEANER_LEFT_BASE_FLAG.name,
89
+ definition: GLEANER_LEFT_BASE_FLAG.definition,
90
+ }),
91
+ ]);
92
+ /** Diarist private transport: frozen source catalog path (ADR 0018 / #708). */
93
+ const DIARIST_TRANSPORT_FLAGS = Object.freeze([
94
+ Object.freeze({
95
+ name: DIARIST_SOURCES_FLAG.name,
96
+ definition: DIARIST_SOURCES_FLAG.definition,
97
+ }),
98
+ ]);
99
+ /**
100
+ * Decode private transport flags into frozen admitted inputs.
101
+ * Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar).
102
+ */
103
+ function decodeReviewerAdmittedInputs(getFlag) {
104
+ let reviewScopeKeys;
105
+ const rawScopeKeys = getFlag("ak-review-scope-keys");
106
+ if (rawScopeKeys !== undefined) {
107
+ if (typeof rawScopeKeys !== "string" || rawScopeKeys.length === 0) {
108
+ throw new Error("Reviewer scope keys must be a nonempty comma-separated string");
109
+ }
110
+ const parsed = rawScopeKeys.split(",");
111
+ if (parsed.some((key) => key.trim().length === 0) || new Set(parsed).size !== parsed.length) {
112
+ throw new Error("Reviewer scope keys contain a blank or exact duplicate key");
113
+ }
114
+ reviewScopeKeys = Object.freeze(parsed);
115
+ }
116
+ let authorityRefs;
117
+ const rawAuthorityRefs = getFlag("ak-review-authority-refs");
118
+ if (rawAuthorityRefs !== undefined) {
119
+ if (typeof rawAuthorityRefs !== "string") {
120
+ throw new Error("Reviewer authority refs transport error: flag value must be a string");
121
+ }
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(rawAuthorityRefs);
125
+ }
126
+ catch (error) {
127
+ throw new Error(`Reviewer authority refs transport error: JSON decode failed: ${error instanceof Error ? error.message : String(error)}`);
128
+ }
129
+ if (!Array.isArray(parsed) || parsed.some((ref) => typeof ref !== "string")) {
130
+ throw new Error("Reviewer authority refs transport error: expected a JSON array of strings");
131
+ }
132
+ authorityRefs = Object.freeze(parsed);
133
+ }
134
+ let ticketNumber;
135
+ const rawTicketNumber = getFlag("ak-review-ticket-number");
136
+ // Shape-invalid flag values do not abort: omit typed candidate; branch→commit→degrade continues.
137
+ if (typeof rawTicketNumber === "string" && /^[1-9]\d*$/.test(rawTicketNumber)) {
138
+ ticketNumber = Number(rawTicketNumber);
139
+ }
140
+ const baseRevision = getFlag("ak-review-base");
141
+ if (typeof baseRevision !== "string" || !baseRevision.trim()) {
142
+ throw new Error("Reviewer role requires --ak-review-base");
143
+ }
144
+ return Object.freeze({
145
+ baseRevision,
146
+ ...(reviewScopeKeys === undefined ? {} : { reviewScopeKeys }),
147
+ ...(authorityRefs === undefined ? {} : { authorityRefs }),
148
+ ...(ticketNumber === undefined ? {} : { ticketNumber }),
149
+ });
150
+ }
151
+ /**
152
+ * Parent system-prompt assembly for the shared activation envelope.
153
+ * Verification cadence lives in soul/quality-law only (ADR 0073: no machine copy).
154
+ */
155
+ function assembleReviewerParentSystemPrompt(input) {
156
+ const specDispositionNote = input.specDisposition === "skipped-missing"
157
+ ? [
158
+ "",
159
+ "<reviewer_spec_disposition>",
160
+ "权威 Spec 不存在;未启动 Spec 取证腿。",
161
+ "</reviewer_spec_disposition>",
162
+ ]
163
+ : [];
164
+ return [
165
+ input.baseSystemPrompt,
166
+ "",
167
+ "<reviewer_soul>",
168
+ input.soul,
169
+ "</reviewer_soul>",
170
+ ...specDispositionNote,
171
+ ].join("\n");
172
+ }
173
+ import { CODER_OUTPUT_TOOL_NAME, createCoderRoleRuntime, createFixerRoleRuntime, FIXER_FLAG_DEFINITIONS, FIXER_OUTPUT_TOOL_NAME, FIXER_PHASES, } from "./worker-role.js";
174
+ import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.js";
175
+ import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.js";
176
+ import { DOCTOR_OUTPUT_TOOL_NAME } from "./doctor-contracts.js";
177
+ import { MERGER_OUTPUT_TOOL_NAME } from "./merger-contracts.js";
178
+ import { createMergerRoleRuntime } from "./merger-role.js";
179
+ export { buildNavigatorInfrastructureFailureFact, classifyPackagedRoleTerminalResult, extractInfrastructureFailureEvidence, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isDurablePackagedRoleTerminalResult, isNavigatorInfrastructureFailureFact, NAVIGATOR_INFRASTRUCTURE_FAILURE_EVIDENCE_KEYS, NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND, } from "./navigator-invocation-identity.js";
180
+ export { activationTraceRecordSchema, namedActivationCause } from "./activation-trace.js";
181
+ export { ACCEPTED_ACTIVATION_EVENT, ACCEPTED_ACTIVATION_FACT_KEYS, ActivationGitRepositoryRequiredError, ActivationLedgerError, ActivationSessionFileMissingError, activationBookDirectory, activationWaitingLedgerPath, appendAcceptedActivationFact, appendAcceptedActivationToBook, buildAcceptedActivationFact, correlationIdentityFromEnv, durableSessionPointer, resolveActivationLedgerHome, resolveBookKeyFromGit, serializeAcceptedActivationFact, } from "./activation-ledger.js";
182
+ export { DISPATCH_STUB_EVENT, buildDispatchStubFact, reconcileInvocation, } from "./activation-reconciliation.js";
183
+ export { TOOL_EXECUTION_UPDATE_HEARTBEAT, TOOL_EXECUTION_UPDATE_THROTTLE_MS, createToolExecutionObservationFace, isProducingToolUpdate, systemToolExecutionObservationMonoNow, toolExecutionObservationRecordSchema, validateToolExecutionObservationRecord, writeToolExecutionObservationRecord, } from "./tool-execution-observation.js";
184
+ export { NOTARY_OUTPUT_TOOL, INSPECTOR_OUTPUT_TOOL, GatekeeperDecisionError, createGatekeeperOutputTool, runGatekeeper, } from "./gatekeeper-role.js";
185
+ export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME, } from "./doctor-role.js";
186
+ export { validateDoctorSubmissionShape, validateDoctorOutput, DoctorEvidenceStore } from "./doctor-contracts.js";
187
+ export { loadDoctorCase } from "./doctor-evidence.js";
188
+ export { JUDGE_OUTPUT_TOOL_NAME, } from "./judge-role.js";
189
+ export { ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV } from "./engine-detour.js";
190
+ export { AGENT_TOOL_NAME, REVIEWER_OUTPUT_TOOL_NAME, } from "./reviewer-role.js";
191
+ export { CODER_OUTPUT_TOOL_NAME, CODER_SKILL_EXPANSION_EVIDENCE_MISSING_CODE, CoderSkillExpansionEvidenceMissingError, FIXER_FLAG_DEFINITIONS, FIXER_OUTPUT_TOOL_NAME, FIXER_PHASES, } from "./worker-role.js";
192
+ export { fixerOutputSchema, validateFixerOutput, validateFixerOutputForPacket } from "./package-contracts/fixer-output.js";
193
+ export { fixerPrerequisiteSchema, fixerPrerequisitesSchema, parseFixerPrerequisites, validateFixerPrerequisites } from "./package-contracts/fixer-packet.js";
194
+ export { AUDIT_ESCALATION_KIND, buildAuditEscalationResult, disposeComplianceDecision, isAuditEscalationResult, projectAuditEscalation } from "./audit-escalation.js";
195
+ export { AUDITOR_SOUL_ROLES, AK_ROLE_AUDITOR_SUBJECT_ENV, loadAuditorSoul, loadAuditorSoulFromSubjectInput, resolveAuditorSubject, } from "./auditor-soul.js";
196
+ export { JUDGE_AUDIT_TOOL_NAME, SOUL_AUDIT_TOOL_NAME, createPiJudgeAuditor } from "./judge-auditor.js";
197
+ export { DOCTOR_AUDIT_TOOL_NAME, createPiDoctorAuditor } from "./doctor-auditor.js";
198
+ export { COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, } from "./collector-role.js";
199
+ export * from "./navigator-attendance.js";
200
+ export { MERGER_INPUT_FLAG, createMergerRoleRuntime } from "./merger-role.js";
201
+ export { MERGER_OUTPUT_TOOL_NAME, mergerInputSchema, mergerOutputSchema, validateMergerInput, validateMergerOutput } from "./merger-contracts.js";
202
+ export { createProductionMergerGitState } from "./merger-git-state.js";
203
+ function activationStage(role, runtime) {
204
+ switch (role) {
205
+ case "judge": return { id: "load-and-install", run: async () => runtime.judge.activate() };
206
+ case "fixer": return { id: "load-and-install", run: async () => runtime.fixer.activate() };
207
+ case "coder": return { id: "load-and-install", run: async () => runtime.coder.activate(runtime.context) };
208
+ case "reviewer": return { id: "load-install-and-dispatch", run: async () => {
209
+ // Envelope owns flag decode inside the activation stage (trace + fail-closed path).
210
+ const admitted = runtime.decodeReviewerAdmitted();
211
+ const activation = await runtime.reviewer.activate(runtime.context, admitted);
212
+ runtime.bindReviewerParent(activation);
213
+ const result = await activation.dispatcher.dispatch(activation.fixedBaseRevision, { context: runtime.context });
214
+ if (result.status !== "accepted") {
215
+ const rejection = new ExplicitInternalActivationError(`Fixed Reviewer dispatch was not accepted: ${result.status}: ${result.diagnostic}`, {
216
+ knownCause: "activation",
217
+ name: "ReviewerDispatchRejectionError",
218
+ });
219
+ // Pi stderr cannot carry the structured violations. Do not mask a durable
220
+ // write failure: its infrastructure cause is truer than the unwritten page.
221
+ const runDir = process.env.AK_ROLE_RUN_DIR;
222
+ if (typeof runDir === "string" && runDir.trim() !== "") {
223
+ recordReviewerDispatchRejectionSync(runDir, {
224
+ diagnostic: rejection.message,
225
+ violations: result.violations,
226
+ });
227
+ }
228
+ throw rejection;
229
+ }
230
+ } };
231
+ case "collector": return { id: "load-and-install", run: async () => runtime.collector.activate(runtime.context, runtime.event) };
232
+ case "doctor": return { id: "load-and-install", run: async () => runtime.doctor.activate() };
233
+ case "notary": return {
234
+ id: "load-and-install",
235
+ run: async () => {
236
+ // Envelope owns ticket flag read (ADR 0018); role receives admitted value only.
237
+ await runtime.notary.activate(runtime.decodeNotaryAdmitted());
238
+ },
239
+ };
240
+ case "countersign": return { id: "load-and-install", run: async () => runtime.countersign.activate() };
241
+ case "gleaner-left": return { id: "load-and-install", run: async () => runtime.gleanerLeft.activate() };
242
+ case "inspector": return { id: "load-and-install", run: async () => runtime.inspector.activate() };
243
+ case "gatekeeper": return { id: "load-and-install", run: async () => runtime.gatekeeper.activate() };
244
+ case "navigator": return { id: "load-and-install", run: async () => runtime.navigator.activate() };
245
+ case "auditor": return { id: "load-and-install", run: async () => runtime.auditor.activate() };
246
+ case "diarist": return { id: "load-and-install", run: async () => runtime.diarist.activate() };
247
+ case "merger": return { id: "prepare-git-and-install", run: async () => runtime.merger() };
248
+ }
249
+ }
250
+ function validateActivationTraceRecord(record) {
251
+ if (!Value.Check(activationTraceRecordSchema, record)) {
252
+ throw new TypeError("Activation trace record does not match its closed contract");
253
+ }
254
+ return record;
255
+ }
256
+ async function emitActivationTrace(writeTrace, record) {
257
+ await writeTrace(validateActivationTraceRecord(record));
258
+ }
259
+ async function executeActivationStage(role, stage, infrastructure) {
260
+ try {
261
+ await stage.run();
262
+ }
263
+ catch (activationError) {
264
+ try {
265
+ await emitActivationTrace(infrastructure.writeTrace, {
266
+ role,
267
+ stageId: stage.id,
268
+ status: "failed",
269
+ timestamp: infrastructure.clock(),
270
+ cause: namedActivationCause(activationError),
271
+ });
272
+ }
273
+ catch (traceError) {
274
+ throw new AggregateError([activationError, traceError], `Activation stage ${stage.id} failed and its failure trace could not be emitted`);
275
+ }
276
+ throw activationError;
277
+ }
278
+ }
279
+ export function writeActivationTraceRecord(record, write = writeSync) {
280
+ writeStderrJsonlRecord(record, write);
281
+ }
282
+ export class ActivationBarrierError extends Error {
283
+ code = "AK_ACTIVATION_NOT_ADMITTED";
284
+ constructor(role) {
285
+ super(`Workflow role ${String(role)} activation did not complete`);
286
+ this.name = "ActivationBarrierError";
287
+ }
288
+ }
289
+ export const WORKFLOW_ROLES = PACKAGED_ROLE_REGISTRY.map(({ role }) => role);
290
+ export const ROLE_FLAG = {
291
+ name: "ak-role",
292
+ definition: {
293
+ description: `Activate a packaged workflow role: ${WORKFLOW_ROLES.slice(0, -1).join(", ")}, or ${WORKFLOW_ROLES.at(-1)}`,
294
+ type: "string",
295
+ },
296
+ };
297
+ /** Host-neutral in-process role help for Navigator prepare (Pi and Grok share this). */
298
+ export function formatNavigatorRoleHelp(role) {
299
+ const metadata = packagedRoleMetadata(role);
300
+ const lines = [
301
+ `Usage: ak-role ${role}`,
302
+ ROLE_FLAG.definition.description,
303
+ ];
304
+ if (metadata?.inputFlag !== undefined) {
305
+ lines.push(` --${metadata.inputFlag} <value> ${role} input material`);
306
+ }
307
+ if (metadata?.phaseFlag !== undefined) {
308
+ lines.push(` --${metadata.phaseFlag} <value> ${role} phase: ${metadata.phases.filter((p) => p !== null).join(" | ")}`);
309
+ }
310
+ lines.push(`Public next-command form: ak-role ${role}`);
311
+ return lines.join("\n");
312
+ }
313
+ function abortContext(ctx) {
314
+ ctx.abort();
315
+ }
316
+ function failInfrastructure(error, ctx) {
317
+ abortContext(ctx);
318
+ if (ctx.mode === "print" || ctx.mode === "json")
319
+ process.exitCode = 1;
320
+ throw error;
321
+ }
322
+ function buildPendingInfrastructureFailure(error) {
323
+ return {
324
+ details: {
325
+ ...buildNavigatorInfrastructureFailureFact(),
326
+ ...extractInfrastructureFailureEvidence(error),
327
+ },
328
+ };
329
+ }
330
+ function navigatorPhase(roleHost, role) {
331
+ const metadata = packagedRoleMetadata(role);
332
+ if (metadata === undefined || metadata.phases[0] === null)
333
+ return null;
334
+ const phaseFlag = packagedRolePhaseFlag(role);
335
+ const requested = phaseFlag === undefined ? undefined : roleHost.getFlag(phaseFlag);
336
+ return requested === "apply" ? "apply" : "plan";
337
+ }
338
+ function navigatorOutputTool(role) {
339
+ return packagedRoleOutputTool(role);
340
+ }
341
+ export function publicNavigatorSettlement(role, phase, event) {
342
+ // One shared classifier owns terminal discriminant (lifecycle + settlement + extractors).
343
+ if (event.toolName !== navigatorOutputTool(role))
344
+ return undefined;
345
+ const classification = classifyPackagedRoleTerminalResult(event);
346
+ if (classification.kind === "nonterminal")
347
+ return undefined;
348
+ if (classification.kind === "infrastructure") {
349
+ return { kind: "role_infrastructure_failure", role, phase };
350
+ }
351
+ // accepted/human — project role/phase status; classifier already rejected infra/contradiction.
352
+ const details = typeof event.details === "object" && event.details !== null && !Array.isArray(event.details)
353
+ ? event.details
354
+ : {};
355
+ // Live Navigator consumes only the audit-owned projection; persisted/replayed
356
+ // records are re-authenticated by settlement against retained audit evidence.
357
+ if (isAuditEscalationProjection(event.details)) {
358
+ return { kind: "human_decision", role, phase, status: "audit_escalation" };
359
+ }
360
+ const status = typeof details.status === "string"
361
+ ? details.status
362
+ : typeof details.judgeStatus === "string" ? details.judgeStatus
363
+ : typeof details.countersignStatus === "string" ? details.countersignStatus : undefined;
364
+ if (status !== undefined && status === "escalate") {
365
+ return { kind: "human_decision", role, phase, status };
366
+ }
367
+ return { kind: "accepted", role, phase, ...(status === undefined ? {} : { status }) };
368
+ }
369
+ export async function projectClosedSubmissionLifecycle(projection, context, phase, recordAccepted, settle) {
370
+ recordAccepted();
371
+ const closure = {
372
+ toolName: navigatorOutputTool(projection.role),
373
+ isError: false,
374
+ details: projection.decisiveFacts,
375
+ };
376
+ context.sessionManager.appendCustomEntry?.("ak-role-submission-closure", closure);
377
+ await settle(publicNavigatorSettlement(projection.role, phase, closure));
378
+ }
379
+ /**
380
+ * Shared registration envelope for filed officers (ADR 0018 / #572):
381
+ * activate, tool register, before_agent_start prompt, inventory check.
382
+ * Role module keeps label/soul/spec shape only; sole-final barrier is ledger-owned.
383
+ * Optional beforeAccept is the sole extension seam (e.g. countersign Notary gate).
384
+ */
385
+ function createFiledOfficerRuntime(roleHost, spec, dependencies) {
386
+ let soul;
387
+ let registered = false;
388
+ return {
389
+ async activate() {
390
+ const loaded = (await dependencies.loadSoul()).trim();
391
+ if (loaded.length === 0)
392
+ throw new Error(`${spec.role} soul is empty`);
393
+ soul = loaded;
394
+ if (!registered) {
395
+ registered = true;
396
+ roleHost.registerTool({
397
+ name: spec.tool.name,
398
+ label: spec.tool.label,
399
+ description: spec.tool.description,
400
+ promptSnippet: spec.tool.promptSnippet,
401
+ parameters: spec.tool.parameters,
402
+ async execute(toolCallId, parameters, signal, _onUpdate, ctx) {
403
+ if (soul === undefined)
404
+ throw new Error(`${spec.role} 职分未装载`);
405
+ const projected = spec.beforeAccept === undefined
406
+ ? undefined
407
+ : await spec.beforeAccept({ toolCallId, parameters, signal, ctx });
408
+ // Accept-as-is + terminate only. Shape is not an admission gate
409
+ // (第 0 条 / ADR 0055); sole-final barrier is ledger-owned (#575).
410
+ return {
411
+ content: [{ type: "text", text: spec.acceptedText }],
412
+ details: projected === undefined ? parameters : projected,
413
+ terminate: true,
414
+ };
415
+ },
416
+ });
417
+ roleHost.on("before_agent_start", (event) => {
418
+ if (soul === undefined)
419
+ throw new Error(`${spec.role} 职分未装载`);
420
+ const tail = `\n\n<${spec.soulTag}_soul>\n${soul}\n</${spec.soulTag}_soul>`;
421
+ return { systemPrompt: `${event.systemPrompt}${tail}` };
422
+ });
423
+ }
424
+ const all = roleHost.getAllTools().map((tool) => tool.name);
425
+ if (all.filter((name) => name === spec.tool.name).length !== 1) {
426
+ throw new Error(`${spec.role} required tool collision or missing: ${spec.tool.name}`);
427
+ }
428
+ },
429
+ };
430
+ }
431
+ export function createGleanerLeftRoleRuntime(roleHost, dependencies) {
432
+ return createFiledOfficerRuntime(roleHost, {
433
+ role: "gleaner-left",
434
+ tool: GLEANER_LEFT_TOOL_SPEC,
435
+ acceptedText: GLEANER_LEFT_ACCEPTED_TEXT,
436
+ soulTag: "gleaner-left",
437
+ }, dependencies);
438
+ }
439
+ function decodeGleanerLeftBase(getFlag) {
440
+ const baseRevision = getFlag(GLEANER_LEFT_BASE_FLAG.name);
441
+ if (typeof baseRevision !== "string" || !baseRevision.trim()) {
442
+ throw new Error("Gleaner-left role requires --ak-gleaner-left-base");
443
+ }
444
+ return baseRevision;
445
+ }
446
+ export function createInspectorRoleRuntime(roleHost, dependencies) {
447
+ return createFiledOfficerRuntime(roleHost, {
448
+ role: "inspector",
449
+ tool: INSPECTOR_TOOL_SPEC,
450
+ acceptedText: INSPECTOR_ACCEPTED_TEXT,
451
+ soulTag: "inspector",
452
+ }, dependencies);
453
+ }
454
+ /** #639: direct Gatekeeper public seat on the shared filed-officer envelope. */
455
+ export function createGatekeeperRoleRuntime(roleHost, dependencies) {
456
+ return createFiledOfficerRuntime(roleHost, {
457
+ role: "gatekeeper",
458
+ tool: GATEKEEPER_TOOL_SPEC,
459
+ acceptedText: GATEKEEPER_ACCEPTED_TEXT,
460
+ soulTag: "gatekeeper",
461
+ }, dependencies);
462
+ }
463
+ /** #639: direct Navigator public seat on the shared filed-officer envelope. */
464
+ export function createNavigatorRoleRuntime(roleHost, dependencies) {
465
+ return createFiledOfficerRuntime(roleHost, {
466
+ role: "navigator",
467
+ tool: NAVIGATOR_TOOL_SPEC,
468
+ acceptedText: NAVIGATOR_ACCEPTED_TEXT,
469
+ soulTag: "navigator",
470
+ }, dependencies);
471
+ }
472
+ /** #675: public 审刑院 seat on the shared filed-officer envelope. */
473
+ export function createAuditorRoleRuntime(roleHost, dependencies) {
474
+ const base = createFiledOfficerRuntime(roleHost, {
475
+ role: "auditor",
476
+ tool: AUDITOR_TOOL_SPEC,
477
+ acceptedText: AUDITOR_ACCEPTED_TEXT,
478
+ soulTag: "auditor",
479
+ }, dependencies);
480
+ return {
481
+ async activate() {
482
+ await base.activate();
483
+ // Same tools whether nested or direct (#675): dossier tool always registered.
484
+ // Source run: only the shared --source-run input face (never own-run fallback).
485
+ const { createAuditorDossierTool, AUDITOR_DOSSIER_TOOL_NAME } = await import("./auditor-dossier-tool.js");
486
+ const { AK_ROLE_AUDITOR_SOURCE_RUN_ENV } = await import("./auditor-soul.js");
487
+ const already = roleHost.getAllTools().some((tool) => tool.name === AUDITOR_DOSSIER_TOOL_NAME);
488
+ if (already)
489
+ return;
490
+ const sourceRun = typeof process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV] === "string"
491
+ && process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim() !== ""
492
+ ? process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim()
493
+ : undefined;
494
+ roleHost.registerTool(createAuditorDossierTool(sourceRun));
495
+ },
496
+ };
497
+ }
498
+ /**
499
+ * #708: 起居郎 public seat on the shared filed-officer envelope.
500
+ * Semantic collection happened in this role's own turn; the accept hook runs the
501
+ * mechanical band (verbatim reverse-verify → idempotent sitian append →
502
+ * watermark) and records the resulting sitian facts next to the receipt.
503
+ * Machine facts never come from model self-report (锚定宪法).
504
+ */
505
+ export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag) {
506
+ // This turn's catalog, snapshotted at activate: the candidateIndexes the role
507
+ // saw and the rows accept commits are the same bytes. Accept never re-reads
508
+ // the file, so anything written to it mid-turn cannot become a diary entry.
509
+ let catalog;
510
+ const runtime = createFiledOfficerRuntime(roleHost, {
511
+ role: "diarist",
512
+ tool: DIARIST_TOOL_SPEC,
513
+ acceptedText: DIARIST_ACCEPTED_TEXT,
514
+ soulTag: "diarist",
515
+ beforeAccept: async ({ parameters }) => {
516
+ const submitted = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
517
+ ? parameters
518
+ : undefined;
519
+ // True-unbound summons: no ticket, no catalog, no diary committed — so
520
+ // this turn produced no sitian facts. A self-reported `sitian` is not
521
+ // one (锚定宪法); drop it rather than let it ride into decisiveFacts.
522
+ // Carrying the field is not a reason to bounce the receipt (第 0 条).
523
+ if (catalog === undefined) {
524
+ if (submitted === undefined || !("sitian" in submitted))
525
+ return undefined;
526
+ const stripped = { ...submitted };
527
+ delete stripped.sitian;
528
+ return stripped;
529
+ }
530
+ const facts = await commitDiaristSelections({
531
+ catalog,
532
+ selections: projectDiaristSelections(parameters),
533
+ });
534
+ return { ...(submitted ?? { receipt: parameters }), sitian: facts };
535
+ },
536
+ }, dependencies);
537
+ return {
538
+ async activate() {
539
+ catalog = loadFrozenDiaristCatalog(getSourcesFlag);
540
+ return runtime.activate();
541
+ },
542
+ };
543
+ }
544
+ /** Envelope-owned decode + one-shot load of the frozen catalog (ADR 0018). */
545
+ function loadFrozenDiaristCatalog(getFlag) {
546
+ const raw = getFlag();
547
+ if (raw === undefined)
548
+ return undefined;
549
+ if (typeof raw !== "string" || raw.trim() === "") {
550
+ throw new Error("Diarist source catalog flag must be a nonempty path");
551
+ }
552
+ return loadDiaristSourceCatalog(raw);
553
+ }
554
+ export function createCountersignRoleRuntime(roleHost, dependencies, hostActions) {
555
+ // Notary inner gate difference only — lifecycle stays on the shared envelope.
556
+ // Pointer-only summons: officer self-fetches from run dossier (#632 / ADR 0079).
557
+ const beforeAccept = hostActions !== undefined && roleHost.requireGatekeeperPass !== undefined
558
+ ? async ({ toolCallId, signal, ctx }) => {
559
+ await roleHost.requireGatekeeperPass({
560
+ context: ctx,
561
+ subject: { kind: "countersign_verdict" },
562
+ ...(signal === undefined ? {} : { signal }),
563
+ hostActions,
564
+ toolCallId,
565
+ });
566
+ }
567
+ : undefined;
568
+ return createFiledOfficerRuntime(roleHost, {
569
+ role: "countersign",
570
+ tool: COUNTERSIGN_TOOL_SPEC,
571
+ acceptedText: COUNTERSIGN_ACCEPTED_TEXT,
572
+ soulTag: "countersign",
573
+ ...(beforeAccept === undefined ? {} : { beforeAccept }),
574
+ }, dependencies);
575
+ }
576
+ export function createRoleRuntimeExtension(dependencies) {
577
+ return (envelopeHost) => {
578
+ let projectClosedSubmission = async () => {
579
+ throw new Error("角色终局投射接缝尚未初始化");
580
+ };
581
+ const roleHost = createSubmissionLedgerHost(envelopeHost.host, new Map(PACKAGED_ROLE_REGISTRY.map(({ role, outputTool }) => [outputTool, role])), failInfrastructure, async (projection, context) => projectClosedSubmission(projection, context));
582
+ roleHost.registerFlag(ROLE_FLAG.name, ROLE_FLAG.definition);
583
+ // Reviewer transport flags: shared envelope owns registration (ADR 0018).
584
+ for (const flag of REVIEWER_TRANSPORT_FLAGS) {
585
+ roleHost.registerFlag(flag.name, flag.definition);
586
+ }
587
+ for (const flag of NOTARY_TRANSPORT_FLAGS) {
588
+ roleHost.registerFlag(flag.name, flag.definition);
589
+ }
590
+ for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) {
591
+ roleHost.registerFlag(flag.name, flag.definition);
592
+ }
593
+ for (const flag of DIARIST_TRANSPORT_FLAGS) {
594
+ roleHost.registerFlag(flag.name, flag.definition);
595
+ }
596
+ let admitted = false;
597
+ let selectedRole;
598
+ /** Live Reviewer parent activation for envelope agent_start prompt assembly. */
599
+ let activeReviewerParent;
600
+ /** Envelope-owned Reviewer Skill expansion state (ADR 0018 — not a role-module facade). */
601
+ let reviewerOriginalRequest;
602
+ let reviewerExpansionCaptured = false;
603
+ let navigatorAttendance;
604
+ // #351: session-lifecycle owner for periodic OAuth refresh (orthogonal to role admission).
605
+ let pendingNavigatorPresentation;
606
+ let pendingNavigatorSettlement;
607
+ let navigatorWorkContext;
608
+ /** toolCallId → fact+evidence; one-shot projected onto durable tool_result (#475). */
609
+ const pendingInfrastructureFailures = new Map();
610
+ // Envelope-owned execute→tool_result bridge for submission non-pass (ADR 0018 / #525).
611
+ const pendingSubmissionNonPassByToolCallId = new Map();
612
+ let engineDetourRegistered = false;
613
+ // #288 primary-session thin adapter. The policy is the sole budget owner;
614
+ // terminating-tool rejections and mechanical delivery requests share two turns.
615
+ let receiptDelivery = createReceiptDeliveryPolicy();
616
+ let noReceiptRecorded = false;
617
+ // Public-run fetch observation (in-process-session statusAwareFetch face).
618
+ let priorFetch;
619
+ let fetchWrapped = false;
620
+ const settleNavigatorProjection = async (settlement) => {
621
+ const attendance = navigatorAttendance;
622
+ if (settlement === undefined || attendance === undefined)
623
+ return;
624
+ const workContext = navigatorWorkContext;
625
+ const pending = (async () => {
626
+ if (settlement.kind !== "accepted") {
627
+ await attendance.settle(settlement);
628
+ return;
629
+ }
630
+ const settlePromise = attendance.settle(settlement);
631
+ // Attach catch immediately so a late rejection after grace timeout cannot
632
+ // surface as unhandledRejection / stale-ctx after session dispose (#675).
633
+ void settlePromise.catch(() => undefined);
634
+ const raced = await raceNavigatorGrace(settlePromise, NAVIGATOR_POST_ROLE_GRACE_MS);
635
+ if (raced.status !== "timeout")
636
+ return;
637
+ if (pendingNavigatorPresentation === undefined) {
638
+ const routePlaybookReadFailure = attendance.knownRoutePlaybookReadFailure?.();
639
+ const report = {
640
+ disposition: "unavailable",
641
+ unavailableReason: "Navigator exceeded post-role delivery grace",
642
+ unavailableSource: "unknown",
643
+ unavailableCause: "unknown",
644
+ ...(routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure }),
645
+ };
646
+ const event = {
647
+ version: 1,
648
+ disposition: "unavailable",
649
+ invocationId: "post-role-grace-timeout",
650
+ role: settlement.role,
651
+ phase: settlement.phase,
652
+ subjectKey: workContext?.subjectKey ?? "",
653
+ unavailableReason: "Navigator exceeded post-role delivery grace",
654
+ unavailableSource: "unknown",
655
+ unavailableCause: "unknown",
656
+ ...(routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure }),
657
+ };
658
+ pendingNavigatorPresentation = { event, report };
659
+ }
660
+ await attendance.dispose();
661
+ })();
662
+ pendingNavigatorSettlement = pending;
663
+ await pending;
664
+ };
665
+ projectClosedSubmission = async (projection, context) => projectClosedSubmissionLifecycle(projection, context, navigatorPhase(roleHost, projection.role), () => receiptDelivery.recordAccepted(), settleNavigatorProjection);
666
+ roleHost.on("input", (event) => {
667
+ const role = roleHost.getFlag(ROLE_FLAG.name);
668
+ if (role !== undefined && !admitted)
669
+ return { action: "handled" };
670
+ // Envelope exclusively owns Reviewer Skill invocation transform + original-request capture.
671
+ if (role === "reviewer"
672
+ && admitted
673
+ && activeReviewerParent !== undefined
674
+ && reviewerOriginalRequest === undefined) {
675
+ reviewerOriginalRequest = event.text;
676
+ return {
677
+ action: "transform",
678
+ text: activeReviewerParent.skillBinding.invocation(event.text),
679
+ ...(event.images === undefined ? {} : { images: event.images }),
680
+ };
681
+ }
682
+ return { action: "continue" };
683
+ });
684
+ roleHost.on("before_agent_start", async (event, ctx) => {
685
+ const role = roleHost.getFlag(ROLE_FLAG.name);
686
+ if (role === undefined)
687
+ return;
688
+ if (!admitted || selectedRole !== role) {
689
+ failInfrastructure(new ActivationBarrierError(role), ctx);
690
+ }
691
+ // Notary session bound: envelope-owned lifecycle write (ADR 0018 / #582).
692
+ // Ticket flag register/read + session entry live here; role projects admitted bound only.
693
+ if (role === "notary") {
694
+ const bound = projectNotaryBoundFromFlags((name) => roleHost.getFlag(name));
695
+ if (bound !== undefined) {
696
+ ctx.sessionManager.appendCustomEntry?.(NOTARY_SESSION_BOUND_ENTRY, bound);
697
+ }
698
+ }
699
+ if (navigatorAttendance !== undefined && navigatorWorkContext !== undefined && navigatorWorkContext.contextError === undefined) {
700
+ // Flagged roles already have a concrete packet/task/case/review input.
701
+ // A bare Judge (and other bare packaged entrypoint) gets its concrete
702
+ // user task at this seam; do not copy the assembled system prompt.
703
+ // Replacement is keyed by typed subject provenance, never prose prefixes.
704
+ // Soft session_start placeholders (no materials yet) recover here; hard
705
+ // contextError from true loader failures stays poisoned and honest.
706
+ if (navigatorWorkContext.subjectProvenance === "placeholder") {
707
+ const subject = event.prompt.trim();
708
+ if (subject !== "") {
709
+ const root = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd);
710
+ const subjectProvenance = "user_prompt";
711
+ const priorAuthority = navigatorWorkContext.authority;
712
+ const authority = typeof priorAuthority === "string" && priorAuthority.trim() !== ""
713
+ ? priorAuthority
714
+ : subject;
715
+ navigatorWorkContext = {
716
+ subjectKey: navigatorSubjectKey(root, subject, subjectProvenance),
717
+ subject,
718
+ authority,
719
+ subjectProvenance,
720
+ };
721
+ await navigatorAttendance.setWorkContext(navigatorWorkContext);
722
+ }
723
+ }
724
+ }
725
+ navigatorAttendance?.prepare();
726
+ // Envelope-owned Reviewer expansion capture + parent prompt assembly (no role-module callback).
727
+ if (role === "reviewer" && activeReviewerParent !== undefined) {
728
+ if (!reviewerExpansionCaptured) {
729
+ if (reviewerOriginalRequest === undefined
730
+ || activeReviewerParent.skillBinding.captureExpansion(roleHost.capabilities?.skillExpansion(event.prompt), reviewerOriginalRequest) === undefined) {
731
+ failInfrastructure(new Error("Canonical code-review Skill expansion did not match the captured request"), ctx);
732
+ }
733
+ reviewerExpansionCaptured = true;
734
+ }
735
+ const specDisposition = activeReviewerParent.getSpecDisposition();
736
+ return {
737
+ systemPrompt: assembleReviewerParentSystemPrompt({
738
+ baseSystemPrompt: event.systemPrompt,
739
+ soul: activeReviewerParent.soul,
740
+ ...(specDisposition === undefined ? {} : { specDisposition }),
741
+ }),
742
+ };
743
+ }
744
+ });
745
+ roleHost.on("tool_result", async (event) => {
746
+ const role = selectedRole;
747
+ if (role === undefined)
748
+ return;
749
+ const pendingInfra = pendingInfrastructureFailures.get(event.toolCallId);
750
+ const isRoleInfrastructureFailure = pendingInfra !== undefined;
751
+ if (pendingInfra !== undefined)
752
+ pendingInfrastructureFailures.delete(event.toolCallId);
753
+ // One-shot project fact + typed evidence so live settlement and durable session agree.
754
+ const infrastructureDetails = pendingInfra?.details;
755
+ const classified = infrastructureDetails === undefined
756
+ ? event
757
+ : { ...event, details: infrastructureDetails };
758
+ const isOutputTool = event.toolName === navigatorOutputTool(role);
759
+ const outputClassification = isOutputTool ? classifyPackagedRoleTerminalResult(classified) : undefined;
760
+ if (isRoleInfrastructureFailure || outputClassification?.kind === "infrastructure") {
761
+ receiptDelivery.stopForInfrastructure();
762
+ }
763
+ else if (isOutputTool && outputClassification?.kind === "nonterminal" && event.isError) {
764
+ const reason = (event.content ?? [])
765
+ .map((part) => part.type === "text" && "text" in part ? part.text : "")
766
+ .join("")
767
+ .trim() || "terminating tool rejected";
768
+ receiptDelivery.recordRejected(reason);
769
+ }
770
+ // Accepted/human terminal projection belongs exclusively to typed ledger
771
+ // closure. tool_result retains only infrastructure settlement.
772
+ const settlement = isRoleInfrastructureFailure || outputClassification?.kind === "infrastructure"
773
+ ? publicNavigatorSettlement(role, navigatorPhase(roleHost, role), classified)
774
+ : undefined;
775
+ await settleNavigatorProjection(settlement);
776
+ // Persist typed infrastructure-failure fact onto the role session toolResult so
777
+ // exact-session restart shares the same durable completion classification.
778
+ if (infrastructureDetails !== undefined) {
779
+ return { details: infrastructureDetails, isError: true };
780
+ }
781
+ // Submission non-pass: throw kept message text for the model; project the
782
+ // envelope-bound structured result onto session details at this tool_result seam.
783
+ const submissionNonPass = pendingSubmissionNonPassByToolCallId.get(event.toolCallId);
784
+ if (submissionNonPass !== undefined) {
785
+ pendingSubmissionNonPassByToolCallId.delete(event.toolCallId);
786
+ return { details: submissionNonPass, isError: true };
787
+ }
788
+ // Recommendation rides the accepted settlement record's content so the one
789
+ // mandatory last-ak_*_output extraction surfaces route/next/reason without
790
+ // a second file, grep, or nesting step. Receipt details stay contract-pure;
791
+ // unavailable/no-advice leave the settlement untouched.
792
+ if (event.isError)
793
+ return;
794
+ const decorated = decorateSettlementWithNavigation(event, pendingNavigatorPresentation);
795
+ if (decorated === undefined)
796
+ return;
797
+ return {
798
+ content: decorated.content,
799
+ };
800
+ });
801
+ // Queue receipt delivery before `agent_settled`: that event means Pi has
802
+ // already decided no queued continuation will run, so a triggerTurn there is
803
+ // too late for print/json sessions. `agent_end` is the last production seam
804
+ // whose queued next turn is consumed before settlement.
805
+ roleHost.on("agent_end", (event, ctx) => {
806
+ const lastMessage = event.messages.at(-1);
807
+ if (lastMessage?.role === "assistant"
808
+ && (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted")) {
809
+ // Provider failure/abort has no tool_result event; classify it here so the
810
+ // receipt policy cannot turn infrastructure death into an exit-0 lifecycle.
811
+ receiptDelivery.stopForInfrastructure();
812
+ return;
813
+ }
814
+ if (receiptDelivery.nextAction() === "request-delivery") {
815
+ receiptDelivery.recordDeliveryRequest();
816
+ // Keep the package-owned continuation off the public input lifecycle:
817
+ // receipt delivery must not be mistaken for later caller input.
818
+ envelopeHost.appendEntry("ak-receipt-delivery-request");
819
+ try {
820
+ sitianReport({
821
+ level: "event",
822
+ kind: "receipt-delivery",
823
+ cwd: ctx.cwd,
824
+ sessionParent: ctx.sessionManager.getSessionFile(),
825
+ payload: { type: "ak-receipt-delivery-request" },
826
+ source: "role-runtime",
827
+ });
828
+ }
829
+ catch { }
830
+ envelopeHost.sendMessage({
831
+ customType: "ak-receipt-delivery-prompt",
832
+ content: RECEIPT_DELIVERY_PROMPT,
833
+ display: false,
834
+ }, { triggerTurn: true, deliverAs: "followUp" });
835
+ }
836
+ else if (receiptDelivery.nextAction() === "no-receipt" && !noReceiptRecorded) {
837
+ const runPointer = process.env.AK_ROLE_RUN_DIR;
838
+ if (runPointer !== undefined) {
839
+ noReceiptRecorded = true;
840
+ const facts = receiptDelivery.facts({ runPointer, attemptPointer: `current:${runPointer}` });
841
+ envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
842
+ try {
843
+ sitianReport({
844
+ level: "event",
845
+ kind: "no-receipt-lifecycle",
846
+ cwd: ctx.cwd,
847
+ sessionParent: ctx.sessionManager.getSessionFile(),
848
+ payload: facts,
849
+ source: "role-runtime",
850
+ });
851
+ }
852
+ catch { }
853
+ }
854
+ }
855
+ });
856
+ roleHost.on("agent_settled", async () => {
857
+ if (pendingNavigatorSettlement !== undefined) {
858
+ await pendingNavigatorSettlement;
859
+ }
860
+ // Do not auto-drain Navigator on ordinary mid-turn agent_settled.
861
+ // Multi-turn roles (#162 coder) fire agent_settled after every tool turn;
862
+ // discarding a healthy/in-flight prepare there forces a cold prepare on the
863
+ // final accepted terminal and races the post-role grace. Keep preparation
864
+ // until an accepted/human/infrastructure tool_result settlement (or dispose).
865
+ // Provider death without any role tool_result leaves preparation held; the
866
+ // next explicit settlement or session end owns it — not mid-turn churn.
867
+ pendingNavigatorSettlement = undefined;
868
+ const presentation = pendingNavigatorPresentation;
869
+ pendingNavigatorPresentation = undefined;
870
+ if (presentation === undefined)
871
+ return;
872
+ await envelopeHost.sendMessage({
873
+ customType: NAVIGATOR_EVENT_TYPE,
874
+ content: formatNavigatorReport(presentation.report),
875
+ display: true,
876
+ details: presentation.event,
877
+ }, { triggerTurn: false });
878
+ });
879
+ roleHost.on("session_shutdown", async () => {
880
+ // #351: stop OAuth keepalive first so shutdown yields zero further ticks.
881
+ envelopeHost.stopKeepalive();
882
+ if (fetchWrapped && priorFetch !== undefined) {
883
+ globalThis.fetch = priorFetch;
884
+ priorFetch = undefined;
885
+ fetchWrapped = false;
886
+ }
887
+ // Flush any still-pending affirmative attendance before teardown. Accepted
888
+ // grace-timeout paths normally emit on agent_settled; abort can skip that hook.
889
+ const presentation = pendingNavigatorPresentation;
890
+ pendingNavigatorPresentation = undefined;
891
+ if (presentation !== undefined) {
892
+ try {
893
+ await envelopeHost.sendMessage({
894
+ customType: NAVIGATOR_EVENT_TYPE,
895
+ content: formatNavigatorReport(presentation.report),
896
+ display: true,
897
+ details: presentation.event,
898
+ }, { triggerTurn: false });
899
+ }
900
+ catch {
901
+ // Teardown must not mask the original role failure cause.
902
+ }
903
+ }
904
+ await navigatorAttendance?.dispose();
905
+ navigatorAttendance = undefined;
906
+ pendingNavigatorSettlement = undefined;
907
+ pendingInfrastructureFailures.clear();
908
+ pendingSubmissionNonPassByToolCallId.clear();
909
+ observationFace.reset();
910
+ });
911
+ const hostActions = {
912
+ failInfrastructure(error, ctx, toolCallId) {
913
+ if (toolCallId !== undefined) {
914
+ pendingInfrastructureFailures.set(toolCallId, buildPendingInfrastructureFailure(error));
915
+ }
916
+ failInfrastructure(error, ctx);
917
+ },
918
+ bindSubmissionNonPass(toolCallId, result) {
919
+ pendingSubmissionNonPassByToolCallId.set(toolCallId, result);
920
+ },
921
+ };
922
+ const judge = createJudgeRoleRuntime(roleHost, {
923
+ loadSoul: dependencies.loadJudgeSoul,
924
+ auditSoulCompliance: dependencies.auditSoulCompliance,
925
+ }, hostActions);
926
+ const fixer = createFixerRoleRuntime(roleHost, {
927
+ async loadSoul() {
928
+ if (dependencies.loadFixerSoul === undefined) {
929
+ throw new Error("fixer soul loader is not configured");
930
+ }
931
+ return dependencies.loadFixerSoul();
932
+ },
933
+ async loadPacket(path) {
934
+ if (dependencies.loadFixPacket === undefined) {
935
+ throw new Error("Fixer packet loader is not configured");
936
+ }
937
+ return dependencies.loadFixPacket(path);
938
+ },
939
+ }, hostActions);
940
+ const coder = createCoderRoleRuntime(roleHost, {
941
+ async loadSoul() {
942
+ if (dependencies.loadCoderSoul === undefined) {
943
+ throw new Error("coder soul loader is not configured");
944
+ }
945
+ return dependencies.loadCoderSoul();
946
+ },
947
+ async loadTask(path) {
948
+ if (dependencies.loadCoderTask === undefined) {
949
+ throw new Error("Coder task loader is not configured");
950
+ }
951
+ return dependencies.loadCoderTask(path);
952
+ },
953
+ ...(dependencies.loadCanonicalSkillBinding === undefined
954
+ ? {}
955
+ : {
956
+ loadCanonicalSkillBinding: (name) => dependencies.loadCanonicalSkillBinding(name),
957
+ }),
958
+ }, hostActions);
959
+ const reviewer = createReviewerRoleRuntime(roleHost, {
960
+ async loadSoul() {
961
+ if (dependencies.loadReviewerSoul === undefined) {
962
+ throw new Error("reviewer soul loader is not configured");
963
+ }
964
+ return dependencies.loadReviewerSoul();
965
+ },
966
+ async createPinnedGitReader() {
967
+ if (dependencies.createReviewerPinnedGitReader === undefined)
968
+ throw new Error("Reviewer runtime dependencies are not configured");
969
+ return dependencies.createReviewerPinnedGitReader();
970
+ },
971
+ async loadCanonicalSkillBinding(name) {
972
+ if (dependencies.loadCanonicalSkillBinding === undefined) {
973
+ throw new Error("Reviewer runtime dependencies are not configured");
974
+ }
975
+ return dependencies.loadCanonicalSkillBinding(name);
976
+ },
977
+ ...(dependencies.createReviewerIssueFetcher === undefined
978
+ ? {}
979
+ : { fetchIssue: dependencies.createReviewerIssueFetcher() }),
980
+ async runDispatch(dispatch, options) {
981
+ if (dependencies.runReviewerDispatch === undefined)
982
+ throw new Error("Reviewer runtime dependencies are not configured");
983
+ return dependencies.runReviewerDispatch(dispatch, options);
984
+ },
985
+ ...(dependencies.shutdownReviewerAgent === undefined
986
+ ? {}
987
+ : { shutdownAgent: dependencies.shutdownReviewerAgent }),
988
+ }, hostActions);
989
+ const doctor = createDoctorRoleRuntime(roleHost, {
990
+ async loadSoul() { if (!dependencies.loadDoctorSoul)
991
+ throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorSoul(); },
992
+ async loadCase(path) { if (!dependencies.loadDoctorCase)
993
+ throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorCase(path); },
994
+ async auditCompliance(options) { if (!dependencies.auditDoctorCompliance)
995
+ throw new Error("Doctor runtime dependencies are not configured"); return dependencies.auditDoctorCompliance(options); },
996
+ }, hostActions);
997
+ const notary = createNotaryRoleRuntime(roleHost, {
998
+ async loadSoul() {
999
+ if (!dependencies.loadNotarySoul)
1000
+ throw new Error("Notary runtime dependencies are not configured");
1001
+ return dependencies.loadNotarySoul();
1002
+ },
1003
+ async loadSourceRunLocator(path) {
1004
+ if (!dependencies.loadNotarySourceRun)
1005
+ throw new Error("Notary runtime dependencies are not configured");
1006
+ return dependencies.loadNotarySourceRun(path);
1007
+ },
1008
+ }, hostActions);
1009
+ const countersign = createCountersignRoleRuntime(roleHost, {
1010
+ async loadSoul() {
1011
+ if (!dependencies.loadCountersignSoul)
1012
+ throw new Error("Countersign runtime dependencies are not configured");
1013
+ return dependencies.loadCountersignSoul();
1014
+ },
1015
+ }, hostActions);
1016
+ const gleanerLeftRuntime = createGleanerLeftRoleRuntime(roleHost, {
1017
+ async loadSoul() {
1018
+ if (!dependencies.loadGleanerLeftSoul)
1019
+ throw new Error("Gleaner-left runtime dependencies are not configured");
1020
+ return dependencies.loadGleanerLeftSoul();
1021
+ },
1022
+ });
1023
+ const gleanerLeft = {
1024
+ async activate() {
1025
+ decodeGleanerLeftBase((name) => envelopeHost.host.getFlag(name));
1026
+ return gleanerLeftRuntime.activate();
1027
+ },
1028
+ };
1029
+ const inspector = createInspectorRoleRuntime(roleHost, {
1030
+ async loadSoul() {
1031
+ if (!dependencies.loadInspectorSoul)
1032
+ throw new Error("Inspector runtime dependencies are not configured");
1033
+ return dependencies.loadInspectorSoul();
1034
+ },
1035
+ });
1036
+ const gatekeeper = createGatekeeperRoleRuntime(roleHost, {
1037
+ async loadSoul() {
1038
+ if (!dependencies.loadGatekeeperSoul)
1039
+ throw new Error("Gatekeeper runtime dependencies are not configured");
1040
+ return dependencies.loadGatekeeperSoul();
1041
+ },
1042
+ });
1043
+ const navigator = createNavigatorRoleRuntime(roleHost, {
1044
+ async loadSoul() {
1045
+ if (!dependencies.loadNavigatorSoul)
1046
+ throw new Error("Navigator runtime dependencies are not configured");
1047
+ return dependencies.loadNavigatorSoul();
1048
+ },
1049
+ });
1050
+ const auditor = createAuditorRoleRuntime(roleHost, {
1051
+ async loadSoul() {
1052
+ if (!dependencies.loadAuditorSoul)
1053
+ throw new Error("Auditor runtime dependencies are not configured");
1054
+ return dependencies.loadAuditorSoul();
1055
+ },
1056
+ });
1057
+ const diarist = createDiaristRoleRuntime(roleHost, {
1058
+ async loadSoul() {
1059
+ if (!dependencies.loadDiaristSoul)
1060
+ throw new Error("Diarist runtime dependencies are not configured");
1061
+ return dependencies.loadDiaristSoul();
1062
+ },
1063
+ }, () => envelopeHost.host.getFlag(DIARIST_SOURCES_FLAG.name));
1064
+ let sessionMergerGitState = dependencies.mergerGitState;
1065
+ const merger = createMergerRoleRuntime(roleHost, {
1066
+ async loadSoul() { if (!dependencies.loadMergerSoul)
1067
+ throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
1068
+ async loadInput(path) { if (!dependencies.loadMergerInput)
1069
+ throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
1070
+ gitState: {
1071
+ activeMerge() { if (!sessionMergerGitState)
1072
+ throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.activeMerge(); },
1073
+ completedMerge(mergeCommitId, automaticMergeTreeId) { if (!sessionMergerGitState)
1074
+ throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.completedMerge(mergeCommitId, automaticMergeTreeId); },
1075
+ },
1076
+ }, hostActions);
1077
+ const collector = createCollectorRoleRuntime(roleHost, {
1078
+ async loadSoul() {
1079
+ if (dependencies.loadCollectorSoul === undefined) {
1080
+ throw new Error("collector soul loader is not configured");
1081
+ }
1082
+ return dependencies.loadCollectorSoul();
1083
+ },
1084
+ createTransport() {
1085
+ if (dependencies.createCollectorTransport === undefined) {
1086
+ throw new Error("Collector GitHub transport is not configured");
1087
+ }
1088
+ return dependencies.createCollectorTransport();
1089
+ },
1090
+ createLedger(config, collectorClock, context) {
1091
+ const append = context.sessionManager?.appendCustomEntry;
1092
+ return createCollectorLedger(config, {
1093
+ clock: collectorClock,
1094
+ ...(append === undefined
1095
+ ? {}
1096
+ : {
1097
+ journal: {
1098
+ append(customType, data) {
1099
+ context.sessionManager?.appendCustomEntry?.(customType, data);
1100
+ },
1101
+ },
1102
+ }),
1103
+ dossierEntries: context.sessionManager?.getEntries?.() ?? [],
1104
+ });
1105
+ },
1106
+ ...(dependencies.createCollectorClock === undefined
1107
+ ? {}
1108
+ : { createClock: dependencies.createCollectorClock }),
1109
+ ...(dependencies.collectorPackageExtensionPath === undefined
1110
+ ? {}
1111
+ : {
1112
+ packageExtensionPath: dependencies.collectorPackageExtensionPath,
1113
+ }),
1114
+ }, hostActions);
1115
+ const clock = dependencies.activationClock ?? (() => new Date().toISOString());
1116
+ const writeTrace = dependencies.activationTraceWriter ?? writeActivationTraceRecord;
1117
+ const observationFace = createToolExecutionObservationFace({
1118
+ role: () => selectedRole,
1119
+ admitted: () => admitted,
1120
+ clock: dependencies.toolExecutionObservationClock ?? clock,
1121
+ monoNow: dependencies.toolExecutionObservationMonoNow ?? systemToolExecutionObservationMonoNow,
1122
+ write: dependencies.toolExecutionObservationWriter ?? writeToolExecutionObservationRecord,
1123
+ });
1124
+ // ExtensionRunner.emit catches ordinary handler throws, emits extension error, and continues.
1125
+ // Observation plane failures must still hit the shared infrastructure termination path
1126
+ // (abort + nonzero print/json exit) with the original cause before that swallow.
1127
+ const observe = async (run, ctx) => {
1128
+ try {
1129
+ await run();
1130
+ }
1131
+ catch (error) {
1132
+ failInfrastructure(error, ctx);
1133
+ }
1134
+ };
1135
+ roleHost.on("tool_execution_start", async (event, ctx) => {
1136
+ await observe(() => observationFace.onStart(event), ctx);
1137
+ });
1138
+ roleHost.on("tool_execution_update", async (event, ctx) => {
1139
+ await observe(() => observationFace.onUpdate(event), ctx);
1140
+ });
1141
+ roleHost.on("tool_execution_end", async (event, ctx) => {
1142
+ await observe(() => observationFace.onEnd(event), ctx);
1143
+ });
1144
+ // Public Role run: record typed non-success HTTP for error evidence + v1 resume.
1145
+ // Same observation owner as in-process-session statusAwareFetch → typed-provider-http
1146
+ // sidecar (settlement already merges observation.httpStatus into knownFailure).
1147
+ // after_provider_response covers the success-path onResponse face; fetch wrap covers
1148
+ // non-2xx Responses where openai-completions throws before onResponse (#675).
1149
+ const recordHttpObservation = async (status, provider, ctx) => {
1150
+ const runDir = process.env.AK_ROLE_RUN_DIR;
1151
+ if (typeof runDir !== "string" || runDir.trim() === "")
1152
+ return;
1153
+ try {
1154
+ await recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider });
1155
+ }
1156
+ catch (error) {
1157
+ if (status >= 200 &&
1158
+ status < 300 &&
1159
+ error instanceof Error &&
1160
+ "code" in error &&
1161
+ error.code === "ENOENT") {
1162
+ return;
1163
+ }
1164
+ failInfrastructure(error, ctx);
1165
+ }
1166
+ };
1167
+ roleHost.on("after_provider_response", async (event, ctx) => {
1168
+ const status = event.status;
1169
+ if (typeof status !== "number")
1170
+ return;
1171
+ const fromCtx = ctx.model?.provider;
1172
+ const provider = typeof fromCtx === "string" && fromCtx.trim() !== ""
1173
+ ? fromCtx
1174
+ : "unknown";
1175
+ await recordHttpObservation(status, provider, ctx);
1176
+ });
1177
+ roleHost.on("session_start", async (event, ctx) => {
1178
+ // Scope fetch observation to this public run (in-process-session statusAwareFetch face).
1179
+ if (!fetchWrapped && typeof globalThis.fetch === "function") {
1180
+ priorFetch = globalThis.fetch.bind(globalThis);
1181
+ const underlying = priorFetch;
1182
+ globalThis.fetch = (async (input, init) => {
1183
+ const response = await underlying(input, init);
1184
+ const runDir = process.env.AK_ROLE_RUN_DIR;
1185
+ if (typeof runDir === "string"
1186
+ && runDir.trim() !== ""
1187
+ && typeof response?.status === "number"
1188
+ && (response.status < 200 || response.status >= 300)) {
1189
+ const provider = typeof ctx.model?.provider === "string" && ctx.model.provider.trim() !== ""
1190
+ ? ctx.model.provider
1191
+ : "unknown";
1192
+ try {
1193
+ await recordTypedProviderHttpStatus(runDir, {
1194
+ httpStatus: response.status,
1195
+ provider,
1196
+ });
1197
+ }
1198
+ catch {
1199
+ // Observation must not break the provider stream (same as in-process-session).
1200
+ }
1201
+ }
1202
+ return response;
1203
+ });
1204
+ fetchWrapped = true;
1205
+ }
1206
+ admitted = false;
1207
+ selectedRole = undefined;
1208
+ activeReviewerParent = undefined;
1209
+ reviewerOriginalRequest = undefined;
1210
+ reviewerExpansionCaptured = false;
1211
+ receiptDelivery = createReceiptDeliveryPolicy();
1212
+ noReceiptRecorded = false;
1213
+ observationFace.reset();
1214
+ pendingNavigatorPresentation = undefined;
1215
+ pendingNavigatorSettlement = undefined;
1216
+ pendingInfrastructureFailures.clear();
1217
+ pendingSubmissionNonPassByToolCallId.clear();
1218
+ navigatorWorkContext = undefined;
1219
+ // #351: OAuth keepalive is orthogonal to --ak-role; start before role early-return
1220
+ // so role-less sessions (and reload after shutdown stop) still keep tokens alive.
1221
+ envelopeHost.startKeepalive(ctx);
1222
+ const rawRole = roleHost.getFlag(ROLE_FLAG.name);
1223
+ if (rawRole === undefined)
1224
+ return;
1225
+ const entry = PACKAGED_ROLE_REGISTRY.find(({ role }) => role === rawRole);
1226
+ if (entry === undefined) {
1227
+ failInfrastructure(new Error(`Unsupported workflow role: ${String(rawRole)}`), ctx);
1228
+ }
1229
+ selectedRole = entry.role;
1230
+ await navigatorAttendance?.dispose();
1231
+ navigatorAttendance = undefined;
1232
+ const runtime = {
1233
+ event,
1234
+ context: ctx,
1235
+ judge,
1236
+ fixer,
1237
+ coder,
1238
+ reviewer,
1239
+ decodeReviewerAdmitted() {
1240
+ return decodeReviewerAdmittedInputs((name) => roleHost.getFlag(name));
1241
+ },
1242
+ bindReviewerParent(activation) {
1243
+ activeReviewerParent = activation;
1244
+ },
1245
+ decodeNotaryAdmitted() {
1246
+ const ticketNumber = readNotaryTicketFlag(roleHost.getFlag(NOTARY_TICKET_FLAG.name));
1247
+ return ticketNumber === undefined ? undefined : { ticketNumber };
1248
+ },
1249
+ collector,
1250
+ doctor,
1251
+ notary,
1252
+ countersign,
1253
+ gleanerLeft,
1254
+ inspector,
1255
+ gatekeeper,
1256
+ navigator,
1257
+ auditor,
1258
+ diarist,
1259
+ merger: async () => {
1260
+ if (dependencies.mergerGitState === undefined) {
1261
+ sessionMergerGitState = dependencies.createMergerGitState?.(ctx.cwd);
1262
+ }
1263
+ if (sessionMergerGitState === undefined)
1264
+ throw new Error("Merger runtime dependencies are not configured");
1265
+ await merger.activate();
1266
+ },
1267
+ };
1268
+ try {
1269
+ // Production topology only (ADR 0048/0049): no test-only ledger hooks.
1270
+ // Admit durable session file first so lifecycle getEntries/appendEntry are truthful:
1271
+ // deferred SM materialization must not wipe an in-memory principal marker.
1272
+ const bookKey = resolveBookKeyFromGit(ctx.cwd);
1273
+ const correlation = correlationIdentityFromEnv();
1274
+ const sessionFile = ctx.sessionManager.getSessionFile?.() || ctx.sessionManager.getSessionDir?.();
1275
+ const ledgerHome = resolveActivationLedgerHomeForPath(sessionFile);
1276
+ const session = durableSessionPointer(ctx.sessionManager);
1277
+ // Same public activation face for every role (#675 / owner 09-06): no nested-env
1278
+ // skip, no work-role whitelist. Attendance attaches when the envelope supplies it.
1279
+ // Navigator seat never re-attaches itself — prepare turns already ARE the navigator
1280
+ // public activation (prevents summonPublicRole navigator ↔ attendance recursion).
1281
+ if (dependencies.createNavigatorAttendance !== undefined
1282
+ && entry.role !== "navigator") {
1283
+ let work;
1284
+ let contextError;
1285
+ if (dependencies.loadNavigatorWorkContext === undefined) {
1286
+ const fallbackSubjectKey = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd);
1287
+ contextError = new Error("Navigator work context loader is not configured");
1288
+ work = { subjectKey: fallbackSubjectKey, subject: `work subject: ${fallbackSubjectKey}`, authority: "", subjectProvenance: "placeholder" };
1289
+ }
1290
+ else {
1291
+ try {
1292
+ work = await dependencies.loadNavigatorWorkContext({
1293
+ context: ctx,
1294
+ role: entry.role,
1295
+ phase: navigatorPhase(roleHost, entry.role),
1296
+ getFlag: (name) => roleHost.getFlag(name),
1297
+ });
1298
+ contextError = work.contextError;
1299
+ }
1300
+ catch (error) {
1301
+ // Contract: README.md#Navigator-attendance — a failed context load continues with a typed placeholder work context; the original cause is retained in contextError for the typed unavailable report.
1302
+ contextError = navigatorUnavailableError("context", error);
1303
+ const fallbackSubjectKey = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd);
1304
+ work = { subjectKey: fallbackSubjectKey, subject: `work subject: ${fallbackSubjectKey}`, authority: "", subjectProvenance: "placeholder" };
1305
+ }
1306
+ }
1307
+ navigatorWorkContext = { ...work, ...(contextError === undefined ? {} : { contextError }) };
1308
+ // Shared envelope owns exact invocation principal from admitted session lifecycle.
1309
+ // session_start is process activation: resume unfinished principal only when marker
1310
+ // role/phase/subjectKey still match; mint for contradictory marker, malformed nearest,
1311
+ // missing marker, or terminal already completed.
1312
+ const sessionEntries = [...ctx.sessionManager.getEntries()];
1313
+ const invocationPhase = navigatorPhase(roleHost, entry.role);
1314
+ const lifecyclePrincipal = resolveLifecycleInvocationPrincipal(sessionEntries, {
1315
+ role: entry.role,
1316
+ phase: invocationPhase,
1317
+ subjectKey: work.subjectKey,
1318
+ });
1319
+ const invocationId = lifecyclePrincipal.invocationId;
1320
+ if (!lifecyclePrincipal.resume) {
1321
+ const data = {
1322
+ invocationId,
1323
+ role: entry.role,
1324
+ phase: invocationPhase,
1325
+ subjectKey: work.subjectKey,
1326
+ };
1327
+ envelopeHost.appendEntry(NAVIGATOR_INVOCATION_ENTRY, data);
1328
+ try {
1329
+ sitianReport({
1330
+ level: "event",
1331
+ kind: "attendance",
1332
+ cwd: ctx.cwd,
1333
+ sessionParent: ctx.sessionManager.getSessionFile(),
1334
+ payload: { type: NAVIGATOR_INVOCATION_ENTRY, ...data },
1335
+ source: "role-runtime",
1336
+ });
1337
+ }
1338
+ catch { }
1339
+ }
1340
+ navigatorAttendance = await dependencies.createNavigatorAttendance({
1341
+ context: ctx,
1342
+ role: entry.role,
1343
+ phase: invocationPhase,
1344
+ subjectKey: work.subjectKey,
1345
+ subject: work.subject,
1346
+ authority: work.authority,
1347
+ invocationId,
1348
+ ...(contextError === undefined ? {} : { contextError }),
1349
+ onEvent: (navigatorEvent, report) => {
1350
+ pendingNavigatorPresentation = { event: navigatorEvent, report };
1351
+ },
1352
+ });
1353
+ // Warm live help during activation so prepare is not help-bound under load.
1354
+ // Concrete work context also starts full preparation so session create
1355
+ // overlaps the role run. Placeholder subjects wait for before_agent_start
1356
+ // (user prompt may replace the subject key) but still inherit warm help.
1357
+ navigatorAttendance.warmHelp?.();
1358
+ if (navigatorWorkContext.contextError === undefined &&
1359
+ navigatorWorkContext.subjectProvenance !== "placeholder") {
1360
+ navigatorAttendance.prepare();
1361
+ }
1362
+ }
1363
+ await executeActivationStage(entry.role, activationStage(entry.role, runtime), { clock, writeTrace });
1364
+ // #357 T2 / #378 / #380 / #391: any role+engine activation registers the package detour tool once.
1365
+ // Gate is env presence only — no per-engine execute branch; no role-module spawn.
1366
+ if (!engineDetourRegistered) {
1367
+ engineDetourRegistered = registerEngineDetourTool(roleHost, hostActions);
1368
+ }
1369
+ // Worker gates ①②: arm records baseline and runs private one-shot hook uninstall (ADR 0070).
1370
+ // Parent session feeds #216 createRecordSession so baseline/bounce survive resume.
1371
+ if (entry.role === "coder" || entry.role === "fixer") {
1372
+ if (entry.role === "coder")
1373
+ coder.armSubmissionGate(ctx.cwd, ctx.sessionManager);
1374
+ else
1375
+ fixer.armSubmissionGate(ctx.cwd, ctx.sessionManager);
1376
+ }
1377
+ appendAcceptedActivationToBook({
1378
+ ledgerHome,
1379
+ fact: buildAcceptedActivationFact({
1380
+ role: entry.role,
1381
+ observedAt: clock(),
1382
+ bookKey,
1383
+ session,
1384
+ correlation,
1385
+ }),
1386
+ });
1387
+ admitted = true;
1388
+ }
1389
+ catch (error) {
1390
+ failInfrastructure(error, ctx);
1391
+ }
1392
+ });
1393
+ };
1394
+ }