@akagilnc/pi-workflow-roles 0.1.4528 → 0.1.4620

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 (55) hide show
  1. package/README.md +5 -4
  2. package/README.zh-CN.md +6 -5
  3. package/dist/acp-host/production-host.js +1834 -637
  4. package/dist/archivist-record-topology.js +27 -35
  5. package/dist/headless-host/description.js +7 -4
  6. package/dist/headless-host/production-host.js +1652 -519
  7. package/dist/migrate-book-topology.js +8 -11
  8. package/dist/navigator-attendance.js +146 -241
  9. package/dist/navigator-public-session.js +81 -15
  10. package/dist/package-contracts/navigator-output.js +30 -12
  11. package/dist/public-cli/invocation.js +10 -4
  12. package/dist/public-cli/judge-run.js +2 -2
  13. package/dist/public-cli/main.js +879 -410
  14. package/dist/public-cli/option-definitions.js +6 -6
  15. package/dist/public-cli/post-admission.js +25 -10
  16. package/dist/public-cli/reviewer-run.js +339 -0
  17. package/dist/public-cli/run-lifecycle.js +29 -7
  18. package/dist/public-cli/settlement.js +46 -32
  19. package/dist/public-cli/terminal.js +17 -27
  20. package/dist/public-cli/turn-request.js +1 -1
  21. package/dist/public-role-summons.js +370 -20
  22. package/dist/submission-ledger.js +56 -23
  23. package/package.json +1 -1
  24. package/resources/836-deleted-machine-instruction-inventory.md +2 -0
  25. package/resources/navigator-route-playbook.md +9 -0
  26. package/scripts/build-package.mjs +6 -1
  27. package/src/acp-host/role-turn-host.ts +129 -13
  28. package/src/headless-host/description.ts +17 -7
  29. package/src/headless-host/role-turn-host.ts +42 -11
  30. package/src/host-contracts.ts +0 -1
  31. package/src/navigator-attendance.ts +290 -425
  32. package/src/navigator-public-session.ts +126 -25
  33. package/src/navigator-role.ts +2 -2
  34. package/src/package-contracts/navigator-output.ts +41 -25
  35. package/src/public-cli/coder-run.ts +2 -2
  36. package/src/public-cli/fixer-run.ts +2 -2
  37. package/src/public-cli/invocation.ts +19 -9
  38. package/src/public-cli/judge-run.ts +2 -2
  39. package/src/public-cli/merger-run.ts +2 -2
  40. package/src/public-cli/option-definitions.ts +6 -6
  41. package/src/public-cli/post-admission.ts +41 -10
  42. package/src/public-cli/reviewer-run.ts +238 -53
  43. package/src/public-cli/run-lifecycle.ts +37 -7
  44. package/src/public-cli/settlement.ts +42 -34
  45. package/src/public-cli/terminal.ts +39 -37
  46. package/src/public-cli/turn-request.ts +7 -1
  47. package/src/public-role-summons.ts +544 -22
  48. package/src/reviewer-role.ts +1 -2
  49. package/src/role-envelope.ts +89 -4
  50. package/src/role-runtime.ts +76 -16
  51. package/src/submission-ledger.ts +63 -21
  52. package/dist/public-cli/command-renderer.js +0 -5
  53. package/dist/public-command-renderer.js +0 -29
  54. package/src/public-cli/command-renderer.ts +0 -9
  55. package/src/public-command-renderer.ts +0 -55
@@ -13,7 +13,6 @@ export type { ReviewerIntent };
13
13
  /** Frozen admitted inputs the behavior layer may consume — no flag surface. */
