@akagilnc/pi-workflow-roles 0.1.5015 → 0.1.5063

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 (95) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -8
  4. package/dist/acp-host/production-host.js +525 -978
  5. package/dist/headless-host/description.js +0 -2
  6. package/dist/headless-host/production-host.js +528 -1014
  7. package/dist/host-descriptions.js +2 -3
  8. package/dist/migrate-book-topology.js +7 -12
  9. package/dist/packaged-role-registry.js +20 -18
  10. package/dist/pi/role-turn-host.js +3 -27
  11. package/dist/public-cli/cli.js +23 -2
  12. package/dist/public-cli/countersign-run.js +13 -8
  13. package/dist/public-cli/instruction-seat-run.js +75 -107
  14. package/dist/public-cli/invocation.js +13 -32
  15. package/dist/public-cli/machine-method-skills.js +150 -0
  16. package/dist/public-cli/main.js +549 -679
  17. package/dist/public-cli/post-admission.js +10 -5
  18. package/dist/public-cli/registry.js +2 -1
  19. package/dist/public-cli/run-lifecycle.js +12 -15
  20. package/dist/public-cli/seat-ticket-binding.js +5 -8
  21. package/dist/public-cli/settlement.js +12 -99
  22. package/dist/public-cli/turn-request.js +16 -8
  23. package/dist/public-role-summons.js +8 -3
  24. package/package.json +5 -1
  25. package/resources/countersign-ticket-issue.md +5 -0
  26. package/resources/diarist-collect.md +1 -0
  27. package/resources/secretariat-ticket-identity.md +15 -0
  28. package/scripts/build-package.mjs +1 -6
  29. package/src/headless-host/description.ts +0 -3
  30. package/src/headless-host/role-turn-host.ts +2 -23
  31. package/src/host-contracts.ts +2 -23
  32. package/src/host-descriptions.ts +2 -3
  33. package/src/packaged-role-registry.ts +17 -16
  34. package/src/pi/adapter.ts +0 -27
  35. package/src/pi/role-turn-host.ts +3 -31
  36. package/src/public-cli/cli.ts +24 -2
  37. package/src/public-cli/countersign-run.ts +17 -10
  38. package/src/public-cli/instruction-seat-run.ts +75 -105
  39. package/src/public-cli/invocation.ts +10 -38
  40. package/src/public-cli/machine-method-skills.ts +153 -0
  41. package/src/public-cli/post-admission.ts +12 -6
  42. package/src/public-cli/registry.ts +1 -0
  43. package/src/public-cli/run-lifecycle.ts +12 -13
  44. package/src/public-cli/seat-ticket-binding.ts +5 -8
  45. package/src/public-cli/settlement.ts +6 -138
  46. package/src/public-cli/turn-request.ts +24 -16
  47. package/src/public-role-summons.ts +16 -3
  48. package/src/reviewer-role.ts +1 -12
  49. package/src/role-envelope.ts +0 -13
  50. package/src/role-runtime-dependencies.ts +0 -13
  51. package/src/role-runtime.ts +4 -56
  52. package/src/worker-role.ts +1 -81
  53. package/dist/method-host-plugin/.claude-plugin/plugin.json +0 -5
  54. package/dist/method-host-plugin/skills/ak-cross-m-review/CONTEXT.md +0 -48
  55. package/dist/method-host-plugin/skills/ak-cross-m-review/LICENSE +0 -21
  56. package/dist/method-host-plugin/skills/ak-cross-m-review/SKILL.md +0 -170
  57. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-completeness.md +0 -118
  58. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-reviewer.md +0 -128
  59. package/dist/method-host-plugin/skills/ak-cross-m-review/provenance.json +0 -41
  60. package/dist/method-host-plugin/skills/diagnosing-bugs/SKILL.md +0 -134
  61. package/dist/method-host-plugin/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  62. package/dist/method-host-plugin/skills/diagnosing-bugs/provenance.json +0 -31
  63. package/dist/method-host-plugin/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -41
  64. package/dist/method-host-plugin/skills/resolving-merge-conflicts/SKILL.md +0 -14
  65. package/dist/method-host-plugin/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  66. package/dist/method-host-plugin/skills/resolving-merge-conflicts/provenance.json +0 -26
  67. package/dist/method-host-plugin/skills/tdd/SKILL.md +0 -38
  68. package/dist/method-host-plugin/skills/tdd/agents/openai.yaml +0 -3
  69. package/dist/method-host-plugin/skills/tdd/mocking.md +0 -59
  70. package/dist/method-host-plugin/skills/tdd/provenance.json +0 -36
  71. package/dist/method-host-plugin/skills/tdd/tests.md +0 -77
  72. package/dist/package-resources/method-skill.js +0 -259
  73. package/resources/method-host-plugin/.claude-plugin/plugin.json +0 -5
  74. package/resources/methods/ak-cross-m-review/CONTEXT.md +0 -48
  75. package/resources/methods/ak-cross-m-review/LICENSE +0 -21
  76. package/resources/methods/ak-cross-m-review/SKILL.md +0 -170
  77. package/resources/methods/ak-cross-m-review/prompts/cmr-completeness.md +0 -118
  78. package/resources/methods/ak-cross-m-review/prompts/cmr-reviewer.md +0 -128
  79. package/resources/methods/ak-cross-m-review/provenance.json +0 -41
  80. package/resources/methods/diagnosing-bugs/SKILL.md +0 -134
  81. package/resources/methods/diagnosing-bugs/agents/openai.yaml +0 -3
  82. package/resources/methods/diagnosing-bugs/provenance.json +0 -31
  83. package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -41
  84. package/resources/methods/resolving-merge-conflicts/SKILL.md +0 -14
  85. package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +0 -3
  86. package/resources/methods/resolving-merge-conflicts/provenance.json +0 -26
  87. package/resources/methods/tdd/SKILL.md +0 -38
  88. package/resources/methods/tdd/agents/openai.yaml +0 -3
  89. package/resources/methods/tdd/mocking.md +0 -59
  90. package/resources/methods/tdd/provenance.json +0 -36
  91. package/resources/methods/tdd/tests.md +0 -77
  92. package/src/canonical-skill-binding.ts +0 -118
  93. package/src/host-native-method.ts +0 -185
  94. package/src/package-resources/method-skill-binding.ts +0 -60
  95. package/src/package-resources/method-skill.ts +0 -363
