@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
@@ -8,17 +8,17 @@ import { resolve, sep } from "node:path";
8
8
  import { isSafePositiveTicketNumber, readBoardTicketNumber } from "../run-ticket-number.js";
9
9
  import { NotarySourceRunError, resolveNotarySourceRunLocator } from "../notary-source-run.js";
10
10
  import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.js";
11
- import { loadPackagedMethodSkillMaterial, } from "../package-resources/method-skill.js";
12
- import { packagedAdmitsCountersign, packagedBindsBoardTicket, packagedMethodLoadFailureCause, packagedRebindSourceOnResume, packagedRoleMetadata, } from "../packaged-role-registry.js";
11
+ import { packagedAdmitsCountersign, packagedBindsBoardTicket, packagedRebindSourceOnResume, packagedRoleMetadata, } from "../packaged-role-registry.js";
13
12
  import { isAuditorSoulRole } from "../auditor-soul.js";
14
13
  import { CliUsageError } from "./cli-errors.js";
15
14
  import { admitPublicRole, bindAdmittedTicketNumber, bindCourtTicketNumbersOnAdmitted, buildInstructionTransportPrompt, materializeCountersignInvocation, persistAdmittedSourceRunPath, recordAdmittedCorrelation, recordChildDiaristRun, relocateAdmittedRunToTicket, withPreparedAttachments, } from "./invocation.js";