14
14
  export type ReviewerAdmittedInputs = Readonly<{
15
15
  baseRevision: string;
16
- /** Caller-selected single lens; required, no default. */
17
16
  lens: "completeness" | "correctness";
18
17
  authorityRefs?: readonly string[];
19
18
  /** Typed #176 ticketNumber from admitted invocation (Spec self-fetch primary). */
@@ -32,7 +31,7 @@ const reviewerAmendmentsSchema = Type.Object({
32
31
  }, {
33
32
  additionalProperties: true,
34
33
  description:
35
- "所选 lens 的 amendments 承载 candidates、逐条处置与 verdict 的完整报告(非仅 verdict 行);未选 lens 可省略。hard-stop/usage error 为 refused 时,已产出报告仍照录进所选 lens 字段。形状指引,非 schema 闸。",
34
+ "已运行 lens 的 amendments 承载 candidates、逐条处置与 verdict 的完整报告(非仅 verdict 行);显式单轴时另一轴可省略。hard-stop/usage error 为 refused 时,已产出报告仍照录对应 lens 字段。形状指引,非 schema 闸。",
36
35
  });
37
36
  // #836 r16 class 1: diagnostic is LLM/human-read narrative content — no code
38
37
  // branches on its length (src/reviewer-role.ts consumer: reviewer content is
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { appendFile, mkdir, writeFile } from "node:fs/promises";
3
3
  import { createServer, type Server, type Socket } from "node:net";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -115,7 +115,11 @@ export async function prepareRoleEnvelope(options: {
115
115
  await mkdir(request.runDirectory, { recursive: true });
116
116
 
117
117
  // Durable principal file for isAvailable / resumable settlement (public-cli).
118
- // #617 DK-4: header layout only — never host conversation/tool writeback into Pi JSONL.
118
+ // #617 DK-4: never host conversation/tool writeback into Pi JSONL.
119
+ // #959: package-owned custom entries (invocation marker, submission-closure,
120
+ // navigator attendance, no-receipt) MUST flush so parent settlement can read
121
+ // them after the headless process ends — memory-only books left extractNavigatorFact
122
+ // with no durable terminal / attendance on codex/claude/ACP parents.
119
123
  let sessionFile = options.sessionFile ?? join(request.runDirectory, "session", "session.jsonl");
120
124
  await mkdir(dirname(sessionFile), { recursive: true });
121
125
  if (request.continuation.kind !== "resume") {
@@ -135,6 +139,31 @@ export async function prepareRoleEnvelope(options: {
135
139
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
136
140
  }
137
141
  }
142
+ // Settlement reads the durable file after the turn; write must complete before
143
+ // closeRound returns. Sync-order via chained promise kept on the envelope.
144
+ // Chain itself never rejects (closeRound/dispose stay orderly). Required package
145
+ // entry flush failure arms the existing typed infrastructure-failure slot —
146
+ // never log-and-continue into accepted (#959 失败诚实).
147
+ let durableWriteChain: Promise<void> = Promise.resolve();
148
+ /** Filled by rememberInfrastructureFailure; declared later, closed over here. */
149
+ let armDurableWriteFailure: ((customType: string, diagnostic: string) => void) | undefined;
150
+ /** Append one package-owned session entry to the durable principal (fire-and-order). */
151
+ const persistPackageSessionEntry = (entry: Record<string, unknown>): void => {
152
+ const customType = typeof entry.customType === "string" ? entry.customType : "unknown";
153
+ const line = `${JSON.stringify({
154
+ ...entry,
155
+ id: typeof entry.id === "string" ? entry.id : randomUUID(),
156
+ timestamp: typeof entry.timestamp === "string" ? entry.timestamp : new Date().toISOString(),
157
+ })}\n`;
158
+ durableWriteChain = durableWriteChain.then(async () => {
159
+ try {
160
+ await appendFile(sessionFile, line, "utf8");
161
+ } catch (error) {
162
+ const diagnostic = error instanceof Error ? error.message : String(error);
163
+ armDurableWriteFailure?.(customType, diagnostic);
164
+ }
165
+ });
166
+ };
138
167
  const context: HostContext = {
139
168
  cwd: request.cwd,
140
169
  mode: "print",
@@ -155,6 +184,7 @@ export async function prepareRoleEnvelope(options: {
155
184
  const entry = { type: "custom", customType, data };
156
185
  sessionEntries.push(entry);
157
186
  customEntries.push({ customType, data });
187
+ persistPackageSessionEntry(entry);
158
188
  },
159
189
  },
160
190
  abort() {
@@ -170,6 +200,7 @@ export async function prepareRoleEnvelope(options: {
170
200
  const entry = { type: "custom_message", customType, ...payload, message: payload };
171
201
  sessionEntries.push(entry);
172
202
  customEntries.push({ customType, data: message.details ?? message.content });
203
+ persistPackageSessionEntry(entry);
173
204
  };
174
205
 
175
206
  const emit = async (event: string, value: unknown): Promise<unknown[]> => {
@@ -304,6 +335,17 @@ export async function prepareRoleEnvelope(options: {
304
335
  };
305
336
  hostAbort.abort();
306
337
  }
338
+ armDurableWriteFailure = (customType, diagnostic) => {
339
+ rememberInfrastructureFailure(
340
+ {
341
+ cause: "infrastructure",
342
+ kind: "role_infrastructure_failure",
343
+ code: "durable-session-write-failed",
344
+ customType,
345
+ },
346
+ [{ type: "text", text: `ak-role: durable session entry flush failed (${customType}): ${diagnostic}` }],
347
+ );
348
+ };
307
349
  /**
308
350
  * One non-correctable infra pathway for execute throws and pre-execution emits:
309
351
  * build fact → fill closeRound slot → arm hostAbort. Projection may follow.
@@ -500,15 +542,50 @@ export async function prepareRoleEnvelope(options: {
500
542
  let disposed = false;
501
543
  // Per-turn run/court identity lives on HostContext (request-scoped). Engine
502
544
  // axis is already a RoleHost flag — never process.env (#818 P1 / #879).
545
+
546
+ /** True once closeRound returned an infrastructure failure to the host. */
547
+ let durableFailureHandedToCloseRound = false;
548
+ /** Drain required package-owned writes; infrastructure outranks every close outcome. */
549
+ const settleDurableWrites = async (): Promise<
550
+ | { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
551
+ | undefined
552
+ > => {
553
+ await durableWriteChain;
554
+ if (infrastructureRoundFailure !== undefined) {
555
+ durableFailureHandedToCloseRound = true;
556
+ return { accepted: false as const, failure: infrastructureRoundFailure };
557
+ }
558
+ return undefined;
559
+ };
560
+
503
561
  const dispose = async (): Promise<void> => {
504
562
  if (disposed) return;
505
563
  disposed = true;
506
564
  const cleanupFailures: unknown[] = [];
565
+ // session_shutdown may still append package durable entries (navigator
566
+ // attendance when agent_settled was skipped). Drain only after those
567
+ // handlers — single terminal judgment via settleDurableWrites (#959).
507
568
  try {
508
569
  await emit("session_shutdown", {});
509
570
  } catch (error) {
510
571
  cleanupFailures.push(error);
511
572
  }
573
+ try {
574
+ // Always drain post-shutdown writes. Only throw when the host has not
575
+ // already received this infrastructure failure via closeRound.
576
+ const alreadyHanded = durableFailureHandedToCloseRound;
577
+ const durableFailure = await settleDurableWrites();
578
+ if (durableFailure !== undefined && !alreadyHanded) {
579
+ cleanupFailures.push(
580
+ Object.assign(
581
+ new Error(durableFailure.failure.diagnostic ?? "durable session entry flush failed"),
582
+ { code: durableFailure.failure.identity?.code ?? "durable-session-write-failed" },
583
+ ),
584
+ );
585
+ }
586
+ } catch (error) {
587
+ cleanupFailures.push(error);
588
+ }
512
589
  try {
513
590
  const closeAll = (server as unknown as { closeAllConnections?: () => void }).closeAllConnections;
514
591
  if (typeof closeAll === "function") closeAll.call(server);
@@ -532,8 +609,7 @@ export async function prepareRoleEnvelope(options: {
532
609
  // duplicate so structured_output + tool-call does not arm non-sole.
533
610
  if (calls.some((call) => call.toolName === terminatingToolName)) return;
534
611
  await invokeAkTool(terminatingToolName, params ?? {});
535
- }
536
-
612
+ };
537
613
  const closeRound: PreparedRoleTurn["closeRound"] = async () => {
538
614
  // Typed round boundary: hand the complete call list to the shared ledger once.
539
615
  if (calls.length > 0) {
@@ -543,6 +619,7 @@ export async function prepareRoleEnvelope(options: {
543
619
  }
544
620
  // Infrastructure failure outranks accepted closure / correctable retry (#593).
545
621
  if (infrastructureRoundFailure !== undefined) {
622
+ durableFailureHandedToCloseRound = true;
546
623
  return { accepted: false as const, failure: infrastructureRoundFailure };
547
624
  }
548
625
  let closure: { customType: string; data: unknown } | undefined;
@@ -553,6 +630,8 @@ export async function prepareRoleEnvelope(options: {
553
630
  // Pi flushes navigator attendance on agent_settled; session/prompt
554
631
  // resolution is the ACP host equivalent round boundary.
555
632
  await emit("agent_settled", {});
633
+ const durableFailure = await settleDurableWrites();
634
+ if (durableFailure !== undefined) return durableFailure;
556
635
  return { accepted: true as const };
557
636
  }
558
637
  if (rejection !== undefined) {
@@ -562,11 +641,17 @@ export async function prepareRoleEnvelope(options: {
562
641
  message: rejection.message,
563
642
  };
564
643
  rejection = undefined;
644
+ // Drain write chain before retry: a package-owned append may still fail after
645
+ // the correctable rejection was booked; infrastructure outranks retry.
646
+ const durableFailure = await settleDurableWrites();
647
+ if (durableFailure !== undefined) return durableFailure;
565
648
  return { accepted: false as const, retry };
566
649
  }
567
650
  // #836: host ended without a recorded submission is not a failure.
568
651
  // Settlement presents ledger contents; empty ledger → no_receipt.
569
652
  await emit("agent_settled", {});
653
+ const durableFailure = await settleDurableWrites();
654
+ if (durableFailure !== undefined) return durableFailure;
570
655
  return { accepted: true as const };
571
656
  };
572
657
 
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { readFileSync, writeSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import {
@@ -12,7 +13,7 @@ import {
12
13
 
13
14
  import { Value } from "typebox/value";
14
15
  import { sitianReport } from "./sitian-facade.ts";
15
- import { createSubmissionLedgerHost } from "./submission-ledger.ts";
16
+ import { createSubmissionLedgerHost, sealAcceptedSubmission } from "./submission-ledger.ts";
16
17
  import { createCollectorLedger } from "./collector-ledger.ts";
17
18
 
18
19
  import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts";
@@ -156,7 +157,7 @@ const REVIEWER_TRANSPORT_FLAGS = Object.freeze([
156
157
  Object.freeze({
157
158
  name: "ak-review-lens",
158
159
  definition: Object.freeze({
159
- description: "Caller-selected single review lens: completeness or correctness",
160
+ description: "Single review lens: completeness or correctness",
160
161
  type: "string" as const,
161
162
  }),
162
163
  }),
@@ -441,7 +442,7 @@ type ActivationRuntime = {
441
442
  };
442
443
  /** Envelope decodes Reviewer transport flags inside the activation stage. */
443
444
  decodeReviewerAdmitted(): ReviewerAdmittedInputs;
444
- /** Envelope stores live parent activation for agent_start prompt assembly. */
445
+ /** Envelope stores live Reviewer activation for agent_start prompt assembly. */
445
446
  bindReviewerParent(activation: ReviewerActivation): void;
446
447
  collector: {
447
448
  activate(context: HostContext, event: { reason: string }): Promise<void>;
@@ -1225,6 +1226,28 @@ export function createCountersignRoleRuntime(
1225
1226
  );
1226
1227
  }
1227
1228
 
1229
+ /**
1230
+ * #959: last assistant text parts as navigator prose exit.
1231
+ * Tool-call-only messages yield undefined — those already ride the tool path.
1232
+ */
1233
+ function lastAssistantProse(
1234
+ messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[],
1235
+ ): string | undefined {
1236
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1237
+ const message = messages[index];
1238
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
1239
+ const texts: string[] = [];
1240
+ for (const part of message.content) {
1241
+ if (part.type === "text" && typeof part.text === "string" && part.text.length > 0) {
1242
+ texts.push(part.text);
1243
+ }
1244
+ }
1245
+ if (texts.length === 0) continue;
1246
+ return texts.join("");
1247
+ }
1248
+ return undefined;
1249
+ }
1250
+
1228
1251
  export function createRoleRuntimeExtension(
1229
1252
  dependencies: RoleRuntimeDependencies,
1230
1253
  ): (envelopeHost: RoleEnvelopeHost) => void {
@@ -1299,10 +1322,9 @@ export function createRoleRuntimeExtension(
1299
1322
  if (settlement === undefined || attendance === undefined) return;
1300
1323
  const workContext = navigatorWorkContext;
1301
1324
  const pending = (async () => {
1302
- if (settlement.kind !== "accepted") {
1303
- await attendance.settle(settlement);
1304
- return;
1305
- }
1325
+ // ADR 0052 / #959: every settlement that starts a post-role feed host round
1326
+ // (accepted, human_decision, role_infrastructure_failure) shares one grace
1327
+ // and the same honest unavailable projection — no unbounded parallel branch.
1306
1328
  const settlePromise = attendance.settle(settlement);
1307
1329
  // Attach catch immediately so a late rejection after grace timeout cannot
1308
1330
  // surface as unhandledRejection / stale-ctx after session dispose (#675).
@@ -1557,18 +1579,12 @@ export function createRoleRuntimeExtension(
1557
1579
  pendingSubmissionNonPassByToolCallId.delete(event.toolCallId);
1558
1580
  return { details: submissionNonPass, isError: true };
1559
1581
  }
1560
- // Recommendation rides the accepted settlement record's content so the one
1561
- // mandatory last-ak_*_output extraction surfaces route/next/reason without
1562
- // a second file, grep, or nesting step. Receipt details stay contract-pure;
1563
- // unavailable/no-advice leave the settlement untouched.
1564
- if (event.isError) return;
1565
- return;
1566
1582
  });
1567
1583
  // Queue receipt delivery before `agent_settled`: that event means Pi has
1568
1584
  // already decided no queued continuation will run, so a triggerTurn there is
1569
1585
  // too late for print/json sessions. `agent_end` is the last production seam
1570
1586
  // whose queued next turn is consumed before settlement.
1571
- roleHost.on("agent_end", (event, ctx) => {
1587
+ roleHost.on("agent_end", async (event, ctx) => {
1572
1588
  const lastMessage = event.messages.at(-1);
1573
1589
  if (lastMessage?.role === "assistant"
1574
1590
  && (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted")) {
@@ -1578,6 +1594,50 @@ export function createRoleRuntimeExtension(
1578
1594
  }
1579
1595
  return;
1580
1596
  }
1597
+ const role = selectedRole ?? roleHost.getFlag(ROLE_FLAG.name);
1598
+ // #959: navigator prose exit — final assistant text is the receipt.
1599
+ // No typed-tool 催交; no JSON required. Tool path still wins when already accepted.
1600
+ if (role === "navigator" && receiptDelivery.nextAction() !== "accepted") {
1601
+ const prose = lastAssistantProse(event.messages);
1602
+ if (prose !== undefined && prose.trim() !== "") {
1603
+ const accepted = { prose };
1604
+ await sealAcceptedSubmission({
1605
+ context: ctx,
1606
+ role: "navigator",
1607
+ accepted,
1608
+ toolCallId: `navigator-prose-exit:${randomUUID()}`,
1609
+ });
1610
+ await projectClosedSubmission(
1611
+ { role: "navigator", kind: "accepted", accepted },
1612
+ ctx,
1613
+ );
1614
+ return;
1615
+ }
1616
+ // Attended with neither tool nor prose → honest no_receipt (not typed 催交).
1617
+ // Exhaust delivery budget without sending the typed prompt so facts() stays lawful.
1618
+ if (!noReceiptRecorded) {
1619
+ const runPointer = runDirectoryFromHostContext(ctx);
1620
+ if (runPointer !== undefined) {
1621
+ noReceiptRecorded = true;
1622
+ while (receiptDelivery.nextAction() === "request-delivery") {
1623
+ receiptDelivery.recordDeliveryRequest();
1624
+ }
1625
+ const facts = receiptDelivery.facts({ runPointer, attemptPointer: `current:${runPointer}` });
1626
+ envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
1627
+ try {
1628
+ sitianReport({
1629
+ level: "event",
1630
+ kind: "no-receipt-lifecycle",
1631
+ cwd: ctx.cwd,
1632
+ sessionParent: ctx.sessionManager.getSessionFile(),
1633
+ payload: facts,
1634
+ source: "role-runtime",
1635
+ });
1636
+ } catch {}
1637
+ }
1638
+ }
1639
+ return;
1640
+ }
1581
1641
  if (receiptDelivery.nextAction() === "request-delivery") {
1582
1642
  receiptDelivery.recordDeliveryRequest();
1583
1643
  // Keep the package-owned continuation off the public input lifecycle:
@@ -1657,8 +1717,8 @@ export function createRoleRuntimeExtension(
1657
1717
  process.exitCode = 1;
1658
1718
  }
1659
1719
  }
1660
- // Flush any still-pending affirmative attendance before teardown. Accepted
1661
- // grace-timeout paths normally emit on agent_settled; abort can skip that hook.
1720
+ // Flush any still-pending affirmative attendance before teardown.
1721
+ // Grace-timeout paths normally emit on agent_settled; abort can skip that hook.
1662
1722
  const presentation = pendingNavigatorPresentation;
1663
1723
  pendingNavigatorPresentation = undefined;
1664
1724
  if (presentation !== undefined) {
@@ -517,6 +517,65 @@ async function restoreState(cwd: string, runId: string, scope: SubmissionLedgerR
517
517
  };
518
518
  }
519
519
 
520
+ /** Sole HostContext-derived session parent for ledger restore/append (never process.env). */
521
+ function sessionParentFromHostContext(context: HostContext): string | undefined {
522
+ const runDirectory = runDirectoryFromHostContext(context);
523
+ if (runDirectory !== undefined) return join(runDirectory, "session", "session.jsonl");
524
+ const sessionFile = context.sessionManager.getSessionFile?.();
525
+ if (typeof sessionFile === "string" && sessionFile.length > 0) return sessionFile;
526
+ const sessionDir = context.sessionManager.getSessionDir?.();
527
+ if (typeof sessionDir === "string" && sessionDir.length > 0) {
528
+ return join(sessionDir, "session.jsonl");
529
+ }
530
+ return undefined;
531
+ }
532
+
533
+ function homeFromHostContext(context: HostContext, home?: string): string | undefined {
534
+ if (home !== undefined) return home;
535
+ const sessionParent = sessionParentFromHostContext(context);
536
+ return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
537
+ }
538
+
539
+ /**
540
+ * #959: seal one accepted submission without a model tool call.
541
+ * Same ledger row shape and priorEventId chain as the terminating-tool wrap.
542
+ * Used when a prose-exit seat (navigator) harvests the final assistant text.
543
+ */
544
+ export async function sealAcceptedSubmission(options: {
545
+ readonly context: HostContext;
546
+ readonly role: TerminalRoleName;
547
+ readonly accepted: unknown;
548
+ readonly toolCallId: string;
549
+ readonly home?: string;
550
+ }): Promise<void> {
551
+ const runId = runIdentity(options.context);
552
+ const attemptId = attemptIdentity(options.context, runId);
553
+ const sessionParent = sessionParentFromHostContext(options.context);
554
+ const home = homeFromHostContext(options.context, options.home);
555
+ const state = await restoreState(options.context.cwd, runId, {
556
+ ...(home === undefined ? {} : { home }),
557
+ ...(sessionParent === undefined ? {} : { sessionParent }),
558
+ });
559
+ const pointer = sitianReport({
560
+ level: "event",
561
+ kind: "sealed",
562
+ subject: { runId, attemptId },
563
+ ...(state.prior === undefined ? {} : { priorEventId: state.prior.identity }),
564
+ payload: {
565
+ type: "sealed",
566
+ attemptId,
567
+ toolCallId: options.toolCallId,
568
+ role: options.role,
569
+ accepted: options.accepted,
570
+ },
571
+ source: "role-runtime",
572
+ cwd: options.context.cwd,
573
+ ...(home !== undefined ? { home } : {}),
574
+ ...(sessionParent === undefined ? {} : { sessionParent }),
575
+ });
576
+ state.prior = pointer;
577
+ }
578
+
520
579
  /**
521
580
  * Submission ledger host — record only (#836).
522
581
  * Each terminating submission is appended with the role's original payload.
@@ -531,28 +590,11 @@ export function createSubmissionLedgerHost(
531
590
  options?: { home?: string },
532
591
  ): RoleHost {
533
592
  const states = new Map<string, Promise<LedgerState>>();
534
- /** Sole HostContext-derived run coordinate for restore and append (never process.env). */
535
- const sessionParentFromContext = (context: HostContext): string | undefined => {
536
- const runDirectory = runDirectoryFromHostContext(context);
537
- if (runDirectory !== undefined) {
538
- return join(runDirectory, "session", "session.jsonl");
539
- }
540
- const sessionFile = context.sessionManager.getSessionFile?.();
541
- if (typeof sessionFile === "string" && sessionFile.length > 0) return sessionFile;
542
- const sessionDir = context.sessionManager.getSessionDir?.();
543
- if (typeof sessionDir === "string" && sessionDir.length > 0) {
544
- return join(sessionDir, "session.jsonl");
545
- }
546
- return undefined;
547
- };
548
- const resolveHomeFromContext = (context: HostContext): string | undefined => {
549
- if (options?.home !== undefined) return options.home;
550
- const sessionParent = sessionParentFromContext(context);
551
- return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
552
- };
593
+ const resolveHomeFromContext = (context: HostContext): string | undefined =>
594
+ homeFromHostContext(context, options?.home);
553
595
  const stateFor = (context: HostContext, runId: string) => states.get(runId) ?? (() => {
554
596
  const home = resolveHomeFromContext(context);
555
- const sessionParent = sessionParentFromContext(context);
597
+ const sessionParent = sessionParentFromHostContext(context);
556
598
  const pending = restoreState(context.cwd, runId, {
557
599
  ...(home === undefined ? {} : { home }),
558
600
  ...(sessionParent === undefined ? {} : { sessionParent }),
@@ -562,7 +604,7 @@ export function createSubmissionLedgerHost(
562
604
  })();
563
605
  const appendFor = (state: LedgerState, context: HostContext, runId: string, attemptId: string, event: SubmissionLedgerEvent): RecordPointer => {
564
606
  const home = resolveHomeFromContext(context);
565
- const sessionParent = sessionParentFromContext(context);
607
+ const sessionParent = sessionParentFromHostContext(context);
566
608
  const pointer = sitianReport({
567
609
  level: "event",
568
610
  kind: event.type,
@@ -1,5 +0,0 @@
1
- /**
2
- * Public CLI re-export of the shared registry-owned command renderer.
3
- * Single implementation lives in src/public-command-renderer.ts.
4
- */
5
- export { isPublicCallableRole, renderPublicAkRoleCommand, } from "../public-command-renderer.js";
@@ -1,29 +0,0 @@
1
- import {
2
- PACKAGED_ROLE_REGISTRY,
3
- packagedRoleMetadata
4
- } from "./packaged-role-registry.js";
5
- const PUBLIC_CALLABLE_ROLES = new Set(
6
- PACKAGED_ROLE_REGISTRY.map((entry) => entry.role)
7
- );
8
- function isPublicCallableRole(role) {
9
- return PUBLIC_CALLABLE_ROLES.has(role);
10
- }
11
- function renderPublicAkRoleCommand(target) {
12
- if (!isPublicCallableRole(target.role)) return void 0;
13
- const role = target.role;
14
- const metadata = packagedRoleMetadata(role);
15
- if (metadata !== void 0 && "bareCommand" in metadata && metadata.bareCommand === false) {
16
- return void 0;
17
- }
18
- if (target.phase === null || target.phase === void 0) {
19
- return `ak-role ${role}`;
20
- }
21
- if (role === "coder" || role === "fixer") {
22
- return `ak-role ${role} ${target.phase}`;
23
- }
24
- return `ak-role ${role}`;
25
- }
26
- export {
27
- isPublicCallableRole,
28
- renderPublicAkRoleCommand
29
- };
@@ -1,9 +0,0 @@
1
- /**
2
- * Public CLI re-export of the shared registry-owned command renderer.
3
- * Single implementation lives in src/public-command-renderer.ts.
4
- */
5
- export {
6
- isPublicCallableRole,
7
- renderPublicAkRoleCommand,
8
- type PublicCommandTarget,
9
- } from "../public-command-renderer.ts";
@@ -1,55 +0,0 @@
1
- /**
2
- * Registry-owned public command renderer (ADR 0052 / #106).
3
- * Typed role/phase facts → public `ak-role` command text.
4
- * Model-authored command prose is never executable output.
5
- *
6
- * Low-level shared owner: Navigator attendance and the public CLI both import
7
- * this module so dist/navigator-attendance.js keeps a complete dependency closure
8
- * without duplicating renderer logic into the preparation graph or bin bundle.
9
- */
10
- import {
11
- PACKAGED_ROLE_REGISTRY,
12
- packagedRoleMetadata,
13
- type PackagedRole,
14
- } from "./packaged-role-registry.ts";
15
-
16
- export type PublicCommandPhase = "plan" | "apply" | null;
17
-
18
- export type PublicCommandTarget = {
19
- role: string;
20
- phase: PublicCommandPhase;
21
- };
22
-
23
- const PUBLIC_CALLABLE_ROLES = new Set<string>(
24
- PACKAGED_ROLE_REGISTRY.map((entry) => entry.role),
25
- );
26
-
27
- export function isPublicCallableRole(role: string): role is PackagedRole {
28
- return PUBLIC_CALLABLE_ROLES.has(role);
29
- }
30
-
31
- /**
32
- * Render one public ak-role invocation from typed next-role facts.
33
- * Returns undefined when the role is not callable or role/phase cannot supply
34
- * all required admission values for a truthful command.
35
- */
36
- export function renderPublicAkRoleCommand(
37
- target: PublicCommandTarget,
38
- ): string | undefined {
39
- if (!isPublicCallableRole(target.role)) return undefined;
40
- const role = target.role as PackagedRole;
41
- const metadata = packagedRoleMetadata(role);
42
- // A role whose admission requires typed values outside role/phase has no
43
- // truthful bare command. Navigator may still recommend the typed direction.
44
- if (metadata !== undefined && "bareCommand" in metadata && metadata.bareCommand === false) {
45
- return undefined;
46
- }
47
- if (target.phase === null || target.phase === undefined) {
48
- return `ak-role ${role}`;
49
- }
50
- // Coder/Fixer expose plan|apply; other roles have no public phase selector.
51
- if (role === "coder" || role === "fixer") {
52
- return `ak-role ${role} ${target.phase}`;
53
- }
54
- return `ak-role ${role}`;
55
- }