@@ -10,15 +10,10 @@ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contrac
10
10
  import { isSafePositiveTicketNumber, readBoardTicketNumber } from "../run-ticket-number.ts";
11
11
  import { NotarySourceRunError, resolveNotarySourceRunLocator } from "../notary-source-run.ts";
12
12
  import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts";
13
- import {
14
- loadPackagedMethodSkillMaterial,
15
- type PackagedMethodSkillMaterial,
16
- } from "../package-resources/method-skill.ts";
17
13
  import type { PackagedRole } from "../packaged-role-registry.ts";
18
14
  import {
19
15
  packagedAdmitsCountersign,
20
16
  packagedBindsBoardTicket,
21
- packagedMethodLoadFailureCause,
22
17
  packagedRebindSourceOnResume,
23
18
  packagedRoleMetadata,
24
19
  } from "../packaged-role-registry.ts";
@@ -42,7 +37,6 @@ import {
42
37
  import {
43
38
  presentControlledFailure,
44
39
  prepareSummonsResumeMaterials,
45
- resolveResumeMethodMaterialAdapters,
46
40
  roleTurnOptions,
47
41
  runPostAdmissionOneShot,
48
42
  runPostAdmissionResumable,
@@ -67,6 +61,7 @@ import {
67
61
  trySettlePublicSeat,
68
62
  } from "./settlement.ts";
69
63
  import type { CliIo } from "./cli-io.ts";
64
+ import { warnMissingMethodSkills } from "./machine-method-skills.ts";
70
65
  import {
71
66
  formatTerminalResult,
72
67
  isLawfulTypedTerminalOutcome,
@@ -74,8 +69,8 @@ import {
74
69
  } from "./terminal.ts";
75
70
  import {
76
71
  admittedSeatTurnDetails,
77
- packagedSettleSkill,
78
72
  projectRoleTurnRequest,
73
+ requiredMethodSkills,
79
74
  type RoleTurnRequestProjectionOptions,
80
75
  } from "./turn-request.ts";
81
76
  import {
@@ -110,7 +105,7 @@ export function buildInstructionSeatTurnRequest(
110
105
  ): RoleTurnRequest {
111
106
  return projectRoleTurnRequest(
112
107
  admitted,
113
- admittedSeatTurnDetails(admitted, options.packageRoot),
108
+ admittedSeatTurnDetails(admitted, options.home, options.host),
114
109
  options,
115
110
  );
116
111
  }
@@ -160,13 +155,12 @@ async function bindAndRelocateDiarist(
160
155
  function seatAdapters(
161
156
  admitted: AdmittedRoleInvocation,
162
157
  env: InstructionSeatRunEnv,
163
- material?: PackagedMethodSkillMaterial,
164
158
  ): PostAdmissionAdapters<AdmittedRoleInvocation> {
165
159
  const record = roleRecord(admitted.role);
166
160
  const present = "presentSettled" in record ? record.presentSettled : "default";
167
161
  return {
168
162
  trySettle: (seat, authority, scope) =>
169
- trySettlePublicSeat(seat, authority, scope, material, env.packageRoot),
163
+ trySettlePublicSeat(seat, authority, scope),
170
164
  ...(present === "always" ? { shouldPresentSettled: () => true } : {}),
171
165
  ...(present === "typed"
172
166
  ? { shouldPresentSettled: (terminal: TerminalResult) => isLawfulTypedTerminalOutcome(terminal.roleOutcome) }
@@ -233,23 +227,8 @@ async function dispatchAdmitted(
233
227
  io: CliIo,
234
228
  ): Promise<SeatRunResult> {
235
229
  const record = roleRecord(admitted.role);
236
- let material: PackagedMethodSkillMaterial | undefined;
237
- const skill = packagedSettleSkill(admitted);
238
- if (skill !== undefined) {
239
- try {
240
- material = await loadPackagedMethodSkillMaterial(env.packageRoot, skill);
241
- } catch (error) {
242
- const knownCause = packagedMethodLoadFailureCause(admitted.role);
243
- return await presentControlledFailure(admitted, {
244
- timedOut: false,
245
- code: null,
246
- stderr: "",
247
- thrown: error,
248
- ...(knownCause === undefined ? {} : { knownCause }),
249
- }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
250
- }
251
- }
252
- const adapters = seatAdapters(admitted, env, material);
230
+ await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
231
+ const adapters = seatAdapters(admitted, env);
253
232
  const execute = async (activeEnv: InstructionSeatRunEnv): Promise<SeatRunResult> => {
254
233
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
255
234
  if (auto) {
@@ -388,9 +367,7 @@ async function runCountersignBody(
388
367
  projectRoot: resolve(parsed.project ?? env.cwd),
389
368
  role: "countersign",
390
369
  parentRunPath: gateParentRunPath,
391
- ...(env.boundTicketNumber === undefined
392
- ? {}
393
- : { ticketNumber: env.boundTicketNumber }),
370
+ ...(isSafePositiveTicketNumber(env.boundTicketNumber) ? { ticketNumber: env.boundTicketNumber } : {}),
394
371
  freshSummons: env.freshSummons,
395
372
  summons: {
396
373
  sourceRunPath: gateParentRunPath,
@@ -430,15 +407,17 @@ async function runCountersignBody(
430
407
  let typedTicket: number | undefined;
431
408
  let typedCourtTicketNumbers: readonly number[] | undefined;
432
409
  let identityDiaristRan = false;
433
- let unboundDiaristRunId: string | undefined;
410
+ let childDiaristRunId: string | undefined;
411
+ let pausedDiaristTerminal: TerminalResult | undefined;
434
412
 
435
- if (env.runCourtDiaristStation === undefined) {
413
+ if (gateParentRunPath === undefined && env.runCourtDiaristStation === undefined) {
436
414
  let outcome: Awaited<ReturnType<typeof invokeCourtDiarist>>;
437
415
  try {
438
416
  outcome = await invokeCourtDiarist({
439
417
  instruction: parsed.instruction ?? "",
440
418
  projectRoot: admitted.projectRoot,
441
419
  failureLabel: "unbound summons",
420
+ correlationId: admitted.runId,
442
421
  ...(env.boundTicketNumber === undefined ? {} : { boundTicketNumber: env.boundTicketNumber }),
443
422
  }, env, io);
444
423
  } catch (error) {
@@ -454,17 +433,16 @@ async function runCountersignBody(
454
433
  }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
455
434
  }
456
435
  identityDiaristRan = true;
436
+ if (outcome.identity.kind === "escalate") pausedDiaristTerminal = outcome.terminal;
457
437
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
458
- unboundDiaristRunId = outcome.admitted.runId;
438
+ childDiaristRunId = outcome.admitted.runId;
459
439
  }
460
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
461
- const diagnostic = outcome.identity.kind === "escalate"
462
- ? outcome.identity.diagnostic
463
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
440
+ if (outcome.failedWithoutEscalate !== undefined) {
441
+ const diagnostic = outcome.failedWithoutEscalate.diagnostic;
464
442
  await materializeAdmission(
465
443
  isSafePositiveTicketNumber(env.boundTicketNumber) ? env.boundTicketNumber : undefined,
466
444
  );
467
- if (unboundDiaristRunId !== undefined) await recordChildDiaristRun(admitted, unboundDiaristRunId);
445
+ if (childDiaristRunId !== undefined) await recordChildDiaristRun(admitted, childDiaristRunId);
468
446
  await markRunAdmitted(admitted, env.principalAuthority);
469
447
  return await presentControlledFailure(admitted, {
470
448
  timedOut: false,
@@ -474,19 +452,20 @@ async function runCountersignBody(
474
452
  }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
475
453
  }
476
454
  if (outcome.identity.kind === "ticket") {
477
- typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
478
- ? env.boundTicketNumber
479
- : outcome.identity.ticketNumber;
455
+ typedTicket = outcome.identity.ticketNumber;
480
456
  typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
481
- } else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
482
- typedTicket = env.boundTicketNumber;
483
457
  }
484
458
  }
485
459
 
486
460
  await materializeAdmission(typedTicket);
487
461
  await markRunAdmitted(admitted, env.principalAuthority);
488
- if (unboundDiaristRunId !== undefined) {
489
- await recordChildDiaristRun(admitted, unboundDiaristRunId);
462
+ if (childDiaristRunId !== undefined) {
463
+ await recordChildDiaristRun(admitted, childDiaristRunId);
464
+ }
465
+ if (pausedDiaristTerminal !== undefined) {
466
+ // The child already submitted its own pause; no parent turn exists yet.
467
+ io.stdout(formatTerminalResult(pausedDiaristTerminal));
468
+ return { exitCode: 0, admitted, terminal: pausedDiaristTerminal };
490
469
  }
491
470
  if (gateParentRunPath !== undefined) {
492
471
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
@@ -540,7 +519,11 @@ async function runCountersignBody(
540
519
  beforeDispatch: async (admittedSeat, lease) => {
541
520
  if (!packagedAdmitsCountersign(admittedSeat.role)) return;
542
521
  if (!identityDiaristRan || typedTicket !== undefined) {
543
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
522
+ const refresh = await runCountersignCourtDiaristStation(admittedSeat, env, io);
523
+ if (refresh !== undefined) {
524
+ io.stdout(formatTerminalResult(refresh.terminal));
525
+ return refresh.terminal;
526
+ }
544
527
  }
545
528
  await relocateAdmittedRunToTicket(admittedSeat, env.principalAuthority, lease);
546
529
  Object.assign(turnRequest, buildInstructionSeatTurnRequest(admittedSeat, turnProjection));
@@ -628,7 +611,6 @@ export async function runPublicInstructionSeat(
628
611
  projectRoot,
629
612
  role,
630
613
  parentRunPath: sourceDirectory,
631
- ...(auditorTicket === undefined ? {} : { ticketNumber: auditorTicket }),
632
614
  freshSummons: env.freshSummons,
633
615
  summons,
634
616
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -657,7 +639,6 @@ export async function runPublicInstructionSeat(
657
639
  projectRoot,
658
640
  role,
659
641
  parentRunPath,
660
- ...(env.boundTicketNumber === undefined ? {} : { ticketNumber: env.boundTicketNumber }),
661
642
  freshSummons: env.freshSummons,
662
643
  summons,
663
644
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -686,7 +667,6 @@ export async function runPublicInstructionSeat(
686
667
  throw error;
687
668
  }
688
669
  const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
689
- const knownTicket = (await readBoardTicketNumber(source.runDirectory)) ?? env.boundTicketNumber;
690
670
  const summons: SameTicketSummonsMaterials = {
691
671
  sourceRunPath: source.runDirectory,
692
672
  sourceRun: source,
@@ -697,7 +677,6 @@ export async function runPublicInstructionSeat(
697
677
  projectRoot,
698
678
  role,
699
679
  parentRunPath: source.runDirectory,
700
- ...(knownTicket === undefined ? {} : { ticketNumber: knownTicket }),
701
680
  freshSummons: env.freshSummons,
702
681
  summons,
703
682
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -737,52 +716,33 @@ export async function runPublicInstructionSeat(
737
716
 
738
717
  const runAdmitted = async (): Promise<SeatRunResult> => {
739
718
  await markRunAdmitted(admitted, env.principalAuthority);
740
- if (record.sameParent === "court-diarist") {
741
- const { invokeCourtDiarist } = await import("./countersign-run.ts");
742
- const outcome = await invokeCourtDiarist({
743
- instruction: parsed.instruction ?? "",
744
- projectRoot: admitted.projectRoot,
745
- failureLabel: "secretariat unbound summons",
746
- attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
747
- correlationId: admitted.runId,
748
- }, {
749
- cwd: env.cwd,
750
- home: env.home,
751
- agentDir: env.agentDir,
752
- packageRoot: env.packageRoot,
753
- ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
754
- ...(env.signal === undefined ? {} : { signal: env.signal }),
755
- ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
756
- }, io);
719
+ if (role === "secretariat") {
720
+ let outcome: Awaited<ReturnType<typeof invokeCourtDiarist>>;
721
+ try {
722
+ outcome = await invokeCourtDiarist({
723
+ instruction: parsed.instruction ?? "",
724
+ projectRoot: admitted.projectRoot,
725
+ failureLabel: "secretariat unbound summons",
726
+ attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
727
+ correlationId: admitted.runId,
728
+ }, env, io);
729
+ } catch (error) {
730
+ return await presentControlledFailure(admitted, {
731
+ timedOut: false, code: null, stderr: "", thrown: error,
732
+ }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
733
+ }
757
734
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
758
735
  await recordChildDiaristRun(admitted, outcome.admitted.runId);
759
736
  }
760
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
761
- const diagnostic = outcome.identity.kind === "escalate"
762
- ? outcome.identity.diagnostic
763
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
737
+ if (outcome.failedWithoutEscalate !== undefined) {
764
738
  return await presentControlledFailure(admitted, {
765
- timedOut: false,
766
- code: null,
767
- stderr: "",
768
- thrown: new Error(diagnostic),
739
+ timedOut: false, code: null, stderr: "",
740
+ thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
769
741
  }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
770
742
  }
771
- if (outcome.identity.kind === "ticket") {
772
- try {
773
- if (outcome.admitted === undefined) throw new Error("court diarist ticket has no admitted run");
774
- await bindAdmittedTicketNumber(outcome.admitted, outcome.identity.ticketNumber);
775
- await relocateAdmittedRunToTicket(outcome.admitted, env.principalAuthority);
776
- await bindAdmittedTicketNumber(admitted, outcome.identity.ticketNumber);
777
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
778
- } catch (error) {
779
- return await presentControlledFailure(admitted, {
780
- timedOut: false,
781
- code: null,
782
- stderr: "",
783
- thrown: error,
784
- }, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
785
- }
743
+ if (outcome.identity.kind === "escalate" && outcome.terminal !== undefined) {
744
+ io.stdout(formatTerminalResult(outcome.terminal));
745
+ return { exitCode: 0, admitted, terminal: outcome.terminal };
786
746
  }
787
747
  }
788
748
  return dispatchAdmitted(admitted, env, io);
@@ -837,22 +797,32 @@ export async function runPublicInstructionSeatResume(
837
797
  adapters: {
838
798
  trySettle: async () => undefined,
839
799
  },
840
- afterAdmittedLoad: (admitted) => {
841
- const skill = packagedSettleSkill(admitted);
842
- if (skill === undefined) {
843
- return Promise.resolve({ kind: "continue" as const, adapters: seatAdapters(admitted, env) });
844
- }
845
- const knownCause = packagedMethodLoadFailureCause(admitted.role);
846
- return resolveResumeMethodMaterialAdapters({
847
- admitted,
848
- authority: env.principalAuthority,
849
- io,
850
- loadMaterial: () => loadPackagedMethodSkillMaterial(env.packageRoot, skill),
851
- adaptersWith: (material) => seatAdapters(admitted, env, material),
852
- emptyAdapters: seatAdapters(admitted, env),
853
- ...(knownCause === undefined ? {} : { knownCause }),
854
- });
800
+ afterAdmittedLoad: async (admitted) => {
801
+ await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
802
+ return { kind: "continue" as const, adapters: seatAdapters(admitted, env) };
855
803
  },
856
804
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
857
805
  });
858
806
  }
807
+
808
+ /** Continue the parent once any escalated child has submitted. */
809
+ export async function continueParentAfterChild(
810
+ parentRunId: string,
811
+ child: AdmittedRoleInvocation,
812
+ env: InstructionSeatRunEnv,
813
+ io: CliIo,
814
+ ): Promise<SeatRunResult> {
815
+ const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority, true);
816
+ if (loaded.run.state !== "admitted") {
817
+ return runPublicInstructionSeatResume({ runId: parentRunId }, env, io);
818
+ }
819
+ const admitted = loaded.admitted;
820
+ if (admitted.role === "countersign" && admitted.ticketNumber !== undefined) {
821
+ const refresh = await runCountersignCourtDiaristStation(admitted, env, io, child.ticketNumber);
822
+ if (refresh !== undefined) {
823
+ io.stdout(formatTerminalResult(refresh.terminal));
824
+ return { exitCode: 0, admitted, terminal: refresh.terminal };
825
+ }
826
+ }
827
+ return dispatchAdmitted(admitted, env, io);
828
+ }
@@ -27,6 +27,7 @@ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
27
27
  import {
28
28
  ensureRoleRunDirectory,
29
29
  ensureRoleRunPlacement,
30
+ listBookRunDirectories,
30
31
  roleRunArtifactsDirectory,
31
32
  roleRunPlacement,
32
33
  type RoleRunSubject,
@@ -539,19 +540,13 @@ export async function persistAdmittedSourceRunPath(
539
540
  /**
540
541
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
541
542
  * object and both durable pages (invocation.json + admitted-request.json).
542
- * Used by known-ticket reuse when admission was unbound (#635 / #709).
543
- * Never clears an existing binding.
543
+ * A later typed role receipt replaces an earlier assertion (#1025).
544
544
  */
545
545
  export async function bindAdmittedTicketNumber(
546
546
  admitted: AdmittedRoleInvocation,
547
547
  ticketNumber: number,
548
548
  ): Promise<void> {
549
- if (admitted.ticketNumber !== undefined) {
550
- if (admitted.ticketNumber === ticketNumber) return;
551
- throw new Error(
552
- `bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`,
553
- );
554
- }
549
+ if (admitted.ticketNumber === ticketNumber) return;
555
550
  await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
556
551
  (admitted as { ticketNumber?: number }).ticketNumber = ticketNumber;
557
552
  }
@@ -642,7 +637,7 @@ export async function bindCourtTicketNumbersOnAdmitted(
642
637
  admitted.courtTicketNumbers = frozen;
643
638
  }
644
639
 
645
- /** Move a settled first-entry run from unbound to its asserted ticket directory. */
640
+ /** File a role run under its latest typed ticket identity. */
646
641
  export async function relocateAdmittedRunToTicket(
647
642
  admitted: AdmittedRoleInvocation,
648
643
  authority: DurablePrincipalAuthority,
@@ -668,6 +663,8 @@ export async function relocateAdmittedRunToTicket(
668
663
  for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
669
664
  if (typeof childRunId !== "string") continue;
670
665
  const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
666
+ // A child that already filed under a ticket keeps its own assertion.
667
+ if (!existsSync(childDirectory)) continue;
671
668
  const childTarget = roleRunPlacement(ledgerHome, {
672
669
  bookKey: admitted.bookKey,
673
670
  subject: { ticketNumber: admitted.ticketNumber },
@@ -675,7 +672,6 @@ export async function relocateAdmittedRunToTicket(
675
672
  role: "diarist",
676
673
  });
677
674
  authority.seal(childTarget);
678
- if (!existsSync(childDirectory) && existsSync(childTarget.runDirectory)) continue;
679
675
  await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
680
676
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
681
677
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
@@ -683,14 +679,13 @@ export async function relocateAdmittedRunToTicket(
683
679
  }
684
680
  }
685
681
 
686
- // An identity diarist can file before its caller obtains a ticket. Both its
687
- // own bind and the caller's later bind use this existing relocation seam.
682
+ // The first ticket assignment moves only an unbound diarist's own diary.
688
683
  if (admitted.role === "diarist") {
689
684
  await rehomeUnboundTicketProvenance(oldRunDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
690
685
  }
691
686
 
692
- // Rename commits the run placement. Diary assignment above is an idempotent
693
- // Sitian append, not part of an atomic transaction with this directory move.
687
+ // Rename commits the run placement. Diary assignment above is a Sitian append
688
+ // outside the atomic directory move.
694
689
  // Persisted paths are resolved from typed run identity on read.
695
690
  await rename(oldRunDirectory, target.runDirectory);
696
691
 
@@ -752,34 +747,11 @@ export async function bindTicketNumberOnRunDirectory(
752
747
  "bindTicketNumberOnRunDirectory",
753
748
  );
754
749
  const admittedPath = join(runDirectory, "admitted-request.json");
755
- const invocationPath = join(runDirectory, "invocation.json");
756
750
  const admitted = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
757
751
  string,
758
752
  unknown
759
753
  >;
760
- const existing = admitted.ticketNumber;
761
- if (typeof existing === "number") {
762
- if (existing === ticketNumber) return;
763
- throw new Error(
764
- `bindTicketNumberOnRunDirectory refuses to replace existing ticket #${existing} with #${ticketNumber}`,
765
- );
766
- }
767
- // Conflict guard before any write: crash window of bindAdmittedTicketNumber
768
- // can leave invocation bound while admitted-request is still unbound — refuse
769
- // silent rebind. Read-before-merge; never check the page just overwritten.
770
- if (existsSync(invocationPath)) {
771
- const invocation = JSON.parse(
772
- await readFile(invocationPath, "utf8"),
773
- ) as Record<string, unknown>;
774
- if (
775
- typeof invocation.ticketNumber === "number" &&
776
- invocation.ticketNumber !== ticketNumber
777
- ) {
778
- throw new Error(
779
- `bindTicketNumberOnRunDirectory refuses to replace invocation ticket #${invocation.ticketNumber} with #${ticketNumber}`,
780
- );
781
- }
782
- }
754
+ if (admitted.ticketNumber === ticketNumber) return;
783
755
  await writeFile(
784
756
  admittedPath,
785
757
  `${JSON.stringify({ ...admitted, ticketNumber }, null, 2)}\n`,
@@ -0,0 +1,153 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { lstat, mkdir, symlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { HEADLESS_HOST_DESCRIPTIONS, HOST_DESCRIPTIONS } from "../host-descriptions.ts";
6
+ import { packagedMethodSkillNames } from "../packaged-role-registry.ts";
7
+
8
+ /** Install source only. Which names are required comes from the role registry. */
9
+ const SKILL_SOURCE: Readonly<Record<string, string>> = {
10
+ tdd: "mattpocock/skills",
11
+ "diagnosing-bugs": "mattpocock/skills",
12
+ "resolving-merge-conflicts": "mattpocock/skills",
13
+ "ak-cross-m-review": "Akagilnc/ak-cross-m-review",
14
+ };
15
+
16
+ function skillRoot(home: string): string {
17
+ return join(home, ".agents/skills");
18
+ }
19
+
20
+ export function installedMethodSkillPath(home: string, name: string): string | undefined {
21
+ return installedSkillPath(skillRoot(home), name);
22
+ }
23
+
24
+ function installedSkillPath(root: string, name: string): string | undefined {
25
+ const path = join(root, name, "SKILL.md");
26
+ try {
27
+ if (!statSync(path).isFile()) return undefined;
28
+ accessSync(path, constants.R_OK);
29
+ return path;
30
+ } catch (error) {
31
+ const code = (error as NodeJS.ErrnoException).code;
32
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "EPERM" || code === "ELOOP") return undefined;
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ function linkedSkillRoot(home: string, host: string | undefined): string | undefined {
38
+ if (host === "claude" || host === "claude-code") {
39
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"), "skills");
40
+ }
41
+ if (host === "hermes") return join(home, ".hermes/skills");
42
+ return undefined;
43
+ }
44
+
45
+ async function occupied(path: string): Promise<boolean> {
46
+ try {
47
+ await lstat(path);
48
+ return true;
49
+ } catch (error) {
50
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function warnOccupied(path: string, stdout: (text: string) => void): void {
56
+ stdout(`Warning: ${path} is already occupied; left untouched.\n`);
57
+ }
58
+
59
+ export async function warnMissingMethodSkills(
60
+ home: string,
61
+ host: string | undefined,
62
+ role: string,
63
+ skills: readonly string[],
64
+ stdout: (text: string) => void,
65
+ ): Promise<void> {
66
+ const missing: string[] = [];
67
+ const linkedRoot = linkedSkillRoot(home, host);
68
+ for (const skill of skills) {
69
+ if (installedMethodSkillPath(home, skill) === undefined ||
70
+ (linkedRoot !== undefined && installedSkillPath(linkedRoot, skill) === undefined)) missing.push(skill);
71
+ }
72
+ for (const skill of missing) {
73
+ stdout(`Warning: required machine Skill "${skill}" is missing for ${role}; its method guidance may be unavailable and reduce work quality. Run \`ak-role setup\` to install required Skills.\n`);
74
+ }
75
+ }
76
+
77
+ function runSkillsCli(args: readonly string[], home: string): boolean {
78
+ const result = spawnSync("npx", ["--yes", "skills", ...args], {
79
+ stdio: "inherit",
80
+ env: { ...process.env, HOME: home },
81
+ });
82
+ if (result.error !== undefined) throw result.error;
83
+ return result.status === 0;
84
+ }
85
+
86
+ function addSkills(source: string, skills: readonly string[], home: string): boolean {
87
+ if (skills.length === 0) return true;
88
+ return runSkillsCli([
89
+ "add", source,
90
+ ...skills.flatMap((skill) => ["--skill", skill]),
91
+ "--agent", "codex", "--global", "--yes",
92
+ ], home);
93
+ }
94
+
95
+ function updateRequiredSkills(home: string): boolean {
96
+ const names = packagedMethodSkillNames();
97
+ if (names.length === 0) return true;
98
+ return runSkillsCli(["update", "-g", "-y", ...names], home);
99
+ }
100
+
101
+ /** Explicit user-run setup; delegates acquisition/install to the ecosystem Skills CLI. */
102
+ export async function runMachineSkillSetup(home: string, stdout: (text: string) => void): Promise<number> {
103
+ const canonical = skillRoot(home);
104
+ const missingBySource = new Map<string, string[]>();
105
+ let unsourced = false;
106
+ for (const name of packagedMethodSkillNames()) {
107
+ const path = join(canonical, name);
108
+ if (await occupied(path)) {
109
+ warnOccupied(path, stdout);
110
+ continue;
111
+ }
112
+ const source = SKILL_SOURCE[name];
113
+ if (source === undefined) {
114
+ unsourced = true;
115
+ stdout(`Warning: required machine Skill "${name}" has no install source; setup did not install it.\n`);
116
+ continue;
117
+ }
118
+ const missing = missingBySource.get(source) ?? [];
119
+ missing.push(name);
120
+ missingBySource.set(source, missing);
121
+ }
122
+ for (const [source, missing] of missingBySource) {
123
+ if (!addSkills(source, missing, home)) return 1;
124
+ }
125
+ if (unsourced) return 1;
126
+
127
+ for (const host of ["claude-code", "hermes"] as const) {
128
+ const root = linkedSkillRoot(home, host)!;
129
+ const binaryFromHome = host === "claude-code"
130
+ ? HEADLESS_HOST_DESCRIPTIONS.claude!.binaryFromHome
131
+ : HOST_DESCRIPTIONS.hermes!.binaryFromHome;
132
+ try {
133
+ accessSync(join(home, ...binaryFromHome), constants.X_OK);
134
+ } catch (error) {
135
+ if (["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) continue;
136
+ throw error;
137
+ }
138
+ for (const name of packagedMethodSkillNames()) {
139
+ const target = join(canonical, name);
140
+ if (installedMethodSkillPath(home, name) === undefined) continue;
141
+ const link = join(root, name);
142
+ if (await occupied(link)) {
143
+ warnOccupied(link, stdout);
144
+ continue;
145
+ }
146
+ await mkdir(root, { recursive: true });
147
+ await symlink(target, link, "dir");
148
+ }
149
+ }
150
+ if (!updateRequiredSkills(home)) return 1;
151
+ stdout("Machine method Skills setup finished. Existing same-name Skills were left untouched.\n");
152
+ return 0;
153
+ }
@@ -223,8 +223,9 @@ function withOnceSuccessfulBeforeDispatch<
223
223
  ...adapters,
224
224
  beforeDispatch: async (admitted, lease) => {
225
225
  if (succeeded) return;
226
- await hook(admitted, lease);
226
+ const result = await hook(admitted, lease);
227
227
  succeeded = true;
228
+ return result;
228
229
  },
229
230
  };
230
231
  }
@@ -352,7 +353,8 @@ export type PostAdmissionAdapters<
352
353
  result: RoleTurnResult;
353
354
  sessionFile: string;
354
355
  }) => Promise<RoleTurnKnownFailure | undefined>;
355
- beforeDispatch?: (admitted: A, lease?: RunWriterLease) => Promise<void> | void;
356
+ /** A child office may pause this run with its unchanged terminal before a host turn starts. */
357
+ beforeDispatch?: (admitted: A, lease?: RunWriterLease) => Promise<T | void> | T | void;
356
358
  /**
357
359
  * After the host turn settles and before any held writer lease is released.
358
360
  * Post-turn bind/relocate (e.g. Diarist first assert) must run here so a
@@ -709,9 +711,8 @@ export async function dispatchPostAdmissionTurn<
709
711
  };
710
712
  }
711
713
  try {
712
- // #858: an unbound seat may assert its ticket on the existing receipt.
713
- // Read the original accepted payload; do not rewrite it, infer from prose,
714
- // or reject missing/malformed declarations. An existing binding wins.
714
+ // #858: the first typed assertion files an unbound run. Later receipts
715
+ // remain untouched; this seam does not adjudicate a ticket change.
715
716
  if (admitted.ticketNumber === undefined) {
716
717
  const rows = await readRecordedSubmissionRows(
717
718
  admitted.projectRoot,
@@ -825,7 +826,10 @@ export async function dispatchPostAdmissionTurn<
825
826
  // (#840 父子不层叠).
826
827
  if (adapters.beforeDispatch !== undefined) {
827
828
  try {
828
- await adapters.beforeDispatch(admitted, lease);
829
+ const paused = await adapters.beforeDispatch(admitted, lease);
830
+ if (paused !== undefined) {
831
+ return { exitCode: 0, admitted, terminal: paused, skipAutoResume: true as const };
832
+ }
829
833
  } catch (error) {
830
834
  const settled = (await presentControlledFailure(
831
835
  admitted,
@@ -1363,6 +1367,7 @@ export function resumeTurnRequestProjectionOptions(
1363
1367
  return {
1364
1368
  packageRoot: env.packageRoot,
1365
1369
  home: env.home,
1370
+ ...(env.host === undefined ? {} : { host: env.host }),
1366
1371
  agentDir: env.agentDir,
1367
1372
  ...(env.model === undefined ? {} : { model: env.model }),
1368
1373
  ...pickEngineAxis(env),
@@ -1389,6 +1394,7 @@ export function roleTurnOptions(
1389
1394
  return {
1390
1395
  packageRoot: env.packageRoot,
1391
1396
  home: env.home,
1397
+ ...(env.host === undefined ? {} : { host: env.host }),
1392
1398
  agentDir: env.agentDir,
1393
1399
  ...(env.model === undefined ? {} : { model: env.model }),
1394
1400
  ...pickEngineAxis(env),
@@ -28,6 +28,7 @@ export const PUBLIC_CLI_SUPPORT_COMMANDS = [
28
28
  "help",
29
29
  "resume",
30
30
  "new",
31
+ "setup",
31
32
  ] as const;
32
33
 
33
34
  export type PublicCliSupportCommand = (typeof PUBLIC_CLI_SUPPORT_COMMANDS)[number];