16
- import { presentControlledFailure, prepareSummonsResumeMaterials, resolveResumeMethodMaterialAdapters, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
15
+ import { presentControlledFailure, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
17
16
  import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, } from "./run-lifecycle.js";
18
17
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
19
18
  import { presentStructuralRejection, seatKnownFailureResolver, trySettlePublicSeat, } from "./settlement.js";
19
+ import { warnMissingMethodSkills } from "./machine-method-skills.js";
20
20
  import { formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
21
- import { admittedSeatTurnDetails, packagedSettleSkill, projectRoleTurnRequest, } from "./turn-request.js";
21
+ import { admittedSeatTurnDetails, projectRoleTurnRequest, requiredMethodSkills, } from "./turn-request.js";
22
22
  import { invokeCourtDiarist, runCountersignCourtDiaristStation, } from "./countersign-run.js";
23
23
  function roleRecord(role) {
24
24
  const record = packagedRoleMetadata(role);
@@ -29,7 +29,7 @@ function roleRecord(role) {
29
29
  }
30
30
  /** Project any admitted public role onto the host-neutral turn request. */
31
31
  export function buildInstructionSeatTurnRequest(admitted, options) {
32
- return projectRoleTurnRequest(admitted, admittedSeatTurnDetails(admitted, options.packageRoot), options);
32
+ return projectRoleTurnRequest(admitted, admittedSeatTurnDetails(admitted, options.home, options.host), options);
33
33
  }
34
34
  function initialPrompt(admitted, env) {
35
35
  const record = roleRecord(admitted.role);
@@ -58,11 +58,11 @@ async function bindAndRelocateDiarist(admitted, authority, lease) {
58
58
  }
59
59
  await relocateAdmittedRunToTicket(admitted, authority, lease);
60
60
  }
61
- function seatAdapters(admitted, env, material) {
61
+ function seatAdapters(admitted, env) {
62
62
  const record = roleRecord(admitted.role);
63
63
  const present = "presentSettled" in record ? record.presentSettled : "default";
64
64
  return {
65
- trySettle: (seat, authority, scope) => trySettlePublicSeat(seat, authority, scope, material, env.packageRoot),
65
+ trySettle: (seat, authority, scope) => trySettlePublicSeat(seat, authority, scope),
66
66
  ...(present === "always" ? { shouldPresentSettled: () => true } : {}),
67
67
  ...(present === "typed"
68
68
  ? { shouldPresentSettled: (terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome) }
@@ -126,24 +126,8 @@ async function withAuditorSoulEnv(options) {
126
126
  }
127
127
  async function dispatchAdmitted(admitted, env, io) {
128
128
  const record = roleRecord(admitted.role);
129
- let material;
130
- const skill = packagedSettleSkill(admitted);
131
- if (skill !== undefined) {
132
- try {
133
- material = await loadPackagedMethodSkillMaterial(env.packageRoot, skill);
134
- }
135
- catch (error) {
136
- const knownCause = packagedMethodLoadFailureCause(admitted.role);
137
- return await presentControlledFailure(admitted, {
138
- timedOut: false,
139
- code: null,
140
- stderr: "",
141
- thrown: error,
142
- ...(knownCause === undefined ? {} : { knownCause }),
143
- }, seatAdapters(admitted, env), env.principalAuthority, io);
144
- }
145
- }
146
- const adapters = seatAdapters(admitted, env, material);
129
+ await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
130
+ const adapters = seatAdapters(admitted, env);
147
131
  const execute = async (activeEnv) => {
148
132
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
149
133
  if (auto) {
@@ -258,9 +242,7 @@ async function runCountersignBody(parsed, env, io) {
258
242
  projectRoot: resolve(parsed.project ?? env.cwd),
259
243
  role: "countersign",
260
244
  parentRunPath: gateParentRunPath,
261
- ...(env.boundTicketNumber === undefined
262
- ? {}
263
- : { ticketNumber: env.boundTicketNumber }),
245
+ ...(isSafePositiveTicketNumber(env.boundTicketNumber) ? { ticketNumber: env.boundTicketNumber } : {}),
264
246
  freshSummons: env.freshSummons,
265
247
  summons: {
266
248
  sourceRunPath: gateParentRunPath,
@@ -296,14 +278,16 @@ async function runCountersignBody(parsed, env, io) {
296
278
  let typedTicket;
297
279
  let typedCourtTicketNumbers;
298
280
  let identityDiaristRan = false;
299
- let unboundDiaristRunId;
300
- if (env.runCourtDiaristStation === undefined) {
281
+ let childDiaristRunId;
282
+ let pausedDiaristTerminal;
283
+ if (gateParentRunPath === undefined && env.runCourtDiaristStation === undefined) {
301
284
  let outcome;
302
285
  try {
303
286
  outcome = await invokeCourtDiarist({
304
287
  instruction: parsed.instruction ?? "",
305
288
  projectRoot: admitted.projectRoot,
306
289
  failureLabel: "unbound summons",
290
+ correlationId: admitted.runId,
307
291
  ...(env.boundTicketNumber === undefined ? {} : { boundTicketNumber: env.boundTicketNumber }),
308
292
  }, env, io);
309
293
  }
@@ -318,16 +302,16 @@ async function runCountersignBody(parsed, env, io) {
318
302
  }, seatAdapters(admitted, env), env.principalAuthority, io);
319
303
  }
320
304
  identityDiaristRan = true;
305
+ if (outcome.identity.kind === "escalate")
306
+ pausedDiaristTerminal = outcome.terminal;
321
307
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
322
- unboundDiaristRunId = outcome.admitted.runId;
308
+ childDiaristRunId = outcome.admitted.runId;
323
309
  }
324
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
325
- const diagnostic = outcome.identity.kind === "escalate"
326
- ? outcome.identity.diagnostic
327
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
310
+ if (outcome.failedWithoutEscalate !== undefined) {
311
+ const diagnostic = outcome.failedWithoutEscalate.diagnostic;
328
312
  await materializeAdmission(isSafePositiveTicketNumber(env.boundTicketNumber) ? env.boundTicketNumber : undefined);
329
- if (unboundDiaristRunId !== undefined)
330
- await recordChildDiaristRun(admitted, unboundDiaristRunId);
313
+ if (childDiaristRunId !== undefined)
314
+ await recordChildDiaristRun(admitted, childDiaristRunId);
331
315
  await markRunAdmitted(admitted, env.principalAuthority);
332
316
  return await presentControlledFailure(admitted, {
333
317
  timedOut: false,
@@ -337,19 +321,19 @@ async function runCountersignBody(parsed, env, io) {
337
321
  }, seatAdapters(admitted, env), env.principalAuthority, io);
338
322
  }
339
323
  if (outcome.identity.kind === "ticket") {
340
- typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
341
- ? env.boundTicketNumber
342
- : outcome.identity.ticketNumber;
324
+ typedTicket = outcome.identity.ticketNumber;
343
325
  typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
344
326
  }
345
- else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
346
- typedTicket = env.boundTicketNumber;
347
- }
348
327
  }
349
328
  await materializeAdmission(typedTicket);
350
329
  await markRunAdmitted(admitted, env.principalAuthority);
351
- if (unboundDiaristRunId !== undefined) {
352
- await recordChildDiaristRun(admitted, unboundDiaristRunId);
330
+ if (childDiaristRunId !== undefined) {
331
+ await recordChildDiaristRun(admitted, childDiaristRunId);
332
+ }
333
+ if (pausedDiaristTerminal !== undefined) {
334
+ // The child already submitted its own pause; no parent turn exists yet.
335
+ io.stdout(formatTerminalResult(pausedDiaristTerminal));
336
+ return { exitCode: 0, admitted, terminal: pausedDiaristTerminal };
353
337
  }
354
338
  if (gateParentRunPath !== undefined) {
355
339
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
@@ -401,7 +385,11 @@ async function runCountersignBody(parsed, env, io) {
401
385
  if (!packagedAdmitsCountersign(admittedSeat.role))
402
386
  return;
403
387
  if (!identityDiaristRan || typedTicket !== undefined) {
404
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
388
+ const refresh = await runCountersignCourtDiaristStation(admittedSeat, env, io);
389
+ if (refresh !== undefined) {
390
+ io.stdout(formatTerminalResult(refresh.terminal));
391
+ return refresh.terminal;
392
+ }
405
393
  }
406
394
  await relocateAdmittedRunToTicket(admittedSeat, env.principalAuthority, lease);
407
395
  Object.assign(turnRequest, buildInstructionSeatTurnRequest(admittedSeat, turnProjection));
@@ -486,7 +474,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
486
474
  projectRoot,
487
475
  role,
488
476
  parentRunPath: sourceDirectory,
489
- ...(auditorTicket === undefined ? {} : { ticketNumber: auditorTicket }),
490
477
  freshSummons: env.freshSummons,
491
478
  summons,
492
479
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -511,7 +498,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
511
498
  projectRoot,
512
499
  role,
513
500
  parentRunPath,
514
- ...(env.boundTicketNumber === undefined ? {} : { ticketNumber: env.boundTicketNumber }),
515
501
  freshSummons: env.freshSummons,
516
502
  summons,
517
503
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -537,7 +523,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
537
523
  throw error;
538
524
  }
539
525
  const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
540
- const knownTicket = (await readBoardTicketNumber(source.runDirectory)) ?? env.boundTicketNumber;
541
526
  const summons = {
542
527
  sourceRunPath: source.runDirectory,
543
528
  sourceRun: source,
@@ -548,7 +533,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
548
533
  projectRoot,
549
534
  role,
550
535
  parentRunPath: source.runDirectory,
551
- ...(knownTicket === undefined ? {} : { ticketNumber: knownTicket }),
552
536
  freshSummons: env.freshSummons,
553
537
  summons,
554
538
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -579,54 +563,34 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
579
563
  }
580
564
  const runAdmitted = async () => {
581
565
  await markRunAdmitted(admitted, env.principalAuthority);
582
- if (record.sameParent === "court-diarist") {
583
- const { invokeCourtDiarist } = await import("./countersign-run.js");
584
- const outcome = await invokeCourtDiarist({
585
- instruction: parsed.instruction ?? "",
586
- projectRoot: admitted.projectRoot,
587
- failureLabel: "secretariat unbound summons",
588
- attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
589
- correlationId: admitted.runId,
590
- }, {
591
- cwd: env.cwd,
592
- home: env.home,
593
- agentDir: env.agentDir,
594
- packageRoot: env.packageRoot,
595
- ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
596
- ...(env.signal === undefined ? {} : { signal: env.signal }),
597
- ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
598
- }, io);
566
+ if (role === "secretariat") {
567
+ let outcome;
568
+ try {
569
+ outcome = await invokeCourtDiarist({
570
+ instruction: parsed.instruction ?? "",
571
+ projectRoot: admitted.projectRoot,
572
+ failureLabel: "secretariat unbound summons",
573
+ attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
574
+ correlationId: admitted.runId,
575
+ }, env, io);
576
+ }
577
+ catch (error) {
578
+ return await presentControlledFailure(admitted, {
579
+ timedOut: false, code: null, stderr: "", thrown: error,
580
+ }, seatAdapters(admitted, env), env.principalAuthority, io);
581
+ }
599
582
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
600
583
  await recordChildDiaristRun(admitted, outcome.admitted.runId);
601
584
  }
602
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
603
- const diagnostic = outcome.identity.kind === "escalate"
604
- ? outcome.identity.diagnostic
605
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
585
+ if (outcome.failedWithoutEscalate !== undefined) {
606
586
  return await presentControlledFailure(admitted, {
607
- timedOut: false,
608
- code: null,
609
- stderr: "",
610
- thrown: new Error(diagnostic),
587
+ timedOut: false, code: null, stderr: "",
588
+ thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
611
589
  }, seatAdapters(admitted, env), env.principalAuthority, io);
612
590
  }
613
- if (outcome.identity.kind === "ticket") {
614
- try {
615
- if (outcome.admitted === undefined)
616
- throw new Error("court diarist ticket has no admitted run");
617
- await bindAdmittedTicketNumber(outcome.admitted, outcome.identity.ticketNumber);
618
- await relocateAdmittedRunToTicket(outcome.admitted, env.principalAuthority);
619
- await bindAdmittedTicketNumber(admitted, outcome.identity.ticketNumber);
620
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
621
- }
622
- catch (error) {
623
- return await presentControlledFailure(admitted, {
624
- timedOut: false,
625
- code: null,
626
- stderr: "",
627
- thrown: error,
628
- }, seatAdapters(admitted, env), env.principalAuthority, io);
629
- }
591
+ if (outcome.identity.kind === "escalate" && outcome.terminal !== undefined) {
592
+ io.stdout(formatTerminalResult(outcome.terminal));
593
+ return { exitCode: 0, admitted, terminal: outcome.terminal };
630
594
  }
631
595
  }
632
596
  return dispatchAdmitted(admitted, env, io);
@@ -670,22 +634,26 @@ export async function runPublicInstructionSeatResume(request, env, io) {
670
634
  adapters: {
671
635
  trySettle: async () => undefined,
672
636
  },
673
- afterAdmittedLoad: (admitted) => {
674
- const skill = packagedSettleSkill(admitted);
675
- if (skill === undefined) {
676
- return Promise.resolve({ kind: "continue", adapters: seatAdapters(admitted, env) });
677
- }
678
- const knownCause = packagedMethodLoadFailureCause(admitted.role);
679
- return resolveResumeMethodMaterialAdapters({
680
- admitted,
681
- authority: env.principalAuthority,
682
- io,
683
- loadMaterial: () => loadPackagedMethodSkillMaterial(env.packageRoot, skill),
684
- adaptersWith: (material) => seatAdapters(admitted, env, material),
685
- emptyAdapters: seatAdapters(admitted, env),
686
- ...(knownCause === undefined ? {} : { knownCause }),
687
- });
637
+ afterAdmittedLoad: async (admitted) => {
638
+ await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
639
+ return { kind: "continue", adapters: seatAdapters(admitted, env) };
688
640
  },
689
641
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
690
642
  });
691
643
  }
644
+ /** Continue the parent once any escalated child has submitted. */
645
+ export async function continueParentAfterChild(parentRunId, child, env, io) {
646
+ const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority, true);
647
+ if (loaded.run.state !== "admitted") {
648
+ return runPublicInstructionSeatResume({ runId: parentRunId }, env, io);
649
+ }
650
+ const admitted = loaded.admitted;
651
+ if (admitted.role === "countersign" && admitted.ticketNumber !== undefined) {
652
+ const refresh = await runCountersignCourtDiaristStation(admitted, env, io, child.ticketNumber);
653
+ if (refresh !== undefined) {
654
+ io.stdout(formatTerminalResult(refresh.terminal));
655
+ return { exitCode: 0, admitted, terminal: refresh.terminal };
656
+ }
657
+ }
658
+ return dispatchAdmitted(admitted, env, io);
659
+ }
@@ -9,7 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
10
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
11
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
- import { ensureRoleRunDirectory, ensureRoleRunPlacement, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
12
+ import { ensureRoleRunDirectory, ensureRoleRunPlacement, listBookRunDirectories, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
13
13
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
14
  import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
15
  import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
@@ -164,15 +164,11 @@ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
164
164
  /**
165
165
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
166
166
  * object and both durable pages (invocation.json + admitted-request.json).
167
- * Used by known-ticket reuse when admission was unbound (#635 / #709).
168
- * Never clears an existing binding.
167
+ * A later typed role receipt replaces an earlier assertion (#1025).
169
168
  */
170
169
  export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
171
- if (admitted.ticketNumber !== undefined) {
172
- if (admitted.ticketNumber === ticketNumber)
173
- return;
174
- throw new Error(`bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`);
175
- }
170
+ if (admitted.ticketNumber === ticketNumber)
171
+ return;
176
172
  await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
177
173
  admitted.ticketNumber = ticketNumber;
178
174
  }
@@ -229,7 +225,7 @@ export async function bindCourtTicketNumbersOnAdmitted(admitted, courtTicketNumb
229
225
  });
230
226
  admitted.courtTicketNumbers = frozen;
231
227
  }
232
- /** Move a settled first-entry run from unbound to its asserted ticket directory. */
228
+ /** File a role run under its latest typed ticket identity. */
233
229
  export async function relocateAdmittedRunToTicket(admitted, authority, heldLease) {
234
230
  if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`))
235
231
  return undefined;
@@ -252,6 +248,9 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
252
248
  if (typeof childRunId !== "string")
253
249
  continue;
254
250
  const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
251
+ // A child that already filed under a ticket keeps its own assertion.
252
+ if (!existsSync(childDirectory))
253
+ continue;
255
254
  const childTarget = roleRunPlacement(ledgerHome, {
256
255
  bookKey: admitted.bookKey,
257
256
  subject: { ticketNumber: admitted.ticketNumber },
@@ -259,21 +258,18 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
259
258
  role: "diarist",
260
259
  });
261
260
  authority.seal(childTarget);
262
- if (!existsSync(childDirectory) && existsSync(childTarget.runDirectory))
263
- continue;
264
261
  await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
265
262
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
266
263
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
267
264
  await rename(childDirectory, childTarget.runDirectory);
268
265
  }
269
266
  }
270
- // An identity diarist can file before its caller obtains a ticket. Both its
271
- // own bind and the caller's later bind use this existing relocation seam.
267
+ // The first ticket assignment moves only an unbound diarist's own diary.
272
268
  if (admitted.role === "diarist") {
273
269
  await rehomeUnboundTicketProvenance(oldRunDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
274
270
  }
275
- // Rename commits the run placement. Diary assignment above is an idempotent
276
- // Sitian append, not part of an atomic transaction with this directory move.
271
+ // Rename commits the run placement. Diary assignment above is a Sitian append
272
+ // outside the atomic directory move.
277
273
  // Persisted paths are resolved from typed run identity on read.
278
274
  await rename(oldRunDirectory, target.runDirectory);
279
275
  // rename moved the open lock inode with the directory. Transfer cleanup
@@ -312,24 +308,9 @@ export async function recordChildDiaristRun(parent, childRunId) {
312
308
  export async function bindTicketNumberOnRunDirectory(runDirectory, ticketNumber) {
313
309
  requireSafePositiveTicketNumber(ticketNumber, "bindTicketNumberOnRunDirectory");
314
310
  const admittedPath = join(runDirectory, "admitted-request.json");
315
- const invocationPath = join(runDirectory, "invocation.json");
316
311
  const admitted = JSON.parse(await readFile(admittedPath, "utf8"));
317
- const existing = admitted.ticketNumber;
318
- if (typeof existing === "number") {
319
- if (existing === ticketNumber)
320
- return;
321
- throw new Error(`bindTicketNumberOnRunDirectory refuses to replace existing ticket #${existing} with #${ticketNumber}`);
322
- }
323
- // Conflict guard before any write: crash window of bindAdmittedTicketNumber
324
- // can leave invocation bound while admitted-request is still unbound — refuse
325
- // silent rebind. Read-before-merge; never check the page just overwritten.
326
- if (existsSync(invocationPath)) {
327
- const invocation = JSON.parse(await readFile(invocationPath, "utf8"));
328
- if (typeof invocation.ticketNumber === "number" &&
329
- invocation.ticketNumber !== ticketNumber) {
330
- throw new Error(`bindTicketNumberOnRunDirectory refuses to replace invocation ticket #${invocation.ticketNumber} with #${ticketNumber}`);
331
- }
332
- }
312
+ if (admitted.ticketNumber === ticketNumber)
313
+ return;
333
314
  await writeFile(admittedPath, `${JSON.stringify({ ...admitted, ticketNumber }, null, 2)}\n`, "utf8");
334
315
  await mergeInvocationIdentityPage(runDirectory, { ticketNumber });
335
316
  }
@@ -0,0 +1,150 @@
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.js";
6
+ import { packagedMethodSkillNames } from "../packaged-role-registry.js";
7
+ /** Install source only. Which names are required comes from the role registry. */
8
+ const SKILL_SOURCE = {
9
+ tdd: "mattpocock/skills",
10
+ "diagnosing-bugs": "mattpocock/skills",
11
+ "resolving-merge-conflicts": "mattpocock/skills",
12
+ "ak-cross-m-review": "Akagilnc/ak-cross-m-review",
13
+ };
14
+ function skillRoot(home) {
15
+ return join(home, ".agents/skills");
16
+ }
17
+ export function installedMethodSkillPath(home, name) {
18
+ return installedSkillPath(skillRoot(home), name);
19
+ }
20
+ function installedSkillPath(root, name) {
21
+ const path = join(root, name, "SKILL.md");
22
+ try {
23
+ if (!statSync(path).isFile())
24
+ return undefined;
25
+ accessSync(path, constants.R_OK);
26
+ return path;
27
+ }
28
+ catch (error) {
29
+ const code = error.code;
30
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "EPERM" || code === "ELOOP")
31
+ return undefined;
32
+ throw error;
33
+ }
34
+ }
35
+ function linkedSkillRoot(home, host) {
36
+ if (host === "claude" || host === "claude-code") {
37
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"), "skills");
38
+ }
39
+ if (host === "hermes")
40
+ return join(home, ".hermes/skills");
41
+ return undefined;
42
+ }
43
+ async function occupied(path) {
44
+ try {
45
+ await lstat(path);
46
+ return true;
47
+ }
48
+ catch (error) {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ return false;
52
+ }
53
+ }
54
+ function warnOccupied(path, stdout) {
55
+ stdout(`Warning: ${path} is already occupied; left untouched.\n`);
56
+ }
57
+ export async function warnMissingMethodSkills(home, host, role, skills, stdout) {
58
+ const missing = [];
59
+ const linkedRoot = linkedSkillRoot(home, host);
60
+ for (const skill of skills) {
61
+ if (installedMethodSkillPath(home, skill) === undefined ||
62
+ (linkedRoot !== undefined && installedSkillPath(linkedRoot, skill) === undefined))
63
+ missing.push(skill);
64
+ }
65
+ for (const skill of missing) {
66
+ 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`);
67
+ }
68
+ }
69
+ function runSkillsCli(args, home) {
70
+ const result = spawnSync("npx", ["--yes", "skills", ...args], {
71
+ stdio: "inherit",
72
+ env: { ...process.env, HOME: home },
73
+ });
74
+ if (result.error !== undefined)
75
+ throw result.error;
76
+ return result.status === 0;
77
+ }
78
+ function addSkills(source, skills, home) {
79
+ if (skills.length === 0)
80
+ return true;
81
+ return runSkillsCli([
82
+ "add", source,
83
+ ...skills.flatMap((skill) => ["--skill", skill]),
84
+ "--agent", "codex", "--global", "--yes",
85
+ ], home);
86
+ }
87
+ function updateRequiredSkills(home) {
88
+ const names = packagedMethodSkillNames();
89
+ if (names.length === 0)
90
+ return true;
91
+ return runSkillsCli(["update", "-g", "-y", ...names], home);
92
+ }
93
+ /** Explicit user-run setup; delegates acquisition/install to the ecosystem Skills CLI. */
94
+ export async function runMachineSkillSetup(home, stdout) {
95
+ const canonical = skillRoot(home);
96
+ const missingBySource = new Map();
97
+ let unsourced = false;
98
+ for (const name of packagedMethodSkillNames()) {
99
+ const path = join(canonical, name);
100
+ if (await occupied(path)) {
101
+ warnOccupied(path, stdout);
102
+ continue;
103
+ }
104
+ const source = SKILL_SOURCE[name];
105
+ if (source === undefined) {
106
+ unsourced = true;
107
+ stdout(`Warning: required machine Skill "${name}" has no install source; setup did not install it.\n`);
108
+ continue;
109
+ }
110
+ const missing = missingBySource.get(source) ?? [];
111
+ missing.push(name);
112
+ missingBySource.set(source, missing);
113
+ }
114
+ for (const [source, missing] of missingBySource) {
115
+ if (!addSkills(source, missing, home))
116
+ return 1;
117
+ }
118
+ if (unsourced)
119
+ return 1;
120
+ for (const host of ["claude-code", "hermes"]) {
121
+ const root = linkedSkillRoot(home, host);
122
+ const binaryFromHome = host === "claude-code"
123
+ ? HEADLESS_HOST_DESCRIPTIONS.claude.binaryFromHome
124
+ : HOST_DESCRIPTIONS.hermes.binaryFromHome;
125
+ try {
126
+ accessSync(join(home, ...binaryFromHome), constants.X_OK);
127
+ }
128
+ catch (error) {
129
+ if (["ENOENT", "ENOTDIR", "EACCES"].includes(error.code ?? ""))
130
+ continue;
131
+ throw error;
132
+ }
133
+ for (const name of packagedMethodSkillNames()) {
134
+ const target = join(canonical, name);
135
+ if (installedMethodSkillPath(home, name) === undefined)
136
+ continue;
137
+ const link = join(root, name);
138
+ if (await occupied(link)) {
139
+ warnOccupied(link, stdout);
140
+ continue;
141
+ }
142
+ await mkdir(root, { recursive: true });
143
+ await symlink(target, link, "dir");
144
+ }
145
+ }
146
+ if (!updateRequiredSkills(home))
147
+ return 1;
148
+ stdout("Machine method Skills setup finished. Existing same-name Skills were left untouched.\n");
149
+ return 0;
150
+ }