@akagilnc/pi-workflow-roles 0.1.4800 → 0.1.4870

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 (143) hide show
  1. package/dist/acp-host/production-host.js +19434 -18218
  2. package/dist/analyst-book-key.js +63 -0
  3. package/dist/analyst-cohort.js +167 -0
  4. package/dist/analyst-entry.js +320 -0
  5. package/dist/analyst-index.js +247 -0
  6. package/dist/analyst-ledger.js +561 -0
  7. package/dist/analyst-median.js +16 -0
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +249 -0
  9. package/dist/analyst-metric-families/b2-frame-buckets-actions.js +189 -0
  10. package/dist/analyst-metric-families/gate-cycles.js +77 -0
  11. package/dist/analyst-metric-families/leg-wall-clock.js +56 -0
  12. package/dist/analyst-metric-families/round-timeline.js +126 -0
  13. package/dist/analyst-metric-families.js +23 -0
  14. package/dist/analyst-metric-family.js +11 -0
  15. package/dist/analyst-model-groups.js +135 -0
  16. package/dist/analyst-page.js +182 -0
  17. package/dist/atomic-write.js +23 -0
  18. package/dist/auditor-soul.js +1 -1
  19. package/dist/countersign-contracts.js +0 -2
  20. package/dist/diarist-contracts.js +0 -1
  21. package/dist/doctor-contracts.js +2 -2
  22. package/dist/gatekeeper-role.js +15 -16
  23. package/dist/gleaner-left-contracts.js +0 -2
  24. package/dist/headless-host/production-host.js +16967 -15749
  25. package/dist/host-contracts.js +0 -4
  26. package/dist/inspector-contracts.js +0 -2
  27. package/dist/institutional-resolution.js +4 -2
  28. package/dist/merger-contracts.js +0 -2
  29. package/dist/migrate-book-topology.js +3908 -3089
  30. package/dist/navigator-public-session.js +4 -3
  31. package/dist/notary-contracts.js +0 -2
  32. package/dist/package-contracts/auditor-output.js +0 -1
  33. package/dist/package-contracts/collector-output.js +0 -2
  34. package/dist/package-contracts/fixer-output.js +0 -2
  35. package/dist/package-contracts/gatekeeper-output.js +0 -2
  36. package/dist/package-contracts/judge-output.js +0 -2
  37. package/dist/package-contracts/navigator-output.js +0 -2
  38. package/dist/package-contracts/reviewer-output.js +0 -2
  39. package/dist/package-contracts/terminating-tools.js +32 -92
  40. package/dist/package-contracts/worker-output.js +0 -4
  41. package/dist/packaged-role-registry.js +518 -36
  42. package/dist/pi/role-turn-host.js +2 -1
  43. package/dist/public-cli/analyst-run.js +185 -0
  44. package/dist/public-cli/cli.js +885 -0
  45. package/dist/public-cli/config.js +12 -9
  46. package/dist/public-cli/countersign-run.js +4 -311
  47. package/dist/public-cli/instruction-seat-run.js +628 -158
  48. package/dist/public-cli/invocation.js +806 -1550
  49. package/dist/public-cli/main.js +6962 -9882
  50. package/dist/public-cli/option-definitions.js +51 -99
  51. package/dist/public-cli/post-admission.js +30 -2
  52. package/dist/public-cli/run-lifecycle.js +234 -353
  53. package/dist/public-cli/seat-ticket-binding.js +6 -0
  54. package/dist/public-cli/settlement.js +261 -675
  55. package/dist/public-cli/turn-request.js +76 -0
  56. package/dist/public-role-summons.js +38 -112
  57. package/dist/role-activation-flags.js +23 -45
  58. package/dist/secretariat-contracts.js +0 -2
  59. package/dist/session-identity.js +17 -8
  60. package/dist/session-opening-materials.js +3 -2
  61. package/extensions/role-runtime.ts +6 -80
  62. package/package.json +1 -1
  63. package/souls/coder.md +1 -2
  64. package/src/acp-host/role-turn-host.ts +4 -1
  65. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -2
  66. package/src/auditor-role.ts +1 -2
  67. package/src/auditor-soul.ts +1 -1
  68. package/src/collector-role.ts +2 -2
  69. package/src/countersign-contracts.ts +0 -1
  70. package/src/countersign-role.ts +1 -1
  71. package/src/diarist-contracts.ts +0 -1
  72. package/src/diarist-role.ts +0 -1
  73. package/src/doctor-auditor.ts +1 -2
  74. package/src/doctor-contracts.ts +1 -1
  75. package/src/doctor-role.ts +3 -2
  76. package/src/external-host-turn-loop.ts +1 -1
  77. package/src/factory-board.ts +10 -7
  78. package/src/gatekeeper-role.ts +16 -10
  79. package/src/gleaner-left-contracts.ts +0 -1
  80. package/src/gleaner-left-role.ts +0 -1
  81. package/src/headless-host/role-turn-host.ts +7 -1
  82. package/src/host-contracts.ts +10 -6
  83. package/src/inspector-contracts.ts +0 -1
  84. package/src/inspector-role.ts +0 -2
  85. package/src/institutional-resolution.ts +6 -4
  86. package/src/judge-role.ts +3 -3
  87. package/src/merger-contracts.ts +0 -1
  88. package/src/merger-role.ts +2 -2
  89. package/src/navigator-public-session.ts +7 -6
  90. package/src/navigator-work-context.ts +6 -8
  91. package/src/notary-contracts.ts +0 -1
  92. package/src/notary-role.ts +2 -3
  93. package/src/package-contracts/auditor-output.ts +0 -1
  94. package/src/package-contracts/collector-output.ts +0 -1
  95. package/src/package-contracts/fixer-output.ts +0 -1
  96. package/src/package-contracts/gatekeeper-output.ts +0 -1
  97. package/src/package-contracts/judge-output.ts +0 -1
  98. package/src/package-contracts/navigator-output.ts +0 -1
  99. package/src/package-contracts/reviewer-output.ts +0 -1
  100. package/src/package-contracts/terminating-tools.ts +27 -99
  101. package/src/package-contracts/worker-output.ts +0 -2
  102. package/src/packaged-role-registry.ts +604 -35
  103. package/src/pi/role-turn-host.ts +2 -1
  104. package/src/public-cli/cli.ts +64 -259
  105. package/src/public-cli/config.ts +18 -10
  106. package/src/public-cli/countersign-run.ts +14 -449
  107. package/src/public-cli/instruction-seat-run.ts +758 -277
  108. package/src/public-cli/invocation.ts +1097 -2051
  109. package/src/public-cli/option-definitions.ts +62 -127
  110. package/src/public-cli/post-admission.ts +47 -2
  111. package/src/public-cli/run-lifecycle.ts +303 -613
  112. package/src/public-cli/seat-ticket-binding.ts +6 -0
  113. package/src/public-cli/settlement.ts +399 -1185
  114. package/src/public-cli/turn-request.ts +92 -1
  115. package/src/public-role-summons.ts +44 -128
  116. package/src/reviewer-role.ts +3 -2
  117. package/src/role-activation-flags.ts +20 -41
  118. package/src/role-runtime-dependencies.ts +25 -26
  119. package/src/role-runtime.ts +121 -232
  120. package/src/secretariat-contracts.ts +0 -1
  121. package/src/secretariat-role.ts +0 -2
  122. package/src/session-identity.ts +23 -8
  123. package/src/session-opening-materials.ts +2 -1
  124. package/src/worker-role.ts +3 -4
  125. package/dist/doctor-auditor.js +0 -25
  126. package/dist/public-cli/diarist-run.js +0 -130
  127. package/dist/public-cli/doctor-run.js +0 -87
  128. package/dist/public-cli/inspector-run.js +0 -151
  129. package/dist/public-cli/judge-run.js +0 -114
  130. package/dist/public-cli/notary-run.js +0 -174
  131. package/dist/public-cli/reviewer-run.js +0 -339
  132. package/src/public-cli/coder-run.ts +0 -255
  133. package/src/public-cli/collector-run.ts +0 -189
  134. package/src/public-cli/diarist-run.ts +0 -233
  135. package/src/public-cli/doctor-run.ts +0 -167
  136. package/src/public-cli/fixer-run.ts +0 -265
  137. package/src/public-cli/gleaner-left-run.ts +0 -167
  138. package/src/public-cli/inspector-run.ts +0 -261
  139. package/src/public-cli/judge-run.ts +0 -196
  140. package/src/public-cli/merger-run.ts +0 -294
  141. package/src/public-cli/notary-run.ts +0 -279
  142. package/src/public-cli/reviewer-run.ts +0 -480
  143. package/src/public-cli/secretariat-run.ts +0 -265
@@ -10,6 +10,7 @@ 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
12
  import { ensureRoleRunDirectory, ensureRoleRunPlacement, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
13
+ import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
13
14
  import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
14
15
  import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
15
16
  import { loadDoctorCase, } from "../doctor-evidence.js";
@@ -337,63 +338,202 @@ export function parsePositiveTicketNumber(raw, flag) {
337
338
  }
338
339
  return value;
339
340
  }
340
- /** 共享解析体:同形 owner 的 argv → instruction/attachments/project。 */
341
- function parseInstructionArgv(args, owner) {
342
- const attachmentPaths = [];
343
- let project;
344
- let subject;
345
- let sourceRun;
346
- const positional = [];
341
+ /**
342
+ * One token scan for public argv.
343
+ * Callers assign values; `--` still ends flag parsing.
344
+ * Analyst keeps its own assignment. Public seats assign in `parsePublicSeatArgv`.
345
+ */
346
+ function scanPublicArgv(args, options, handlers) {
347
347
  const tokens = [...args];
348
- const definitions = roleOptions(owner);
349
- const options = createTypedOptionConsumer(definitions);
350
348
  while (tokens.length > 0) {
351
349
  if (tokens[0] === "--") {
352
350
  tokens.shift();
353
- positional.push(...tokens);
354
- break;
351
+ handlers.onDoubleDash(tokens);
352
+ return;
355
353
  }
356
354
  const taken = options.takeDashed(tokens);
357
355
  if (taken !== undefined) {
358
- if (taken.def.id === "attach") {
359
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
360
- continue;
356
+ handlers.onDashed(taken);
357
+ continue;
358
+ }
359
+ handlers.onBare(tokens.shift());
360
+ }
361
+ }
362
+ function appendBareInstruction(owner, token, positional) {
363
+ if (token.startsWith("-") && token !== "-") {
364
+ throw new CliUsageError(`unknown ${owner} option: ${token}`);
365
+ }
366
+ positional.push(token);
367
+ }
368
+ function isMergerInternalPacketFace(token) {
369
+ return (token === "--targetObjectId" || token.startsWith("--targetObjectId=")
370
+ || token === "--sourceObjectId" || token.startsWith("--sourceObjectId=")
371
+ || token === "--expectedConflictPaths" || token.startsWith("--expectedConflictPaths=")
372
+ || token === "--resolutionScope" || token.startsWith("--resolutionScope="));
373
+ }
374
+ /** One assignment for every public-seat option id. Owner only selects the table. */
375
+ function assignPublicSeatOption(owner, taken, fields) {
376
+ const value = taken.value;
377
+ switch (taken.def.id) {
378
+ case "attach":
379
+ fields.attachmentPaths.push(requireOptionPath(taken.def.canonical, value));
380
+ return;
381
+ case "project":
382
+ fields.project = requireOptionPath(taken.def.canonical, value);
383
+ return;
384
+ case "subject": {
385
+ const raw = typeof value === "string" ? value.trim() : "";
386
+ const subject = packagedAdmittedSubject(owner, raw);
387
+ if (subject === undefined) {
388
+ const allowed = packagedSubjectChoices(owner)?.join("|") ?? "";
389
+ throw new CliUsageError(`${owner} --subject must be ${allowed}, got ${value ?? "(missing)"}`);
390
+ }
391
+ fields.subject = subject;
392
+ return;
393
+ }
394
+ case "source-run": {
395
+ const text = typeof value === "string" ? value : "";
396
+ if (text.trim() === "") {
397
+ throw new CliUsageError(`${owner} --source-run requires a run locator`);
361
398
  }
362
- if (taken.def.id === "project") {
363
- project = requireOptionPath(taken.def.canonical, taken.value);
364
- continue;
399
+ fields.sourceRun = packagedStoresSourceRunRaw(owner) ? text : text.trim();
400
+ return;
401
+ }
402
+ case "base":
403
+ fields.baseRevision = packagedBaseIsRevision(owner)
404
+ ? requireReviewerBaseRevision(value)
405
+ : requireOptionPath(taken.def.canonical, value);
406
+ return;
407
+ case "lens":
408
+ fields.lens = requireReviewerLens(value);
409
+ return;
410
+ case "authority-ref":
411
+ fields.authorityRefs.push(requireAuthorityRef(value));
412
+ return;
413
+ case "prerequisites":
414
+ fields.prerequisitesPath = requireOptionPath(taken.def.canonical, value);
415
+ return;
416
+ case "pr":
417
+ fields.prNumber = parsePositivePrOption(value);
418
+ return;
419
+ case "repo":
420
+ fields.repo = parseRepoOption(value);
421
+ return;
422
+ case "request-manifest":
423
+ fields.requestManifestPath = requireOptionPath(taken.def.canonical, value);
424
+ return;
425
+ case "wait-ms": {
426
+ if (value === undefined || !/^[1-9]\d*$/.test(value.trim())) {
427
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
365
428
  }
366
- if (taken.def.id === "subject") {
367
- const raw = typeof taken.value === "string" ? taken.value.trim() : "";
368
- if (raw !== "judge" && raw !== "doctor") {
369
- throw new CliUsageError(`auditor --subject must be judge|doctor, got ${taken.value ?? "(missing)"}`);
370
- }
371
- subject = raw;
372
- continue;
429
+ const parsed = Number(value.trim());
430
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
431
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
373
432
  }
374
- if (taken.def.id === "source-run") {
375
- const raw = typeof taken.value === "string" ? taken.value.trim() : "";
376
- if (raw === "") {
377
- throw new CliUsageError("auditor --source-run requires a run locator");
378
- }
379
- sourceRun = raw;
380
- continue;
433
+ fields.waitWindowMs = parsed;
434
+ return;
435
+ }
436
+ case "issue": {
437
+ if (value === undefined || value.trim() === "") {
438
+ throw new CliUsageError("doctor --issue requires a positive integer");
381
439
  }
382
- throw new CliUsageError(`unknown ${owner} option: ${taken.def.canonical}`);
440
+ fields.issueNumber = parseDoctorIssueNumber(value);
441
+ return;
383
442
  }
384
- const token = tokens.shift();
443
+ case "runs": {
444
+ if (value === undefined || value.trim() === "") {
445
+ throw new CliUsageError("doctor --runs requires a path");
446
+ }
447
+ fields.runs = value;
448
+ return;
449
+ }
450
+ default:
451
+ throw new CliUsageError(`unknown ${owner} option: ${taken.def.canonical}`);
452
+ }
453
+ }
454
+ function rejectSeatBareToken(owner, token) {
455
+ const bare = packagedBareToken(owner);
456
+ if (bare === "burden" && isRejectedPublicSpelling(owner, token)) {
457
+ throw new CliUsageError("judge does not accept a public burden selector; Judge infers its own burden");
458
+ }
459
+ if (bare === "packet"
460
+ && (isRejectedPublicSpelling(owner, token) || isMergerInternalPacketFace(token))) {
461
+ throw new CliUsageError("merger does not accept public packet fields; the adapter reads Git merge materials");
462
+ }
463
+ if (bare === "instruction") {
385
464
  if (token.startsWith("-") && token !== "-") {
386
- throw new CliUsageError(`unknown ${owner} option: ${token}`);
465
+ throw new CliUsageError(`unknown notary option: ${token}`);
466
+ }
467
+ throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
468
+ }
469
+ }
470
+ /**
471
+ * Sole public-seat argv parse. Seat rows differ by the option table and by
472
+ * which result keys that table produces. Analyst does not enter here.
473
+ */
474
+ export function parsePublicSeatArgv(owner, args) {
475
+ if (packagedBareToken(owner) === "burden") {
476
+ const dd = args.indexOf("--");
477
+ const preDd = dd === -1 ? args : args.slice(0, dd);
478
+ for (const token of preDd) {
479
+ if (isRejectedPublicSpelling(owner, token)) {
480
+ throw new CliUsageError("judge does not accept a public burden selector; Judge infers its own burden");
481
+ }
387
482
  }
388
- positional.push(token);
389
483
  }
484
+ const fields = {
485
+ attachmentPaths: [],
486
+ positional: [],
487
+ authorityRefs: [],
488
+ };
489
+ const options = createTypedOptionConsumer(roleOptions(owner));
490
+ scanPublicArgv(args, options, {
491
+ onDashed(taken) {
492
+ assignPublicSeatOption(owner, taken, fields);
493
+ },
494
+ onBare(token) {
495
+ rejectSeatBareToken(owner, token);
496
+ appendBareInstruction(owner, token, fields.positional);
497
+ },
498
+ onDoubleDash(rest) {
499
+ if (packagedBareToken(owner) === "instruction" && rest.length > 0) {
500
+ throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
501
+ }
502
+ fields.positional.push(...rest);
503
+ },
504
+ });
505
+ const phaseDef = optionsForOwner(owner).find((def) => def.id === "phase" && def.form === "positional");
506
+ const phase = phaseDef === undefined
507
+ ? undefined
508
+ : options.consumeLeadingPhase(fields.positional);
390
509
  options.assertRequired();
510
+ const project = fields.project === undefined ? {} : { project: fields.project };
511
+ if (packagedArgvResult(owner) === "source-run") {
512
+ const sourceRun = fields.sourceRun;
513
+ if (sourceRun === undefined || sourceRun.trim() === "") {
514
+ throw new CliUsageError(`${owner} --source-run requires a run locator`);
515
+ }
516
+ return { sourceRun, ...project };
517
+ }
518
+ const instruction = fields.positional.join(" ");
519
+ const material = packagedArgvResult(owner) === "instruction"
520
+ ? { instruction, ...project }
521
+ : { instruction, attachmentPaths: fields.attachmentPaths, ...project };
391
522
  return {
392
- instruction: positional.join(" "),
393
- attachmentPaths,
394
- ...(project === undefined ? {} : { project }),
395
- ...(subject === undefined ? {} : { subject }),
396
- ...(sourceRun === undefined ? {} : { sourceRun }),
523
+ ...material,
524
+ ...(phase === undefined ? {} : { phase }),
525
+ ...(fields.prerequisitesPath === undefined ? {} : { prerequisitesPath: fields.prerequisitesPath }),
526
+ ...(fields.prNumber === undefined ? {} : { prNumber: fields.prNumber }),
527
+ ...(fields.repo === undefined ? {} : { repo: fields.repo }),
528
+ ...(fields.requestManifestPath === undefined ? {} : { requestManifestPath: fields.requestManifestPath }),
529
+ ...(fields.waitWindowMs === undefined ? {} : { waitWindowMs: fields.waitWindowMs }),
530
+ ...(fields.issueNumber === undefined ? {} : { issueNumber: fields.issueNumber }),
531
+ ...(fields.runs === undefined ? {} : { runs: fields.runs }),
532
+ ...(fields.sourceRun === undefined ? {} : { sourceRun: fields.sourceRun }),
533
+ ...(fields.baseRevision === undefined ? {} : { baseRevision: fields.baseRevision }),
534
+ ...(fields.lens === undefined ? {} : { lens: fields.lens }),
535
+ ...(packagedEmitsAuthorityRefs(owner) ? { authorityRefs: fields.authorityRefs } : {}),
536
+ ...(fields.subject === undefined ? {} : { subject: fields.subject }),
397
537
  };
398
538
  }
399
539
  /** Reject missing/blank path values so empty overrides cannot silently degrade. */
@@ -477,139 +617,6 @@ export function requireAuthorityRef(value) {
477
617
  optionLike: "--authority-ref requires a durable reference, not inline Spec prose",
478
618
  });
479
619
  }
480
- /**
481
- * Parse Judge-specific argv after the `judge` token.
482
- * Spellings from PUBLIC_OPTION_TABLE.judge; rejects burden family (#342).
483
- */
484
- export function parseJudgeArgv(args) {
485
- // Judge owns burden inference — rejected spellings checked per-token.
486
- // `--` ends flag parsing: everything after is opaque instruction.
487
- const dd = args.indexOf("--");
488
- const preDd = dd === -1 ? args : args.slice(0, dd);
489
- for (const token of preDd) {
490
- if (isRejectedPublicSpelling("judge", token)) {
491
- throw new CliUsageError("judge does not accept a public burden selector; Judge infers its own burden");
492
- }
493
- }
494
- return parseInstructionArgv(args, "judge");
495
- }
496
- export function parseCountersignArgv(args) {
497
- return parseInstructionArgv(args, "countersign");
498
- }
499
- export function parseInspectorArgv(args) {
500
- return parseInstructionArgv(args, "inspector");
501
- }
502
- export function parseGatekeeperArgv(args) {
503
- return parseInstructionArgv(args, "gatekeeper");
504
- }
505
- export function parseNavigatorArgv(args) {
506
- return parseInstructionArgv(args, "navigator");
507
- }
508
- export function parseAuditorArgv(args) {
509
- return parseInstructionArgv(args, "auditor");
510
- }
511
- export function parseDiaristArgv(args) {
512
- return parseInstructionArgv(args, "diarist");
513
- }
514
- export function parseSecretariatArgv(args) {
515
- return parseInstructionArgv(args, "secretariat");
516
- }
517
- /**
518
- * Parse Coder-specific argv after the `coder` token.
519
- * Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.coder (#342).
520
- */
521
- export function parseCoderArgv(args) {
522
- const attachmentPaths = [];
523
- let project;
524
- const positional = [];
525
- const tokens = [...args];
526
- const definitions = roleOptions("coder");
527
- const options = createTypedOptionConsumer(definitions);
528
- while (tokens.length > 0) {
529
- if (tokens[0] === "--") {
530
- tokens.shift();
531
- positional.push(...tokens);
532
- break;
533
- }
534
- const taken = options.takeDashed(tokens);
535
- if (taken !== undefined) {
536
- if (taken.def.id === "attach") {
537
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
538
- continue;
539
- }
540
- if (taken.def.id === "project") {
541
- project = requireOptionPath(taken.def.canonical, taken.value);
542
- continue;
543
- }
544
- throw new CliUsageError(`unknown coder option: ${taken.def.canonical}`);
545
- }
546
- const token = tokens.shift();
547
- if (token.startsWith("-") && token !== "-") {
548
- throw new CliUsageError(`unknown coder option: ${token}`);
549
- }
550
- positional.push(token);
551
- }
552
- // Phase aliases + default come solely from the typed coder phase row (#342).
553
- const phase = options.consumeLeadingPhase(positional);
554
- options.assertRequired();
555
- return {
556
- phase,
557
- instruction: positional.join(" "),
558
- attachmentPaths,
559
- ...(project === undefined ? {} : { project }),
560
- };
561
- }
562
- /**
563
- * Parse Fixer-specific argv after the `fixer` token.
564
- * Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.fixer (#342).
565
- */
566
- export function parseFixerArgv(args) {
567
- const attachmentPaths = [];
568
- let project;
569
- let prerequisitesPath;
570
- const positional = [];
571
- const tokens = [...args];
572
- const definitions = roleOptions("fixer");
573
- const options = createTypedOptionConsumer(definitions);
574
- while (tokens.length > 0) {
575
- if (tokens[0] === "--") {
576
- tokens.shift();
577
- positional.push(...tokens);
578
- break;
579
- }
580
- const taken = options.takeDashed(tokens);
581
- if (taken !== undefined) {
582
- if (taken.def.id === "attach") {
583
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
584
- continue;
585
- }
586
- if (taken.def.id === "project") {
587
- project = requireOptionPath(taken.def.canonical, taken.value);
588
- continue;
589
- }
590
- if (taken.def.id === "prerequisites") {
591
- prerequisitesPath = requireOptionPath(taken.def.canonical, taken.value);
592
- continue;
593
- }
594
- throw new CliUsageError(`unknown fixer option: ${taken.def.canonical}`);
595
- }
596
- const token = tokens.shift();
597
- if (token.startsWith("-") && token !== "-") {
598
- throw new CliUsageError(`unknown fixer option: ${token}`);
599
- }
600
- positional.push(token);
601
- }
602
- // Phase aliases + default come solely from the typed fixer phase row (#342).
603
- const phase = options.consumeLeadingPhase(positional);
604
- options.assertRequired();
605
- return {
606
- phase,
607
- instruction: positional.join(" "),
608
- attachmentPaths,
609
- ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
610
- ...(project === undefined ? {} : { project }),
611
- };
612
- }
613
620
  async function readRegularFileAttachment(sourcePath) {
614
621
  const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
615
622
  let st;
@@ -729,121 +736,573 @@ function ticketAdmissionFields(ticketNumber) {
729
736
  ticketNumber: requireSafePositiveTicketNumber(ticketNumber, "assertedTicketNumber"),
730
737
  };
731
738
  }
739
+ /** Project an already-typed summons ticket into admit options. Absent stays absent. */
740
+ export function summonedTicketFields(ticketNumber) {
741
+ if (ticketNumber === undefined)
742
+ return {};
743
+ return { assertedTicketNumber: ticketNumber };
744
+ }
732
745
  /**
733
- * Shared instruction-seat admission for Judge and Inspector: project check,
734
- * principal/placement issue, attachment freeze, admitted-request and invocation
735
- * ledger write. CorrelationId is projected only when supplied (Inspector).
736
- * Ticket binding is post-admission via shared seat LLM path (#635).
746
+ * Fields every public admit call shares, including the summons ticket.
747
+ * Seat runners spread this once; they do not restate ticket placement.
737
748
  */
738
- async function admitStandardMaterialInvocation(role, options) {
739
- // Empty project override must not reach resolve("") → cwd (silent default).
749
+ export function admissionCallerOptions(env) {
750
+ return {
751
+ home: env.home,
752
+ principalAuthority: env.principalAuthority,
753
+ cwd: env.cwd,
754
+ ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
755
+ ...(env.model === undefined ? {} : { model: env.model }),
756
+ ...(env.correlationId === undefined || env.correlationId.trim() === ""
757
+ ? {}
758
+ : { correlationId: env.correlationId }),
759
+ ...summonedTicketFields(env.boundTicketNumber),
760
+ };
761
+ }
762
+ export async function admitPublicRole(role, parsed, env, override) {
763
+ const record = packagedRoleMetadata(role);
764
+ if (record === undefined) {
765
+ throw new CliUsageError(`unknown role: ${role}`);
766
+ }
767
+ const shared = {
768
+ ...admissionCallerOptions(env),
769
+ ...(override?.assertedTicketNumber === undefined
770
+ ? {}
771
+ : { assertedTicketNumber: override.assertedTicketNumber }),
772
+ };
773
+ const instruction = parsed.instruction ?? "";
774
+ const attachmentPaths = parsed.attachmentPaths ?? [];
775
+ const project = parsed.project === undefined ? {} : { project: parsed.project };
776
+ switch (record.admission) {
777
+ case "instruction":
778
+ return admitStandardMaterialInvocation(role, {
779
+ ...shared,
780
+ instruction,
781
+ attachmentPaths,
782
+ ...project,
783
+ });
784
+ case "court-materials":
785
+ return admitStandardMaterialInvocation("countersign", {
786
+ ...shared,
787
+ instruction,
788
+ attachmentPaths,
789
+ ...project,
790
+ ...(override?.deferPersistence === undefined
791
+ ? {}
792
+ : { deferPersistence: override.deferPersistence }),
793
+ });
794
+ case "worker-task": {
795
+ if (instruction.trim() === "") {
796
+ throw new CliUsageError("coder requires a nonblank task instruction");
797
+ }
798
+ const phase = parsed.phase ?? "apply";
799
+ if (!record.phases.some((item) => item === phase)) {
800
+ throw new CliUsageError("coder phase must be plan or apply");
801
+ }
802
+ return admitStandardMaterialInvocation("coder", {
803
+ ...shared,
804
+ instruction,
805
+ attachmentPaths,
806
+ ...project,
807
+ placedFields: async (placed) => {
808
+ const taskPath = join(placed.runDirectory, "task.md");
809
+ await writeFile(taskPath, instruction, "utf8");
810
+ return { phase, taskPath };
811
+ },
812
+ });
813
+ }
814
+ case "worker-packet": {
815
+ if (parsed.project !== undefined) {
816
+ requireOptionPath("--project", parsed.project);
817
+ }
818
+ if (instruction.trim() === "") {
819
+ throw new CliUsageError("fixer requires a nonblank repair instruction");
820
+ }
821
+ const phase = parsed.phase ?? "apply";
822
+ if (!record.phases.some((item) => item === phase)) {
823
+ throw new CliUsageError("fixer phase must be plan or apply");
824
+ }
825
+ let prerequisites = Object.freeze([]);
826
+ let prerequisitesSource;
827
+ if (parsed.prerequisitesPath !== undefined) {
828
+ const absolutePrereq = isAbsolute(parsed.prerequisitesPath)
829
+ ? parsed.prerequisitesPath
830
+ : resolve(parsed.prerequisitesPath);
831
+ try {
832
+ prerequisitesSource = await readFile(absolutePrereq, "utf8");
833
+ }
834
+ catch (error) {
835
+ throw new CliUsageError(`fixer prerequisites path is unreadable: ${parsed.prerequisitesPath}`, { cause: error });
836
+ }
837
+ try {
838
+ prerequisites = parseFixerPrerequisites(prerequisitesSource);
839
+ }
840
+ catch (error) {
841
+ if (error instanceof FixerPacketValidationError) {
842
+ throw new CliUsageError(error.message, { cause: error });
843
+ }
844
+ throw error;
845
+ }
846
+ }
847
+ return admitStandardMaterialInvocation("fixer", {
848
+ ...shared,
849
+ instruction,
850
+ attachmentPaths,
851
+ ...project,
852
+ placedFields: async (placed) => {
853
+ let prerequisitesPath;
854
+ if (prerequisitesSource !== undefined) {
855
+ prerequisitesPath = join(placed.runDirectory, "prerequisites.json");
856
+ await writeFile(prerequisitesPath, `${JSON.stringify(prerequisites, null, 2)}\n`, "utf8");
857
+ }
858
+ const packetPath = join(placed.runDirectory, "fix-packet.md");
859
+ await writeFile(packetPath, instruction, "utf8");
860
+ return {
861
+ phase,
862
+ packetPath,
863
+ prerequisites,
864
+ ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
865
+ };
866
+ },
867
+ });
868
+ }
869
+ case "collect-target": {
870
+ if (parsed.project !== undefined) {
871
+ requireOptionPath("--project", parsed.project);
872
+ }
873
+ let explicitPrNumber;
874
+ if (parsed.prNumber !== undefined) {
875
+ try {
876
+ explicitPrNumber = parseCollectorPrNumber(parsed.prNumber);
877
+ }
878
+ catch (error) {
879
+ const detail = error instanceof Error ? error.message : String(error);
880
+ throw new CliUsageError(detail, { cause: error });
881
+ }
882
+ }
883
+ const projectRoot = resolve(parsed.project ?? shared.cwd);
884
+ let repository;
885
+ if (parsed.repo !== undefined) {
886
+ try {
887
+ repository = parseCollectorRepository(parsed.repo);
888
+ }
889
+ catch (error) {
890
+ const detail = error instanceof Error ? error.message : String(error);
891
+ throw new CliUsageError(detail, { cause: error });
892
+ }
893
+ }
894
+ else {
895
+ repository = resolveGitHubRemoteRepository(projectRoot);
896
+ }
897
+ let manifest = emptyCollectorManifest();
898
+ let manifestCanonicalJson;
899
+ if (parsed.requestManifestPath !== undefined) {
900
+ try {
901
+ manifest = await loadCollectorManifest(parsed.requestManifestPath);
902
+ manifestCanonicalJson = manifest.canonicalJson;
903
+ }
904
+ catch (error) {
905
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
906
+ }
907
+ }
908
+ const manifestDigest = manifest.digest;
909
+ const admittedCollector = await admitStandardMaterialInvocation("collector", {
910
+ ...shared,
911
+ instruction,
912
+ attachmentPaths,
913
+ ...project,
914
+ placedFields: async (placed) => {
915
+ // Task materials are frozen before target resolution (#676 A).
916
+ const target = await resolveCollectorTarget({
917
+ projectRoot,
918
+ repository,
919
+ ...(explicitPrNumber === undefined ? {} : { explicitPrNumber }),
920
+ });
921
+ const prNumber = target.kind === "bound" ? target.prNumber : undefined;
922
+ let requestManifestPath;
923
+ if (manifestCanonicalJson !== undefined) {
924
+ requestManifestPath = join(placed.runDirectory, "request-manifest.json");
925
+ await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
926
+ }
927
+ return {
928
+ ...(prNumber === undefined ? {} : { prNumber }),
929
+ repository: repository.canonical,
930
+ repositoryDisplay: repository.display,
931
+ ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
932
+ ...(parsed.waitWindowMs === undefined ? {} : { waitWindowMs: parsed.waitWindowMs }),
933
+ manifestDigest,
934
+ };
935
+ },
936
+ });
937
+ return { ...admittedCollector, repository };
938
+ }
939
+ case "case-identity": {
940
+ if (parsed.project !== undefined) {
941
+ requireOptionPath("--project", parsed.project);
942
+ }
943
+ const issueNumber = parsed.issueNumber ?? Number.NaN;
944
+ if (!Number.isInteger(issueNumber) ||
945
+ issueNumber < 1 ||
946
+ !DOCTOR_ISSUE_NUMBER_PATTERN.test(String(issueNumber))) {
947
+ throw new CliUsageError(`doctor --issue must be a positive integer, got ${issueNumber}`);
948
+ }
949
+ let frozenAttachments = [];
950
+ const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
951
+ ...shared,
952
+ instruction,
953
+ attachmentPaths: [],
954
+ freezeAttachments: false,
955
+ ...project,
956
+ placedFields: async (placed) => {
957
+ let caseRunsPath;
958
+ try {
959
+ caseRunsPath = await resolveDoctorCaseRunsPath({
960
+ home: shared.home,
961
+ projectRoot: placed.projectRoot,
962
+ bookKey: placed.bookKey,
963
+ issueNumber,
964
+ ...(parsed.runs === undefined ? {} : { runs: parsed.runs }),
965
+ });
966
+ }
967
+ catch (error) {
968
+ if (error instanceof CliUsageError)
969
+ throw error;
970
+ const detail = error instanceof Error ? error.message : String(error);
971
+ throw new CliUsageError(detail, { cause: error });
972
+ }
973
+ if (parsed.runs === undefined) {
974
+ ensureRealDirectoryTree(placed.ledgerHome, caseRunsPath);
975
+ }
976
+ let caseIdentity;
977
+ try {
978
+ const patient = await loadDoctorCase(caseRunsPath);
979
+ if (patient.identity.issueNumber !== issueNumber) {
980
+ throw new CliUsageError(`doctor case issue ${patient.identity.issueNumber} does not match --issue ${issueNumber}`);
981
+ }
982
+ caseIdentity = patient.identity;
983
+ caseRunsPath = await realpath(caseRunsPath);
984
+ }
985
+ catch (error) {
986
+ if (error instanceof CliUsageError)
987
+ throw error;
988
+ const detail = error instanceof Error ? error.message : String(error);
989
+ throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
990
+ }
991
+ frozenAttachments = await freezeAttachments(attachmentPaths, placed.attachmentsDirectory);
992
+ return {
993
+ issueNumber,
994
+ caseRunsPath,
995
+ caseIdentity,
996
+ attachments: persistedAttachmentRefs(frozenAttachments),
997
+ };
998
+ },
999
+ });
1000
+ return { ...admittedDoctor, attachments: frozenAttachments };
1001
+ }
1002
+ case "source-locator": {
1003
+ if (parsed.project !== undefined) {
1004
+ requireOptionPath("--project", parsed.project);
1005
+ }
1006
+ const projectRoot = resolve(parsed.project ?? shared.cwd);
1007
+ let sourceRun;
1008
+ try {
1009
+ sourceRun = await resolveNotarySourceRunLocator({
1010
+ projectRoot,
1011
+ sourceRun: parsed.sourceRun ?? "",
1012
+ home: shared.home,
1013
+ });
1014
+ }
1015
+ catch (error) {
1016
+ if (error instanceof NotarySourceRunError) {
1017
+ throw new CliUsageError(error.message, { cause: error });
1018
+ }
1019
+ throw error;
1020
+ }
1021
+ // Board ticket on the source run wins; otherwise the summons ticket. Code does not infer one.
1022
+ const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
1023
+ const knownTicketNumber = inheritedTicketNumber ?? shared.assertedTicketNumber;
1024
+ return admitStandardMaterialInvocation("notary", {
1025
+ ...shared,
1026
+ ...(knownTicketNumber === undefined
1027
+ ? {}
1028
+ : { assertedTicketNumber: knownTicketNumber }),
1029
+ instruction: "",
1030
+ attachmentPaths: [],
1031
+ ...project,
1032
+ freezeAttachments: false,
1033
+ admittedFields: {
1034
+ sourceRunPath: sourceRun.runDirectory,
1035
+ sourceRun,
1036
+ },
1037
+ });
1038
+ }
1039
+ case "gleaner": {
1040
+ if (parsed.project !== undefined) {
1041
+ requireOptionPath("--project", parsed.project);
1042
+ }
1043
+ const baseRevision = parsed.baseRevision ?? "";
1044
+ if (baseRevision.trim() === "") {
1045
+ throw new CliUsageError("--base requires a nonempty revision");
1046
+ }
1047
+ return admitStandardMaterialInvocation("gleaner-left", {
1048
+ ...shared,
1049
+ instruction,
1050
+ attachmentPaths: [],
1051
+ ...project,
1052
+ freezeAttachments: false,
1053
+ admittedFields: { baseRevision },
1054
+ });
1055
+ }
1056
+ case "review-basis": {
1057
+ const lens = requireReviewerLens(parsed.lens);
1058
+ if (parsed.project !== undefined) {
1059
+ requireOptionPath("--project", parsed.project);
1060
+ }
1061
+ const baseRevision = requireReviewerBaseRevision(parsed.baseRevision);
1062
+ const rawRefs = parsed.authorityRefs ?? [];
1063
+ if (rawRefs.length === 0) {
1064
+ throw new CliUsageError("reviewer requires --authority-ref <ref>");
1065
+ }
1066
+ const authorityRefs = Object.freeze(rawRefs.map((ref) => requireAuthorityRef(ref)));
1067
+ const admittedReviewer = await admitStandardMaterialInvocation("reviewer", {
1068
+ ...shared,
1069
+ instruction,
1070
+ attachmentPaths,
1071
+ ...project,
1072
+ admittedFields: {
1073
+ baseRevision,
1074
+ lens,
1075
+ authorityRefs: [...authorityRefs],
1076
+ },
1077
+ });
1078
+ return { ...admittedReviewer, authorityRefs };
1079
+ }
1080
+ case "merge-envelope": {
1081
+ if (parsed.project !== undefined) {
1082
+ requireOptionPath("--project", parsed.project);
1083
+ }
1084
+ if (instruction.trim() === "") {
1085
+ throw new CliUsageError("merger requires a nonblank task instruction");
1086
+ }
1087
+ const projectRoot = resolve(parsed.project ?? shared.cwd);
1088
+ const derived = await deriveMergerEnvelopeFromActiveMerge(projectRoot, createProductionMergerGitState(projectRoot));
1089
+ return admitStandardMaterialInvocation("merger", {
1090
+ ...shared,
1091
+ instruction,
1092
+ attachmentPaths,
1093
+ ...project,
1094
+ placedFields: async (placed) => {
1095
+ const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;
1096
+ const sourceLabel = derived.sourceObjectId === "" ? "(none observed)" : derived.sourceObjectId;
1097
+ const mergerInput = validateMergerInput({
1098
+ attemptId: placed.runId,
1099
+ targetObjectId: derived.targetObjectId,
1100
+ sourceObjectId: derived.sourceObjectId,
1101
+ materials: {
1102
+ task: mergerMaterialFromUtf8(instruction),
1103
+ authority: mergerMaterialFromUtf8(instruction),
1104
+ targetIntent: mergerMaterialFromUtf8(`Investigate primary sources for target parent ${targetLabel}. Do not invent intent.`),
1105
+ sourceIntent: mergerMaterialFromUtf8(`Investigate primary sources for source parent ${sourceLabel}. Do not invent intent.`),
1106
+ },
1107
+ expectedConflictPaths: [...derived.expectedConflictPaths],
1108
+ resolutionScope: [...derived.resolutionScope],
1109
+ authorizedChecks: [],
1110
+ });
1111
+ const mergerInputPath = join(placed.runDirectory, "merger-input.json");
1112
+ await writeFile(mergerInputPath, `${JSON.stringify(mergerInput, null, 2)}\n`, "utf8");
1113
+ return {
1114
+ mergerInputPath,
1115
+ derived: {
1116
+ targetObjectId: derived.targetObjectId,
1117
+ sourceObjectId: derived.sourceObjectId,
1118
+ expectedConflictPaths: [...derived.expectedConflictPaths],
1119
+ resolutionScope: [...derived.resolutionScope],
1120
+ },
1121
+ };
1122
+ },
1123
+ });
1124
+ }
1125
+ }
1126
+ }
1127
+ /** One placement subject: a typed ticket already on the summons, otherwise unbound. */
1128
+ function admissionSubject(ticketNumber) {
1129
+ const fields = ticketAdmissionFields(ticketNumber);
1130
+ if (fields.ticketNumber === undefined)
1131
+ return { unbound: true };
1132
+ return { ticketNumber: fields.ticketNumber };
1133
+ }
1134
+ /**
1135
+ * One admission placement (#505): typed ticket already on the summons, otherwise unbound.
1136
+ * Every public seat persists through this function. Seat-only fields are supplied by the caller.
1137
+ */
1138
+ async function placeRoleAdmission(options) {
740
1139
  if (options.project !== undefined) {
741
1140
  requireOptionPath("--project", options.project);
742
1141
  }
743
1142
  const projectRoot = resolve(options.project ?? options.cwd);
744
- const runId = (options.createRunId ?? uuidv7)();
745
- // Validate asserted ticket before placement so 0/NaN never become subjects.
1143
+ const runId = options.runId ?? (options.createRunId ?? uuidv7)();
746
1144
  const ticketFields = ticketAdmissionFields(options.assertedTicketNumber);
747
1145
  const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
748
1146
  cwd: projectRoot,
749
1147
  runId,
1148
+ role: options.role,
1149
+ subject: admissionSubject(options.assertedTicketNumber),
1150
+ home: options.home,
1151
+ ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1152
+ });
1153
+ const attachments = options.freezeAttachments === false
1154
+ ? []
1155
+ : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1156
+ return {
1157
+ runId,
1158
+ bookKey,
1159
+ projectRoot,
1160
+ runDirectory,
1161
+ principal,
1162
+ sessionDirectory,
1163
+ sessionFile,
1164
+ attachments,
1165
+ attachmentsDirectory,
1166
+ ledgerHome,
1167
+ ticketFields,
1168
+ };
1169
+ }
1170
+ function persistedAttachmentRefs(attachments) {
1171
+ return attachments.map((attachment) => ({
1172
+ provenancePath: attachment.provenancePath,
1173
+ frozenPath: attachment.frozenPath,
1174
+ byteLength: attachment.byteLength,
1175
+ sha256: attachment.sha256,
1176
+ mediaKind: attachment.mediaKind,
1177
+ }));
1178
+ }
1179
+ async function persistPlacedAdmission(admitted, placed, model) {
1180
+ const admittedRequestPath = join(placed.runDirectory, "admitted-request.json");
1181
+ await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1182
+ sessionDirectory: placed.sessionDirectory,
1183
+ sessionFile: placed.sessionFile,
1184
+ });
1185
+ await writeRoleInvocationLedger({
1186
+ ...admitted,
1187
+ sessionDirectory: placed.sessionDirectory,
1188
+ sessionFile: placed.sessionFile,
1189
+ }, admitted.role, model);
1190
+ return admittedRequestPath;
1191
+ }
1192
+ /**
1193
+ * Shared admission: project check, placement, attachment freeze,
1194
+ * admitted-request and invocation ledger write.
1195
+ * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1196
+ * before materializeCountersignInvocation writes the page.
1197
+ * Seats whose extra facts are known before placement pass them as admittedFields.
1198
+ * Seats whose extra facts depend on the placed run pass placedFields
1199
+ * (coder writes task.md; fixer writes fix-packet.md; collector writes request-manifest.json;
1200
+ * doctor resolves the case, then freezes attachments; merger writes merger-input.json).
1201
+ */
1202
+ async function admitStandardMaterialInvocation(role, options) {
1203
+ const defer = options.deferPersistence === true;
1204
+ const skipFreeze = defer || options.freezeAttachments === false;
1205
+ const placed = await placeRoleAdmission({
750
1206
  role,
751
- subject: ticketFields.ticketNumber === undefined
752
- ? { unbound: true }
753
- : { ticketNumber: ticketFields.ticketNumber },
754
1207
  home: options.home,
1208
+ principalAuthority: options.principalAuthority,
1209
+ cwd: options.cwd,
1210
+ attachmentPaths: options.attachmentPaths,
1211
+ ...(skipFreeze ? { freezeAttachments: false } : {}),
1212
+ ...(defer ? { materialize: false } : {}),
1213
+ ...(options.project === undefined ? {} : { project: options.project }),
1214
+ ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
1215
+ ...(options.assertedTicketNumber === undefined
1216
+ ? {}
1217
+ : { assertedTicketNumber: options.assertedTicketNumber }),
755
1218
  });
756
- const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
757
1219
  const correlationFields = options.correlationId === undefined
758
1220
  ? {}
759
1221
  : { correlationId: options.correlationId };
760
1222
  const instruction = options.instruction;
761
1223
  const instructionEmpty = instruction.trim() === "";
1224
+ const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
1225
+ const admittedFields = {
1226
+ ...(options.admittedFields ?? {}),
1227
+ ...(placedExtra ?? {}),
1228
+ };
762
1229
  const admitted = {
763
1230
  role,
764
- runId,
765
- bookKey,
766
- projectRoot,
767
- runDirectory,
768
- principal,
1231
+ runId: placed.runId,
1232
+ bookKey: placed.bookKey,
1233
+ projectRoot: placed.projectRoot,
1234
+ runDirectory: placed.runDirectory,
1235
+ principal: placed.principal,
769
1236
  ...correlationFields,
770
1237
  instruction,
771
1238
  instructionEmpty,
772
- attachments: attachments.map((a) => ({
773
- provenancePath: a.provenancePath,
774
- frozenPath: a.frozenPath,
775
- byteLength: a.byteLength,
776
- sha256: a.sha256,
777
- mediaKind: a.mediaKind,
778
- })),
779
- ...ticketFields,
1239
+ ...admittedFields,
1240
+ attachments: persistedAttachmentRefs(placed.attachments),
1241
+ ...placed.ticketFields,
780
1242
  };
781
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
782
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
783
- sessionDirectory,
784
- sessionFile,
785
- });
786
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1243
+ const admittedRequestPath = defer
1244
+ ? join(placed.runDirectory, "admitted-request.json")
1245
+ : await persistPlacedAdmission(admitted, placed, options.model);
787
1246
  return {
788
1247
  role,
789
- runId,
790
- bookKey,
791
- projectRoot,
1248
+ runId: placed.runId,
1249
+ bookKey: placed.bookKey,
1250
+ projectRoot: placed.projectRoot,
792
1251
  instruction,
793
1252
  instructionEmpty,
794
- attachments,
795
- runDirectory,
796
- principal,
1253
+ attachments: placed.attachments,
1254
+ runDirectory: placed.runDirectory,
1255
+ principal: placed.principal,
797
1256
  admittedRequestPath,
798
1257
  ...correlationFields,
799
- ...ticketFields,
1258
+ ...placed.ticketFields,
1259
+ ...admittedFields,
800
1260
  };
801
1261
  }
802
- /**
803
- * Atomically admit a Judge Role run: freeze Attachments, persist the request,
804
- * and reserve session placement under the #78 ledger book.
805
- */
806
- export async function admitJudgeInvocation(options) {
807
- return admitStandardMaterialInvocation("judge", options);
808
- }
809
- /**
810
- * Admit a direct Inspector (台院) run: freeze attachments, persist the request,
811
- * and reserve session placement. Same instruction-seat face as Judge (#568).
812
- */
813
- export async function admitInspectorInvocation(options) {
814
- return admitStandardMaterialInvocation("inspector", options);
815
- }
816
- /**
817
- * Admit a direct Gatekeeper (门下省) run (#639): freeze attachments, persist
818
- * the request, reserve session placement — same instruction-seat face.
819
- */
820
- export async function admitGatekeeperInvocation(options) {
821
- return admitStandardMaterialInvocation("gatekeeper", options);
822
- }
823
- /**
824
- * Admit a direct Navigator (游奕使) run (#639): freeze attachments, persist
825
- * the request, reserve session placement — same instruction-seat face.
826
- */
827
- export async function admitNavigatorInvocation(options) {
828
- return admitStandardMaterialInvocation("navigator", options);
829
- }
830
- /** Admit a public 审刑院 run (#675). */
831
- export async function admitAuditorInvocation(options) {
832
- return admitStandardMaterialInvocation("auditor", options);
1262
+ function admittedTransportPromptKind(admitted) {
1263
+ if (admitted.role === undefined)
1264
+ return "instruction";
1265
+ const record = packagedRoleMetadata(admitted.role);
1266
+ if (record !== undefined && "transportPrompt" in record)
1267
+ return record.transportPrompt;
1268
+ return "instruction";
833
1269
  }
834
1270
  /**
835
- * Admit a direct Diarist (起居郎) run (#708): freeze attachments, persist the
836
- * request, reserve session placement — same instruction-seat face.
1271
+ * One initial prompt transport. The registry `transportPrompt` leaf selects
1272
+ * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
1273
+ * caller instruction plus frozen attachment paths.
837
1274
  */
838
- export async function admitDiaristInvocation(options) {
839
- return admitStandardMaterialInvocation("diarist", options);
840
- }
841
- /** Admit a public Secretariat (中书省) run (#924). */
842
- export async function admitSecretariatInvocation(options) {
843
- return admitStandardMaterialInvocation("secretariat", options);
844
- }
845
- /** Shared prompt transport for instruction-seat roles (judge/countersign/inspector). */
846
1275
  export function buildInstructionTransportPrompt(admitted, engineMaterial) {
1276
+ const kind = admittedTransportPromptKind(admitted);
1277
+ if (kind === "fixed-kickoff") {
1278
+ return appendEngineSessionMaterial([NOTARY_FIXED_KICKOFF], engineMaterial).join("\n");
1279
+ }
1280
+ if (kind === "baseline") {
1281
+ if (admitted.baseRevision === undefined) {
1282
+ throw new Error("baseline transport prompt is missing the bound revision");
1283
+ }
1284
+ const lines = [`左拾遗案已受理。比较基线:${admitted.baseRevision}`];
1285
+ if (!admitted.instructionEmpty) {
1286
+ lines.push("", admitted.instruction);
1287
+ }
1288
+ return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1289
+ }
1290
+ if (kind === "skill-args") {
1291
+ if (admitted.baseRevision === undefined
1292
+ || admitted.lens === undefined
1293
+ || admitted.authorityRefs === undefined) {
1294
+ throw new Error("skill-args transport prompt is missing frozen skill args");
1295
+ }
1296
+ const lines = [buildReviewerSkillArgProjection({
1297
+ baseRevision: admitted.baseRevision,
1298
+ lens: admitted.lens,
1299
+ authorityRefs: admitted.authorityRefs,
1300
+ })];
1301
+ if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1302
+ lines.push("", admitted.instruction);
1303
+ }
1304
+ return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1305
+ }
847
1306
  const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
848
1307
  if (admitted.attachments.length > 0) {
849
1308
  lines.push("");
@@ -854,91 +1313,26 @@ export function buildInstructionTransportPrompt(admitted, engineMaterial) {
854
1313
  }
855
1314
  return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
856
1315
  }
857
- /** Build the Pi prompt transport for an admitted Judge request. */
858
- export function buildJudgeTransportPrompt(admitted, engineMaterial) {
859
- return buildInstructionTransportPrompt(admitted, engineMaterial);
860
- }
861
- export function buildSecretariatTransportPrompt(admitted, engineMaterial) {
862
- return buildInstructionTransportPrompt(admitted, engineMaterial);
863
- }
864
- export function buildInspectorTransportPrompt(admitted, engineMaterial) {
865
- return buildInstructionTransportPrompt(admitted, engineMaterial);
866
- }
867
- /**
868
- * Admit a Countersign run immediately, or reserve its coordinates for deferred
869
- * materialization after identity lookup (#572 / ADR 0074 / #863).
870
- */
871
- export async function admitCountersignInvocation(options) {
872
- // Same shared freeze/coordinate body as judge but role: "countersign" so
873
- // the ledger and session coordinates use the correct role from the start.
874
- if (options.project !== undefined) {
875
- requireOptionPath("--project", options.project);
876
- }
877
- const projectRoot = resolve(options.project ?? options.cwd);
878
- const runId = (options.createRunId ?? uuidv7)();
879
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
880
- cwd: projectRoot,
881
- runId,
1316
+ /** Persist a deferred Countersign admission after same-ticket lookup found no retained run. */
1317
+ export async function materializeCountersignInvocation(admitted, options) {
1318
+ const placed = await placeRoleAdmission({
882
1319
  role: "countersign",
883
- subject: { unbound: true },
884
1320
  home: options.home,
885
- materialize: options.deferPersistence !== true,
1321
+ principalAuthority: options.principalAuthority,
1322
+ cwd: admitted.projectRoot,
1323
+ attachmentPaths: [],
1324
+ freezeAttachments: false,
1325
+ runId: admitted.runId,
1326
+ ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
886
1327
  });
887
- const attachments = options.deferPersistence === true
888
- ? []
889
- : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
890
- // Ticket binding is LLM-only post-admission (#635); admission stays unbound.
891
- const instruction = options.instruction;
892
- const instructionEmpty = instruction.trim() === "";
893
- const admitted = {
894
- role: "countersign",
895
- runId,
896
- bookKey,
897
- projectRoot,
898
- runDirectory,
899
- principal,
900
- ...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }),
901
- instruction,
902
- instructionEmpty,
903
- attachments: attachments.map((a) => ({
904
- provenancePath: a.provenancePath,
905
- frozenPath: a.frozenPath,
906
- byteLength: a.byteLength,
907
- sha256: a.sha256,
908
- mediaKind: a.mediaKind,
909
- })),
910
- };
911
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
912
- if (options.deferPersistence !== true) {
913
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
914
- sessionDirectory,
915
- sessionFile,
916
- });
917
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1328
+ const placement = placed;
1329
+ admitted.runDirectory = placement.runDirectory;
1330
+ admitted.admittedRequestPath = join(placement.runDirectory, "admitted-request.json");
1331
+ admitted.principal = placement.principal;
1332
+ const ticketFields = ticketAdmissionFields(options.ticketNumber);
1333
+ if (ticketFields.ticketNumber !== undefined) {
1334
+ admitted.ticketNumber = ticketFields.ticketNumber;
918
1335
  }
919
- return {
920
- role: "countersign",
921
- runId,
922
- bookKey,
923
- projectRoot,
924
- instruction,
925
- instructionEmpty,
926
- attachments,
927
- runDirectory,
928
- principal,
929
- admittedRequestPath,
930
- ...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }),
931
- };
932
- }
933
- /** Persist a deferred Countersign admission after same-ticket lookup found no retained run. */
934
- export async function materializeCountersignInvocation(admitted, options) {
935
- const placement = issueAdmissionPlacement(options.principalAuthority, {
936
- cwd: admitted.projectRoot,
937
- runId: admitted.runId,
938
- role: "countersign",
939
- subject: { unbound: true },
940
- home: options.home,
941
- });
942
1336
  const attachments = await freezePreparedAttachmentsIntoRun(options.preparedAttachments, admitted.runDirectory);
943
1337
  admitted.attachments = attachments;
944
1338
  await writeAdmittedRequestPersistence(admitted.admittedRequestPath, admitted, {
@@ -947,10 +1341,6 @@ export async function materializeCountersignInvocation(admitted, options) {
947
1341
  });
948
1342
  await writeRoleInvocationLedger({ ...admitted, sessionDirectory: placement.sessionDirectory, sessionFile: placement.sessionFile }, admitted.role, options.model);
949
1343
  }
950
- /** Build the Pi prompt transport for an admitted Countersign request. */
951
- export function buildCountersignTransportPrompt(admitted, engineMaterial) {
952
- return buildInstructionTransportPrompt(admitted, engineMaterial);
953
- }
954
1344
  /** Load admitted-request.json written at admission (Navigator work-context seam). */
955
1345
  export async function loadAdmittedJudgeRequest(runDirectory) {
956
1346
  try {
@@ -958,7 +1348,7 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
958
1348
  if (raw === null || typeof raw !== "object" || Array.isArray(raw))
959
1349
  return undefined;
960
1350
  const record = raw;
961
- if (record.role !== "judge")
1351
+ if (!packagedPublicInstructionSubject(record.role))
962
1352
  return undefined;
963
1353
  if (typeof record.instruction !== "string")
964
1354
  return undefined;
@@ -980,209 +1370,6 @@ export async function ensureRunArtifactsDir(runDirectory) {
980
1370
  const directory = roleRunArtifactsDirectory(runDirectory);
981
1371
  return ensureRoleRunDirectory(resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)), directory);
982
1372
  }
983
- /**
984
- * Admit a Coder Role run on the common Invocation request.
985
- * Nonblank task remains authoritative: blank instruction is a structural reject.
986
- * Phase (default apply / explicit plan) is frozen into the admitted request.
987
- */
988
- export async function admitCoderInvocation(options) {
989
- if (options.project !== undefined) {
990
- requireOptionPath("--project", options.project);
991
- }
992
- const instruction = options.instruction;
993
- if (instruction.trim() === "") {
994
- throw new CliUsageError("coder requires a nonblank task instruction");
995
- }
996
- if (options.phase !== "plan" && options.phase !== "apply") {
997
- throw new CliUsageError("coder phase must be plan or apply");
998
- }
999
- const projectRoot = resolve(options.project ?? options.cwd);
1000
- const runId = (options.createRunId ?? uuidv7)();
1001
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1002
- cwd: projectRoot,
1003
- runId,
1004
- role: "coder",
1005
- subject: { unbound: true },
1006
- home: options.home,
1007
- });
1008
- const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1009
- const taskPath = join(runDirectory, "task.md");
1010
- await writeFile(taskPath, instruction, "utf8");
1011
- const admitted = {
1012
- role: "coder",
1013
- phase: options.phase,
1014
- runId,
1015
- bookKey,
1016
- projectRoot,
1017
- runDirectory,
1018
- principal,
1019
- instruction,
1020
- instructionEmpty: false,
1021
- taskPath,
1022
- attachments: attachments.map((a) => ({
1023
- provenancePath: a.provenancePath,
1024
- frozenPath: a.frozenPath,
1025
- byteLength: a.byteLength,
1026
- sha256: a.sha256,
1027
- mediaKind: a.mediaKind,
1028
- })),
1029
- };
1030
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1031
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1032
- sessionDirectory,
1033
- sessionFile,
1034
- });
1035
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1036
- return {
1037
- role: "coder",
1038
- phase: options.phase,
1039
- runId,
1040
- bookKey,
1041
- projectRoot,
1042
- instruction,
1043
- instructionEmpty: false,
1044
- attachments,
1045
- runDirectory,
1046
- principal,
1047
- admittedRequestPath,
1048
- taskPath,
1049
- };
1050
- }
1051
- /**
1052
- * Build the Pi prompt transport for an admitted Coder request.
1053
- * Task bytes already live at taskPath for --ak-coder-task; the prompt carries
1054
- * the same instruction plus frozen Attachment paths.
1055
- */
1056
- export function buildCoderTransportPrompt(admitted, engineMaterial) {
1057
- const lines = [admitted.instruction];
1058
- if (admitted.attachments.length > 0) {
1059
- lines.push("");
1060
- lines.push("已受理附件(冻结快照路径):");
1061
- for (const attachment of admitted.attachments) {
1062
- lines.push(`- ${attachment.frozenPath}`);
1063
- }
1064
- }
1065
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1066
- }
1067
- /**
1068
- * Admit a Fixer Role run on the common Invocation request plus optional prerequisites.
1069
- * Nonblank instruction remains authoritative. Phase defaults to apply at parse time.
1070
- * Prerequisite grammar is structural; unmet/insufficient prerequisites stay Fixer judgments.
1071
- */
1072
- export async function admitFixerInvocation(options) {
1073
- if (options.project !== undefined) {
1074
- requireOptionPath("--project", options.project);
1075
- }
1076
- const instruction = options.instruction;
1077
- if (instruction.trim() === "") {
1078
- throw new CliUsageError("fixer requires a nonblank repair instruction");
1079
- }
1080
- if (options.phase !== "plan" && options.phase !== "apply") {
1081
- throw new CliUsageError("fixer phase must be plan or apply");
1082
- }
1083
- // Validate/read prerequisites before freezing request materials.
1084
- let prerequisites = Object.freeze([]);
1085
- let prerequisitesSource;
1086
- if (options.prerequisitesPath !== undefined) {
1087
- const absolutePrereq = isAbsolute(options.prerequisitesPath)
1088
- ? options.prerequisitesPath
1089
- : resolve(options.prerequisitesPath);
1090
- try {
1091
- prerequisitesSource = await readFile(absolutePrereq, "utf8");
1092
- }
1093
- catch (error) {
1094
- throw new CliUsageError(`fixer prerequisites path is unreadable: ${options.prerequisitesPath}`, { cause: error });
1095
- }
1096
- try {
1097
- prerequisites = parseFixerPrerequisites(prerequisitesSource);
1098
- }
1099
- catch (error) {
1100
- if (error instanceof FixerPacketValidationError) {
1101
- throw new CliUsageError(error.message, { cause: error });
1102
- }
1103
- throw error;
1104
- }
1105
- }
1106
- const projectRoot = resolve(options.project ?? options.cwd);
1107
- const runId = (options.createRunId ?? uuidv7)();
1108
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1109
- cwd: projectRoot,
1110
- runId,
1111
- role: "fixer",
1112
- subject: { unbound: true },
1113
- home: options.home,
1114
- });
1115
- const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1116
- let prerequisitesPath;
1117
- if (prerequisitesSource !== undefined) {
1118
- prerequisitesPath = join(runDirectory, "prerequisites.json");
1119
- await writeFile(prerequisitesPath, `${JSON.stringify(prerequisites, null, 2)}\n`, "utf8");
1120
- }
1121
- const packetPath = join(runDirectory, "fix-packet.md");
1122
- await writeFile(packetPath, instruction, "utf8");
1123
- const admitted = {
1124
- role: "fixer",
1125
- phase: options.phase,
1126
- runId,
1127
- bookKey,
1128
- projectRoot,
1129
- runDirectory,
1130
- principal,
1131
- instruction,
1132
- instructionEmpty: false,
1133
- packetPath,
1134
- ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
1135
- prerequisites: prerequisites.map((entry) => ({
1136
- id: entry.id,
1137
- requirement: entry.requirement,
1138
- })),
1139
- attachments: attachments.map((a) => ({
1140
- provenancePath: a.provenancePath,
1141
- frozenPath: a.frozenPath,
1142
- byteLength: a.byteLength,
1143
- sha256: a.sha256,
1144
- mediaKind: a.mediaKind,
1145
- })),
1146
- };
1147
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1148
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1149
- sessionDirectory,
1150
- sessionFile,
1151
- });
1152
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1153
- return {
1154
- role: "fixer",
1155
- phase: options.phase,
1156
- runId,
1157
- bookKey,
1158
- projectRoot,
1159
- instruction,
1160
- instructionEmpty: false,
1161
- attachments,
1162
- runDirectory,
1163
- principal,
1164
- admittedRequestPath,
1165
- packetPath,
1166
- ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
1167
- prerequisites,
1168
- };
1169
- }
1170
- /**
1171
- * Build the Pi prompt transport for an admitted Fixer request.
1172
- * Instruction bytes live at packetPath; prerequisites at optional path.
1173
- * Diagnosis method is available via package --skill, not forced into this prompt.
1174
- */
1175
- export function buildFixerTransportPrompt(admitted, engineMaterial) {
1176
- const lines = [admitted.instruction];
1177
- if (admitted.attachments.length > 0) {
1178
- lines.push("");
1179
- lines.push("已受理附件(冻结快照路径):");
1180
- for (const attachment of admitted.attachments) {
1181
- lines.push(`- ${attachment.frozenPath}`);
1182
- }
1183
- }
1184
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1185
- }
1186
1373
  function parsePositivePrOption(raw) {
1187
1374
  if (raw === undefined || raw.trim() === "")
1188
1375
  throw new CliUsageError("--pr requires a positive pull request number");
@@ -1198,78 +1385,6 @@ function parseRepoOption(raw) {
1198
1385
  throw new CliUsageError("--repo requires owner/repo");
1199
1386
  return raw;
1200
1387
  }
1201
- export function parseCollectorArgv(args) {
1202
- // Spellings from PUBLIC_OPTION_TABLE.collector (#342).
1203
- const attachmentPaths = [];
1204
- let project;
1205
- let repo;
1206
- let prNumber;
1207
- let requestManifestPath;
1208
- let waitWindowMs;
1209
- const positional = [];
1210
- const tokens = [...args];
1211
- const definitions = roleOptions("collector");
1212
- const options = createTypedOptionConsumer(definitions);
1213
- while (tokens.length > 0) {
1214
- if (tokens[0] === "--") {
1215
- tokens.shift();
1216
- positional.push(...tokens);
1217
- break;
1218
- }
1219
- const taken = options.takeDashed(tokens);
1220
- if (taken !== undefined) {
1221
- if (taken.def.id === "attach") {
1222
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
1223
- continue;
1224
- }
1225
- if (taken.def.id === "project") {
1226
- project = requireOptionPath(taken.def.canonical, taken.value);
1227
- continue;
1228
- }
1229
- if (taken.def.id === "pr") {
1230
- prNumber = parsePositivePrOption(taken.value);
1231
- continue;
1232
- }
1233
- if (taken.def.id === "repo") {
1234
- repo = parseRepoOption(taken.value);
1235
- continue;
1236
- }
1237
- if (taken.def.id === "request-manifest") {
1238
- requestManifestPath = requireOptionPath(taken.def.canonical, taken.value);
1239
- continue;
1240
- }
1241
- if (taken.def.id === "wait-ms") {
1242
- if (taken.value === undefined || !/^[1-9]\d*$/.test(taken.value.trim())) {
1243
- throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
1244
- }
1245
- const parsed = Number(taken.value.trim());
1246
- if (!Number.isSafeInteger(parsed) || parsed < 1) {
1247
- throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
1248
- }
1249
- waitWindowMs = parsed;
1250
- continue;
1251
- }
1252
- throw new CliUsageError(`unknown collector option: ${taken.def.canonical}`);
1253
- }
1254
- const token = tokens.shift();
1255
- if (token.startsWith("-") && token !== "-") {
1256
- throw new CliUsageError(`unknown collector option: ${token}`);
1257
- }
1258
- positional.push(token);
1259
- }
1260
- // Unconditional required from typed table via shared consumer (#342).
1261
- // #676 D1: --pr is optional; ambiguous targets reject at admission, not by guessing.
1262
- options.assertRequired();
1263
- return {
1264
- ...(prNumber === undefined ? {} : { prNumber }),
1265
- instruction: positional.join(" "),
1266
- attachmentPaths,
1267
- ...(project === undefined ? {} : { project }),
1268
- ...(repo === undefined ? {} : { repo }),
1269
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1270
- ...(waitWindowMs === undefined ? {} : { waitWindowMs }),
1271
- };
1272
- }
1273
1388
  /**
1274
1389
  * Resolve owner/repo from the project's `origin` remote (github.com only).
1275
1390
  * Supports https and SSH GitHub URL shapes; never scrapes instruction prose.
@@ -1318,745 +1433,64 @@ function isGitRemoteMissing(error) {
1318
1433
  const status = error.status;
1319
1434
  return status === 2;
1320
1435
  }
1321
- /**
1322
- * Admit a Collector Role run: assemble the retained leg manifest from typed
1323
- * declarations, resolve repository + PR target (#676 D1), and place the session under #78.
1324
- * Explicit PR is not preflighted for existence; context resolution uses online association.
1325
- */
1326
- export async function admitCollectorInvocation(options) {
1327
- if (options.project !== undefined) {
1328
- requireOptionPath("--project", options.project);
1329
- }
1330
- let explicitPrNumber;
1331
- if (options.prNumber !== undefined) {
1332
- try {
1333
- explicitPrNumber = parseCollectorPrNumber(options.prNumber);
1334
- }
1335
- catch (error) {
1336
- const detail = error instanceof Error ? error.message : String(error);
1337
- throw new CliUsageError(detail, { cause: error });
1338
- }
1339
- }
1340
- const projectRoot = resolve(options.project ?? options.cwd);
1341
- let repository;
1342
- if (options.repo !== undefined) {
1343
- try {
1344
- repository = parseCollectorRepository(options.repo);
1345
- }
1346
- catch (error) {
1347
- const detail = error instanceof Error ? error.message : String(error);
1348
- throw new CliUsageError(detail, { cause: error });
1349
- }
1350
- }
1351
- else {
1352
- repository = resolveGitHubRemoteRepository(projectRoot);
1353
- }
1354
- // Validate optional request-manifest before freezing request materials.
1355
- let manifest = emptyCollectorManifest();
1356
- let manifestCanonicalJson;
1357
- if (options.requestManifestPath !== undefined) {
1358
- try {
1359
- manifest = await loadCollectorManifest(options.requestManifestPath);
1360
- manifestCanonicalJson = manifest.canonicalJson;
1361
- }
1362
- catch (error) {
1363
- throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
1364
- }
1365
- }
1366
- const manifestDigest = manifest.digest;
1367
- const runId = (options.createRunId ?? uuidv7)();
1368
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1369
- cwd: projectRoot,
1370
- runId,
1371
- role: "collector",
1372
- subject: { unbound: true },
1373
- home: options.home,
1374
- });
1375
- // #676 A: freeze task materials BEFORE target resolution so the role receives
1376
- // real instruction + attachments. Admission binds only explicit --pr or unique
1377
- // head/commit association — task-text scrape is not a target lock.
1378
- const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
1379
- const instruction = options.instruction ?? "";
1380
- const instructionEmpty = instruction.trim() === "";
1381
- const target = await resolveCollectorTarget({
1382
- projectRoot,
1383
- repository,
1384
- ...(explicitPrNumber === undefined ? {} : { explicitPrNumber }),
1385
- });
1386
- const prNumber = target.kind === "bound" ? target.prNumber : undefined;
1387
- let requestManifestPath;
1388
- if (manifestCanonicalJson !== undefined) {
1389
- requestManifestPath = join(runDirectory, "request-manifest.json");
1390
- await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
1391
- }
1392
- const admitted = {
1393
- role: "collector",
1394
- runId,
1395
- bookKey,
1396
- projectRoot,
1397
- runDirectory,
1398
- principal,
1399
- instruction,
1400
- instructionEmpty,
1401
- ...(prNumber === undefined ? {} : { prNumber }),
1402
- repository: repository.canonical,
1403
- repositoryDisplay: repository.display,
1404
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1405
- ...(options.waitWindowMs === undefined ? {} : { waitWindowMs: options.waitWindowMs }),
1406
- manifestDigest,
1407
- attachments: attachments.map((a) => ({
1408
- provenancePath: a.provenancePath,
1409
- frozenPath: a.frozenPath,
1410
- byteLength: a.byteLength,
1411
- sha256: a.sha256,
1412
- mediaKind: a.mediaKind,
1413
- })),
1414
- };
1415
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1416
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1417
- sessionDirectory,
1418
- sessionFile,
1419
- });
1420
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1421
- return {
1422
- role: "collector",
1423
- runId,
1424
- bookKey,
1425
- projectRoot,
1426
- instruction,
1427
- instructionEmpty,
1428
- attachments,
1429
- runDirectory,
1430
- principal,
1431
- admittedRequestPath,
1432
- ...(prNumber === undefined ? {} : { prNumber }),
1433
- repository,
1434
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1435
- ...(options.waitWindowMs === undefined ? {} : { waitWindowMs: options.waitWindowMs }),
1436
- manifestDigest,
1437
- };
1438
- }
1439
- /**
1440
- * #676 A: Collector consumes the real call task + frozen attachments so the role
1441
- * can identify issue/PR from materials via ak_collector_bind_target. Explicit --pr
1442
- * still wins at admission; unique head/commit association also binds. No fixed
1443
- * kickoff rewrite of the caller task; no mechanical task-text target lock.
1444
- */
1445
- export function buildCollectorTransportPrompt(admitted, engineMaterial) {
1446
- return buildInstructionTransportPrompt(admitted, engineMaterial);
1447
- }
1448
1436
  /** Positive Issue number grammar shared with Doctor case path identity. */
1449
1437
  const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
1450
- /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
1451
- /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
1452
- const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
1453
- /**
1454
- * Parse a positive Issue number for public Doctor admission.
1455
- * Leading zeros and non-integers are structural rejects.
1456
- */
1457
- export function parseDoctorIssueNumber(raw) {
1458
- const trimmed = raw.trim();
1459
- if (!DOCTOR_ISSUE_NUMBER_PATTERN.test(trimmed)) {
1460
- throw new CliUsageError(`doctor --issue must be a positive integer, got ${raw}`);
1461
- }
1462
- return Number(trimmed);
1463
- }
1464
- /**
1465
- * Parse Doctor-specific argv after the `doctor` token.
1466
- * Requires --issue; optional confined --runs override; common --attach/--project.
1467
- */
1468
- export function parseDoctorArgv(args) {
1469
- // Spellings from PUBLIC_OPTION_TABLE.doctor (#342).
1470
- const attachmentPaths = [];
1471
- let project;
1472
- let issueRaw;
1473
- let runs;
1474
- const positional = [];
1475
- const tokens = [...args];
1476
- const definitions = roleOptions("doctor");
1477
- const options = createTypedOptionConsumer(definitions);
1478
- while (tokens.length > 0) {
1479
- if (tokens[0] === "--") {
1480
- tokens.shift();
1481
- positional.push(...tokens);
1482
- break;
1483
- }
1484
- const taken = options.takeDashed(tokens);
1485
- if (taken !== undefined) {
1486
- if (taken.def.id === "issue") {
1487
- if (taken.value === undefined || taken.value.trim() === "") {
1488
- throw new CliUsageError("doctor --issue requires a positive integer");
1489
- }
1490
- issueRaw = taken.value;
1491
- continue;
1492
- }
1493
- if (taken.def.id === "runs") {
1494
- if (taken.value === undefined || taken.value.trim() === "") {
1495
- throw new CliUsageError("doctor --runs requires a path");
1496
- }
1497
- runs = taken.value;
1498
- continue;
1499
- }
1500
- if (taken.def.id === "attach") {
1501
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
1502
- continue;
1503
- }
1504
- if (taken.def.id === "project") {
1505
- project = requireOptionPath(taken.def.canonical, taken.value);
1506
- continue;
1507
- }
1508
- throw new CliUsageError(`unknown doctor option: ${taken.def.canonical}`);
1509
- }
1510
- const token = tokens.shift();
1511
- if (token.startsWith("-") && token !== "-") {
1512
- throw new CliUsageError(`unknown doctor option: ${token}`);
1513
- }
1514
- positional.push(token);
1515
- }
1516
- // Unconditional required (e.g. --issue) from typed table via shared consumer (#342).
1517
- options.assertRequired();
1518
- const issueNumber = parseDoctorIssueNumber(issueRaw);
1519
- if (runs !== undefined && runs.trim() === "") {
1520
- throw new CliUsageError("doctor --runs requires a path");
1521
- }
1522
- return {
1523
- issueNumber,
1524
- instruction: positional.join(" "),
1525
- attachmentPaths,
1526
- ...(project === undefined ? {} : { project }),
1527
- ...(runs === undefined ? {} : { runs }),
1528
- };
1529
- }
1530
- /**
1531
- * Resolve the retained Doctor case runs root from Issue identity.
1532
- * Default is the #78 book locator; optional --runs must stay project-confined
1533
- * and match Doctor case grammar for the same issue number.
1534
- */
1535
- export async function resolveDoctorCaseRunsPath(options) {
1536
- const ledgerHome = resolveActivationLedgerHome(options.home);
1537
- // Canonical topology: <book>/<ticket>/runs (docs/dossier-topology.md).
1538
- const defaultRuns = join(activationBookDirectory(ledgerHome, options.bookKey), String(options.issueNumber), "runs");
1539
- if (options.runs === undefined) {
1540
- return defaultRuns;
1541
- }
1542
- const raw = options.runs.trim();
1543
- if (raw === "") {
1544
- throw new CliUsageError("doctor --runs requires a path");
1545
- }
1546
- // Project-relative only — absolute overrides would bypass confinement.
1547
- if (isAbsolute(raw)) {
1548
- throw new CliUsageError("doctor --runs must be a project-relative path");
1549
- }
1550
- const resolved = resolve(options.projectRoot, raw);
1551
- if (resolved !== options.projectRoot &&
1552
- !pathContainedIn(options.projectRoot, resolved)) {
1553
- throw new CliUsageError("doctor --runs escapes the project root");
1554
- }
1555
- let real;
1556
- try {
1557
- real = await realpath(resolved);
1558
- }
1559
- catch (error) {
1560
- const detail = error instanceof Error ? error.message : String(error);
1561
- throw new CliUsageError(`doctor --runs is not a readable retained runs root: ${detail}`, { cause: error });
1562
- }
1563
- const normalized = real.split(sep).join("/");
1564
- const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
1565
- if (!match) {
1566
- throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory");
1567
- }
1568
- if (Number(match[1]) !== options.issueNumber) {
1569
- throw new CliUsageError(`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`);
1570
- }
1571
- return real;
1572
- }
1573
- /**
1574
- * Admit a Doctor Role run: resolve Issue → retained runs root via #78 (or a
1575
- * confined override), construct the structurally exact case identity through
1576
- * loadDoctorCase, and place the Doctor session under the book runs lane.
1577
- * Does not copy session content into a second store.
1578
- */
1579
- export async function admitDoctorInvocation(options) {
1580
- if (options.project !== undefined) {
1581
- requireOptionPath("--project", options.project);
1582
- }
1583
- if (!Number.isInteger(options.issueNumber) ||
1584
- options.issueNumber < 1 ||
1585
- !DOCTOR_ISSUE_NUMBER_PATTERN.test(String(options.issueNumber))) {
1586
- throw new CliUsageError(`doctor --issue must be a positive integer, got ${options.issueNumber}`);
1587
- }
1588
- const projectRoot = resolve(options.project ?? options.cwd);
1589
- const runId = (options.createRunId ?? uuidv7)();
1590
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1591
- cwd: projectRoot,
1592
- runId,
1593
- role: "doctor",
1594
- subject: { unbound: true },
1595
- home: options.home,
1596
- });
1597
- let caseRunsPath;
1598
- try {
1599
- caseRunsPath = await resolveDoctorCaseRunsPath({
1600
- home: options.home,
1601
- projectRoot,
1602
- bookKey,
1603
- issueNumber: options.issueNumber,
1604
- ...(options.runs === undefined ? {} : { runs: options.runs }),
1605
- });
1606
- }
1607
- catch (error) {
1608
- if (error instanceof CliUsageError)
1609
- throw error;
1610
- const detail = error instanceof Error ? error.message : String(error);
1611
- throw new CliUsageError(detail, { cause: error });
1612
- }
1613
- // Default #78 locator may not exist yet — ensure the empty runs root so
1614
- // loadDoctorCase can form an empty case and Doctor's refusal owns insufficiency.
1615
- if (options.runs === undefined) {
1616
- ensureRealDirectoryTree(ledgerHome, caseRunsPath);
1617
- }
1618
- let caseIdentity;
1619
- try {
1620
- const patient = await loadDoctorCase(caseRunsPath);
1621
- if (patient.identity.issueNumber !== options.issueNumber) {
1622
- throw new CliUsageError(`doctor case issue ${patient.identity.issueNumber} does not match --issue ${options.issueNumber}`);
1623
- }
1624
- caseIdentity = patient.identity;
1625
- caseRunsPath = await realpath(caseRunsPath);
1626
- }
1627
- catch (error) {
1628
- if (error instanceof CliUsageError)
1629
- throw error;
1630
- const detail = error instanceof Error ? error.message : String(error);
1631
- throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
1632
- }
1633
- const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
1634
- const instruction = options.instruction ?? "";
1635
- const instructionEmpty = instruction.trim() === "";
1636
- const admitted = {
1637
- role: "doctor",
1638
- runId,
1639
- bookKey,
1640
- projectRoot,
1641
- runDirectory,
1642
- principal,
1643
- instruction,
1644
- instructionEmpty,
1645
- issueNumber: options.issueNumber,
1646
- caseRunsPath,
1647
- caseIdentity,
1648
- attachments: attachments.map((a) => ({
1649
- provenancePath: a.provenancePath,
1650
- frozenPath: a.frozenPath,
1651
- byteLength: a.byteLength,
1652
- sha256: a.sha256,
1653
- mediaKind: a.mediaKind,
1654
- })),
1655
- };
1656
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1657
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1658
- sessionDirectory,
1659
- sessionFile,
1660
- });
1661
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1662
- return {
1663
- role: "doctor",
1664
- runId,
1665
- bookKey,
1666
- projectRoot,
1667
- instruction,
1668
- instructionEmpty,
1669
- attachments,
1670
- runDirectory,
1671
- principal,
1672
- admittedRequestPath,
1673
- issueNumber: options.issueNumber,
1674
- caseRunsPath,
1675
- caseIdentity,
1676
- };
1677
- }
1678
- /** Build the Pi prompt transport for an admitted Doctor request. */
1679
- export function buildDoctorTransportPrompt(admitted, engineMaterial) {
1680
- const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
1681
- if (admitted.attachments.length > 0) {
1682
- lines.push("");
1683
- lines.push("已受理附件(冻结快照路径):");
1684
- for (const attachment of admitted.attachments) {
1685
- lines.push(`- ${attachment.frozenPath}`);
1686
- }
1687
- }
1688
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1689
- }
1690
- /**
1691
- * Parse Notary-specific argv after the `notary` token.
1692
- * Input contract = zero prompt, zero attachment projection (#448 / #276).
1693
- */
1694
- export function parseNotaryArgv(args) {
1695
- let project;
1696
- let sourceRun;
1697
- const tokens = [...args];
1698
- const definitions = roleOptions("notary");
1699
- const options = createTypedOptionConsumer(definitions);
1700
- while (tokens.length > 0) {
1701
- if (tokens[0] === "--") {
1702
- tokens.shift();
1703
- if (tokens.length > 0) {
1704
- throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
1705
- }
1706
- break;
1707
- }
1708
- const taken = options.takeDashed(tokens);
1709
- if (taken !== undefined) {
1710
- if (taken.def.id === "project") {
1711
- project = requireOptionPath(taken.def.canonical, taken.value);
1712
- continue;
1713
- }
1714
- if (taken.def.id === "source-run") {
1715
- if (taken.value === undefined || taken.value.trim() === "") {
1716
- throw new CliUsageError("notary --source-run requires a run locator");
1717
- }
1718
- sourceRun = taken.value;
1719
- continue;
1720
- }
1721
- throw new CliUsageError(`unknown notary option: ${taken.def.canonical}`);
1722
- }
1723
- const token = tokens.shift();
1724
- if (token.startsWith("-") && token !== "-") {
1725
- throw new CliUsageError(`unknown notary option: ${token}`);
1726
- }
1727
- throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
1728
- }
1729
- options.assertRequired();
1730
- if (sourceRun === undefined || sourceRun.trim() === "") {
1731
- throw new CliUsageError("notary --source-run requires a run locator");
1732
- }
1733
- return {
1734
- sourceRun,
1735
- ...(project === undefined ? {} : { project }),
1736
- };
1737
- }
1738
- export async function admitNotaryInvocation(options) {
1739
- if (options.project !== undefined) {
1740
- requireOptionPath("--project", options.project);
1741
- }
1742
- const projectRoot = resolve(options.project ?? options.cwd);
1743
- let sourceRun;
1744
- try {
1745
- sourceRun = await resolveNotarySourceRunLocator({
1746
- projectRoot,
1747
- sourceRun: options.sourceRun,
1748
- home: options.home,
1749
- });
1750
- }
1751
- catch (error) {
1752
- if (error instanceof NotarySourceRunError) {
1753
- throw new CliUsageError(error.message, { cause: error });
1754
- }
1755
- throw error;
1756
- }
1757
- // Notary inherits board identity only — migration-derived stays display/placement.
1758
- const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
1759
- const runId = options.createRunId?.() ?? uuidv7();
1760
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1761
- cwd: projectRoot,
1762
- runId,
1763
- role: "notary",
1764
- subject: inheritedTicketNumber === undefined
1765
- ? { unbound: true }
1766
- : { ticketNumber: inheritedTicketNumber },
1767
- home: options.home,
1768
- });
1769
- const ticketFields = ticketAdmissionFields(inheritedTicketNumber);
1770
- const admitted = {
1771
- role: "notary",
1772
- runId,
1773
- bookKey,
1774
- projectRoot,
1775
- runDirectory,
1776
- principal,
1777
- instruction: "",
1778
- instructionEmpty: true,
1779
- attachments: [],
1780
- sourceRunPath: sourceRun.runDirectory,
1781
- sourceRun,
1782
- ...ticketFields,
1783
- ...(options.correlationId === undefined
1784
- ? {}
1785
- : { correlationId: options.correlationId }),
1786
- };
1787
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1788
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1789
- sessionDirectory,
1790
- sessionFile,
1791
- });
1792
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1793
- return {
1794
- role: "notary",
1795
- runId,
1796
- bookKey,
1797
- projectRoot,
1798
- instruction: "",
1799
- instructionEmpty: true,
1800
- attachments: [],
1801
- runDirectory,
1802
- principal,
1803
- admittedRequestPath,
1804
- sourceRunPath: sourceRun.runDirectory,
1805
- sourceRun,
1806
- ...ticketFields,
1807
- ...(options.correlationId === undefined
1808
- ? {}
1809
- : { correlationId: options.correlationId }),
1810
- };
1811
- }
1812
- /** Package-owned fixed kickoff only — never caller instruction/attachments.
1813
- * Ticket rides admitted → activation → agent-start typed bound (not free-text kickoff). */
1814
- export function buildNotaryTransportPrompt(_admitted, engineMaterial) {
1815
- return appendEngineSessionMaterial([NOTARY_FIXED_KICKOFF], engineMaterial).join("\n");
1816
- }
1438
+ /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
1439
+ /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
1440
+ const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
1817
1441
  /**
1818
- * Parse Gleaner-Left argv after the `gleaner-left` token.
1819
- * Public flags: --project, required --base. No --attach / ticket face (unanchored self-fetch).
1820
- * Instruction may be empty; callers must not pass directional instruction.
1442
+ * Parse a positive Issue number for public Doctor admission.
1443
+ * Leading zeros and non-integers are structural rejects.
1821
1444
  */
1822
- export function parseGleanerLeftArgv(args) {
1823
- let project;
1824
- let baseRevision;
1825
- const positional = [];
1826
- const tokens = [...args];
1827
- const definitions = roleOptions("gleaner-left");
1828
- const options = createTypedOptionConsumer(definitions);
1829
- while (tokens.length > 0) {
1830
- if (tokens[0] === "--") {
1831
- tokens.shift();
1832
- positional.push(...tokens);
1833
- break;
1834
- }
1835
- const taken = options.takeDashed(tokens);
1836
- if (taken !== undefined) {
1837
- if (taken.def.id === "project") {
1838
- project = requireOptionPath(taken.def.canonical, taken.value);
1839
- continue;
1840
- }
1841
- if (taken.def.id === "base") {
1842
- baseRevision = requireOptionPath(taken.def.canonical, taken.value);
1843
- continue;
1844
- }
1845
- throw new CliUsageError(`unknown gleaner-left option: ${taken.def.canonical}`);
1846
- }
1847
- const token = tokens.shift();
1848
- if (token.startsWith("-") && token !== "-") {
1849
- throw new CliUsageError(`unknown gleaner-left option: ${token}`);
1850
- }
1851
- positional.push(token);
1445
+ export function parseDoctorIssueNumber(raw) {
1446
+ const trimmed = raw.trim();
1447
+ if (!DOCTOR_ISSUE_NUMBER_PATTERN.test(trimmed)) {
1448
+ throw new CliUsageError(`doctor --issue must be a positive integer, got ${raw}`);
1852
1449
  }
1853
- options.assertRequired();
1854
- return {
1855
- instruction: positional.join(" "),
1856
- baseRevision: baseRevision,
1857
- ...(project === undefined ? {} : { project }),
1858
- };
1450
+ return Number(trimmed);
1859
1451
  }
1860
1452
  /**
1861
- * Admit a Gleaner-Left Role run on the fixed comparison base.
1862
- * Empty instruction is the lawful path; no attachment/ticket admission face.
1453
+ * Resolve the retained Doctor case runs root from Issue identity.
1454
+ * Default is the #78 book locator; optional --runs must stay project-confined
1455
+ * and match Doctor case grammar for the same issue number.
1863
1456
  */
1864
- export async function admitGleanerLeftInvocation(options) {
1865
- if (options.project !== undefined) {
1866
- requireOptionPath("--project", options.project);
1457
+ export async function resolveDoctorCaseRunsPath(options) {
1458
+ const ledgerHome = resolveActivationLedgerHome(options.home);
1459
+ // Canonical topology: <book>/<ticket>/runs (docs/dossier-topology.md).
1460
+ const defaultRuns = join(activationBookDirectory(ledgerHome, options.bookKey), String(options.issueNumber), "runs");
1461
+ if (options.runs === undefined) {
1462
+ return defaultRuns;
1867
1463
  }
1868
- if (options.baseRevision.trim() === "") {
1869
- throw new CliUsageError("--base requires a nonempty revision");
1464
+ const raw = options.runs.trim();
1465
+ if (raw === "") {
1466
+ throw new CliUsageError("doctor --runs requires a path");
1870
1467
  }
1871
- const projectRoot = resolve(options.project ?? options.cwd);
1872
- const runId = (options.createRunId ?? uuidv7)();
1873
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1874
- cwd: projectRoot,
1875
- runId,
1876
- role: "gleaner-left",
1877
- subject: { unbound: true },
1878
- home: options.home,
1879
- });
1880
- const instruction = options.instruction;
1881
- const instructionEmpty = instruction.trim() === "";
1882
- const admitted = {
1883
- role: "gleaner-left",
1884
- runId,
1885
- bookKey,
1886
- projectRoot,
1887
- runDirectory,
1888
- principal,
1889
- instruction,
1890
- instructionEmpty,
1891
- baseRevision: options.baseRevision,
1892
- attachments: [],
1893
- ...(options.correlationId === undefined
1894
- ? {}
1895
- : { correlationId: options.correlationId }),
1896
- };
1897
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
1898
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1899
- sessionDirectory,
1900
- sessionFile,
1901
- });
1902
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
1903
- return {
1904
- role: "gleaner-left",
1905
- runId,
1906
- bookKey,
1907
- projectRoot,
1908
- instruction,
1909
- instructionEmpty,
1910
- attachments: [],
1911
- runDirectory,
1912
- principal,
1913
- admittedRequestPath,
1914
- baseRevision: options.baseRevision,
1915
- ...(options.correlationId === undefined
1916
- ? {}
1917
- : { correlationId: options.correlationId }),
1918
- };
1919
- }
1920
- /** Bound comparison base is a typed fact, not a directional instruction. */
1921
- export function buildGleanerLeftTransportPrompt(admitted, engineMaterial) {
1922
- const lines = [
1923
- `左拾遗案已受理。比较基线:${admitted.baseRevision}`,
1924
- ];
1925
- if (!admitted.instructionEmpty) {
1926
- lines.push("", admitted.instruction);
1468
+ // Project-relative only — absolute overrides would bypass confinement.
1469
+ if (isAbsolute(raw)) {
1470
+ throw new CliUsageError("doctor --runs must be a project-relative path");
1927
1471
  }
1928
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1929
- }
1930
- export function parseReviewerArgv(args) {
1931
- // Spellings from PUBLIC_OPTION_TABLE.reviewer (#342). No --attach face.
1932
- const attachmentPaths = [];
1933
- const authorityRefs = [];
1934
- let project;
1935
- let baseRevision;
1936
- let lens;
1937
- const positional = [];
1938
- const tokens = [...args];
1939
- const definitions = roleOptions("reviewer");
1940
- const options = createTypedOptionConsumer(definitions);
1941
- while (tokens.length > 0) {
1942
- if (tokens[0] === "--") {
1943
- tokens.shift();
1944
- positional.push(...tokens);
1945
- break;
1946
- }
1947
- const taken = options.takeDashed(tokens);
1948
- if (taken !== undefined) {
1949
- if (taken.def.id === "project") {
1950
- project = requireOptionPath(taken.def.canonical, taken.value);
1951
- continue;
1952
- }
1953
- if (taken.def.id === "base") {
1954
- baseRevision = requireReviewerBaseRevision(taken.value);
1955
- continue;
1956
- }
1957
- if (taken.def.id === "lens") {
1958
- // Public override is single-axis only; omission stays undefined for the parallel branch.
1959
- lens = requireReviewerLens(taken.value);
1960
- continue;
1961
- }
1962
- if (taken.def.id === "authority-ref") {
1963
- authorityRefs.push(requireAuthorityRef(taken.value));
1964
- continue;
1965
- }
1966
- throw new CliUsageError(`unknown reviewer option: ${taken.def.canonical}`);
1967
- }
1968
- const token = tokens.shift();
1969
- if (token.startsWith("-") && token !== "-") {
1970
- throw new CliUsageError(`unknown reviewer option: ${token}`);
1971
- }
1972
- positional.push(token);
1472
+ const resolved = resolve(options.projectRoot, raw);
1473
+ if (resolved !== options.projectRoot &&
1474
+ !pathContainedIn(options.projectRoot, resolved)) {
1475
+ throw new CliUsageError("doctor --runs escapes the project root");
1973
1476
  }
1974
- // Base and authority are required; omitted lens selects the parallel two-axis mode.
1975
- options.assertRequired();
1976
- return {
1977
- instruction: positional.join(" "),
1978
- attachmentPaths,
1979
- baseRevision: baseRevision,
1980
- ...(lens === undefined ? {} : { lens }),
1981
- authorityRefs,
1982
- ...(project === undefined ? {} : { project }),
1983
- };
1984
- }
1985
- /**
1986
- * Admit a Reviewer Role run on the fixed base + lens + authority set.
1987
- * Caller instruction is optional provenance; typed fields project Skill inputs.
1988
- * authorityRefs are frozen as durable references only — not Spec prose.
1989
- */
1990
- export async function admitReviewerInvocation(options) {
1991
- if (options.project !== undefined) {
1992
- requireOptionPath("--project", options.project);
1477
+ let real;
1478
+ try {
1479
+ real = await realpath(resolved);
1993
1480
  }
1994
- const baseRevision = requireReviewerBaseRevision(options.baseRevision);
1995
- const lens = requireReviewerLens(options.lens);
1996
- if (options.authorityRefs.length === 0) {
1997
- throw new CliUsageError("reviewer requires --authority-ref <ref>");
1481
+ catch (error) {
1482
+ const detail = error instanceof Error ? error.message : String(error);
1483
+ throw new CliUsageError(`doctor --runs is not a readable retained runs root: ${detail}`, { cause: error });
1998
1484
  }
1999
- const authorityRefs = Object.freeze(options.authorityRefs.map((ref) => requireAuthorityRef(ref)));
2000
- const projectRoot = resolve(options.project ?? options.cwd);
2001
- const runId = (options.createRunId ?? uuidv7)();
2002
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
2003
- cwd: projectRoot,
2004
- runId,
2005
- role: "reviewer",
2006
- subject: { unbound: true },
2007
- home: options.home,
2008
- });
2009
- // Public parse already rejects attachments; keep freeze loop for structural symmetry.
2010
- const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
2011
- const instruction = options.instruction;
2012
- const instructionEmpty = instruction.trim() === "";
2013
- const admitted = {
2014
- role: "reviewer",
2015
- runId,
2016
- bookKey,
2017
- projectRoot,
2018
- runDirectory,
2019
- principal,
2020
- instruction,
2021
- instructionEmpty,
2022
- baseRevision,
2023
- lens,
2024
- authorityRefs: [...authorityRefs],
2025
- ...(options.correlationId === undefined
2026
- ? {}
2027
- : { correlationId: options.correlationId }),
2028
- attachments: attachments.map((a) => ({
2029
- provenancePath: a.provenancePath,
2030
- frozenPath: a.frozenPath,
2031
- byteLength: a.byteLength,
2032
- sha256: a.sha256,
2033
- mediaKind: a.mediaKind,
2034
- })),
2035
- };
2036
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
2037
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
2038
- sessionDirectory,
2039
- sessionFile,
2040
- });
2041
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
2042
- return {
2043
- role: "reviewer",
2044
- runId,
2045
- bookKey,
2046
- projectRoot,
2047
- instruction,
2048
- instructionEmpty,
2049
- attachments,
2050
- runDirectory,
2051
- principal,
2052
- admittedRequestPath,
2053
- baseRevision,
2054
- lens,
2055
- authorityRefs,
2056
- ...(options.correlationId === undefined
2057
- ? {}
2058
- : { correlationId: options.correlationId }),
2059
- };
1485
+ const normalized = real.split(sep).join("/");
1486
+ const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
1487
+ if (!match) {
1488
+ throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory");
1489
+ }
1490
+ if (Number(match[1]) !== options.issueNumber) {
1491
+ throw new CliUsageError(`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`);
1492
+ }
1493
+ return real;
2060
1494
  }
2061
1495
  /** Frozen Skill arg projection shared by initial and resume (never reverse-parsed). */
2062
1496
  export function buildReviewerSkillArgProjection(admitted) {
@@ -2066,72 +1500,6 @@ export function buildReviewerSkillArgProjection(admitted) {
2066
1500
  ...admitted.authorityRefs.map((ref) => `--authority ${ref}`),
2067
1501
  ].join(" ");
2068
1502
  }
2069
- /**
2070
- * Build the host-neutral prompt transport for an admitted Reviewer request.
2071
- * Typed base/lens/authority project to Skill invocation args (never reverse-parsed from prose).
2072
- * Optional caller words + engine material follow.
2073
- */
2074
- export function buildReviewerTransportPrompt(admitted, engineMaterial) {
2075
- const lines = [buildReviewerSkillArgProjection(admitted)];
2076
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
2077
- lines.push("", admitted.instruction);
2078
- }
2079
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
2080
- }
2081
- /**
2082
- * Parse Merger-specific argv after the `merger` token.
2083
- * Spellings from PUBLIC_OPTION_TABLE.merger; internal packet fields rejected (#342).
2084
- */
2085
- export function parseMergerArgv(args) {
2086
- const attachmentPaths = [];
2087
- let project;
2088
- const positional = [];
2089
- const tokens = [...args];
2090
- const definitions = roleOptions("merger");
2091
- const options = createTypedOptionConsumer(definitions);
2092
- while (tokens.length > 0) {
2093
- if (tokens[0] === "--") {
2094
- tokens.shift();
2095
- positional.push(...tokens);
2096
- break;
2097
- }
2098
- const taken = options.takeDashed(tokens);
2099
- if (taken !== undefined) {
2100
- if (taken.def.id === "attach") {
2101
- attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
2102
- continue;
2103
- }
2104
- if (taken.def.id === "project") {
2105
- project = requireOptionPath(taken.def.canonical, taken.value);
2106
- continue;
2107
- }
2108
- throw new CliUsageError(`unknown merger option: ${taken.def.canonical}`);
2109
- }
2110
- const token = tokens.shift();
2111
- // Rejected public spellings (#342) plus other internal packet field faces.
2112
- if (isRejectedPublicSpelling("merger", token) ||
2113
- token === "--targetObjectId" ||
2114
- token.startsWith("--targetObjectId=") ||
2115
- token === "--sourceObjectId" ||
2116
- token.startsWith("--sourceObjectId=") ||
2117
- token === "--expectedConflictPaths" ||
2118
- token.startsWith("--expectedConflictPaths=") ||
2119
- token === "--resolutionScope" ||
2120
- token.startsWith("--resolutionScope=")) {
2121
- throw new CliUsageError("merger does not accept public packet fields; the adapter reads Git merge materials");
2122
- }
2123
- if (token.startsWith("-") && token !== "-") {
2124
- throw new CliUsageError(`unknown merger option: ${token}`);
2125
- }
2126
- positional.push(token);
2127
- }
2128
- options.assertRequired();
2129
- return {
2130
- instruction: positional.join(" "),
2131
- attachmentPaths,
2132
- ...(project === undefined ? {} : { project }),
2133
- };
2134
- }
2135
1503
  function mergerMaterialFromUtf8(text) {
2136
1504
  const bytes = Buffer.from(text, "utf8");
2137
1505
  return Object.freeze({
@@ -2155,115 +1523,6 @@ export async function deriveMergerEnvelopeFromActiveMerge(projectRoot, gitState
2155
1523
  resolutionScope,
2156
1524
  });
2157
1525
  }
2158
- /**
2159
- * Admit a Merger Role run on the common Invocation request.
2160
- * Git materials (parents, conflicts, scope) are read from the worktree and
2161
- * handed to the role as assignment materials — including when empty (#827).
2162
- * Callers never supply public packet fields for those facts.
2163
- */
2164
- export async function admitMergerInvocation(options) {
2165
- if (options.project !== undefined) {
2166
- requireOptionPath("--project", options.project);
2167
- }
2168
- const instruction = options.instruction;
2169
- if (instruction.trim() === "") {
2170
- throw new CliUsageError("merger requires a nonblank task instruction");
2171
- }
2172
- const projectRoot = resolve(options.project ?? options.cwd);
2173
- const derived = await deriveMergerEnvelopeFromActiveMerge(projectRoot, options.gitState ?? createProductionMergerGitState(projectRoot));
2174
- const runId = (options.createRunId ?? uuidv7)();
2175
- const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
2176
- cwd: projectRoot,
2177
- runId,
2178
- role: "merger",
2179
- subject: { unbound: true },
2180
- home: options.home,
2181
- });
2182
- const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
2183
- // Intent materials seed primary-source investigation; the method owns the work.
2184
- const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;
2185
- const sourceLabel = derived.sourceObjectId === "" ? "(none observed)" : derived.sourceObjectId;
2186
- const targetIntent = mergerMaterialFromUtf8(`Investigate primary sources for target parent ${targetLabel}. Do not invent intent.`);
2187
- const sourceIntent = mergerMaterialFromUtf8(`Investigate primary sources for source parent ${sourceLabel}. Do not invent intent.`);
2188
- const taskMaterial = mergerMaterialFromUtf8(instruction);
2189
- const authorityMaterial = mergerMaterialFromUtf8(instruction);
2190
- const mergerInput = validateMergerInput({
2191
- attemptId: runId,
2192
- targetObjectId: derived.targetObjectId,
2193
- sourceObjectId: derived.sourceObjectId,
2194
- materials: {
2195
- task: taskMaterial,
2196
- authority: authorityMaterial,
2197
- targetIntent,
2198
- sourceIntent,
2199
- },
2200
- expectedConflictPaths: [...derived.expectedConflictPaths],
2201
- resolutionScope: [...derived.resolutionScope],
2202
- authorizedChecks: [],
2203
- });
2204
- const mergerInputPath = join(runDirectory, "merger-input.json");
2205
- await writeFile(mergerInputPath, `${JSON.stringify(mergerInput, null, 2)}\n`, "utf8");
2206
- const admitted = {
2207
- role: "merger",
2208
- runId,
2209
- bookKey,
2210
- projectRoot,
2211
- runDirectory,
2212
- principal,
2213
- instruction,
2214
- instructionEmpty: false,
2215
- mergerInputPath,
2216
- derived: {
2217
- targetObjectId: derived.targetObjectId,
2218
- sourceObjectId: derived.sourceObjectId,
2219
- expectedConflictPaths: [...derived.expectedConflictPaths],
2220
- resolutionScope: [...derived.resolutionScope],
2221
- },
2222
- attachments: attachments.map((a) => ({
2223
- provenancePath: a.provenancePath,
2224
- frozenPath: a.frozenPath,
2225
- byteLength: a.byteLength,
2226
- sha256: a.sha256,
2227
- mediaKind: a.mediaKind,
2228
- })),
2229
- };
2230
- const admittedRequestPath = join(runDirectory, "admitted-request.json");
2231
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
2232
- sessionDirectory,
2233
- sessionFile,
2234
- });
2235
- await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
2236
- return {
2237
- role: "merger",
2238
- runId,
2239
- bookKey,
2240
- projectRoot,
2241
- instruction,
2242
- instructionEmpty: false,
2243
- attachments,
2244
- runDirectory,
2245
- principal,
2246
- admittedRequestPath,
2247
- mergerInputPath,
2248
- derived: admitted.derived,
2249
- };
2250
- }
2251
- /**
2252
- * Build the host-neutral prompt transport for an admitted Merger request.
2253
- * Method material binds on RoleTurnRequest.methods; Pi-native `/skill:` form
2254
- * is adapter-internal only (ADR 0082).
2255
- */
2256
- export function buildMergerTransportPrompt(admitted, engineMaterial) {
2257
- const lines = [admitted.instruction];
2258
- if (admitted.attachments.length > 0) {
2259
- lines.push("");
2260
- lines.push("已受理附件(冻结快照路径):");
2261
- for (const attachment of admitted.attachments) {
2262
- lines.push(`- ${attachment.frozenPath}`);
2263
- }
2264
- }
2265
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
2266
- }
2267
1526
  const ANALYST_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
2268
1527
  /**
2269
1528
  * Parse a positive ticket / issue number for public analyst admission.
@@ -2369,7 +1628,6 @@ function requireOptionValue(flag, value, what) {
2369
1628
  */
2370
1629
  export function parseAnalystArgv(args) {
2371
1630
  const valueLists = new Map();
2372
- const tokens = [...args];
2373
1631
  const definitions = roleOptions("analyst");
2374
1632
  // Shared typed consumer: dashed + positional take, repeatable, required (#342).
2375
1633
  const options = createTypedOptionConsumer(definitions);
@@ -2380,63 +1638,61 @@ export function parseAnalystArgv(args) {
2380
1638
  else
2381
1639
  existing.push(value);
2382
1640
  };
2383
- while (tokens.length > 0) {
2384
- if (tokens[0] === "--") {
2385
- tokens.shift();
2386
- if (tokens.length > 0) {
2387
- throw new CliUsageError(`unexpected analyst argument: ${tokens[0]}`);
2388
- }
2389
- break;
2390
- }
2391
- const taken = options.takeDashed(tokens);
2392
- if (taken !== undefined) {
1641
+ scanPublicArgv(args, options, {
1642
+ onDashed(taken) {
2393
1643
  if (taken.def.valueMetavar === null) {
2394
1644
  pushValue(taken.def.id, "");
2395
- continue;
1645
+ return;
2396
1646
  }
2397
1647
  if (taken.def.id === "ticket") {
2398
1648
  if (taken.value === undefined || taken.value.trim() === "") {
2399
1649
  throw new CliUsageError("analyst --ticket requires a positive integer");
2400
1650
  }
2401
1651
  pushValue("ticket", taken.value);
2402
- continue;
1652
+ return;
2403
1653
  }
2404
1654
  if (taken.def.id === "attach") {
2405
1655
  pushValue("attach", requireOptionPath(taken.def.canonical, taken.value));
2406
- continue;
1656
+ return;
2407
1657
  }
2408
1658
  if (taken.def.id === "group-a-label" || taken.def.id === "group-b-label") {
2409
1659
  pushValue(taken.def.id, requireOptionValue(taken.def.canonical, taken.value, "a label"));
2410
- continue;
1660
+ return;
2411
1661
  }
2412
1662
  if (taken.def.id === "group-a-issues" || taken.def.id === "group-b-issues") {
2413
1663
  pushValue(taken.def.id, requireOptionValue(taken.def.canonical, taken.value, "a comma-separated list of N or book:N"));
2414
- continue;
1664
+ return;
2415
1665
  }
2416
1666
  throw new CliUsageError(`unknown analyst option: ${taken.def.canonical}`);
2417
- }
2418
- const token = tokens.shift();
2419
- // #399: deleted --project-root; disabled --model-groups public face.
2420
- if (isRejectedPublicSpelling("analyst", token)) {
2421
- if (token === "--project-root" || token.startsWith("--project-root=")) {
2422
- throw new CliUsageError("analyst no longer accepts --project-root (deleted); use bare call for whole book or --ticket N (cwd git common-dir selects the book)");
1667
+ },
1668
+ onBare(token) {
1669
+ // #399: deleted --project-root; disabled --model-groups public face.
1670
+ if (isRejectedPublicSpelling("analyst", token)) {
1671
+ if (token === "--project-root" || token.startsWith("--project-root=")) {
1672
+ throw new CliUsageError("analyst no longer accepts --project-root (deleted); use bare call for whole book or --ticket N (cwd git common-dir selects the book)");
1673
+ }
1674
+ if (token === "--model-groups" || token.startsWith("--model-groups=")) {
1675
+ throw new CliUsageError("analyst --model-groups public CLI face is disabled; input face is being redesigned for multi-issue comparison (see follow-up ticket)");
1676
+ }
1677
+ throw new CliUsageError(`unknown analyst option: ${token}`);
2423
1678
  }
2424
- if (token === "--model-groups" || token.startsWith("--model-groups=")) {
2425
- throw new CliUsageError("analyst --model-groups public CLI face is disabled; input face is being redesigned for multi-issue comparison (see follow-up ticket)");
1679
+ if (token.startsWith("-") && token !== "-") {
1680
+ throw new CliUsageError(`unknown analyst option: ${token}`);
2426
1681
  }
2427
- throw new CliUsageError(`unknown analyst option: ${token}`);
2428
- }
2429
- if (token.startsWith("-") && token !== "-") {
2430
- throw new CliUsageError(`unknown analyst option: ${token}`);
2431
- }
2432
- // Positional selectors (e.g. sweep) via shared typed consumer — not a parallel list.
2433
- const positional = options.takePositional(token);
2434
- if (positional !== undefined) {
2435
- pushValue(positional.id, "");
2436
- continue;
2437
- }
2438
- throw new CliUsageError(`unexpected analyst argument: ${token}`);
2439
- }
1682
+ // Positional selectors (e.g. sweep) via shared typed consumer — not a parallel list.
1683
+ const positional = options.takePositional(token);
1684
+ if (positional !== undefined) {
1685
+ pushValue(positional.id, "");
1686
+ return;
1687
+ }
1688
+ throw new CliUsageError(`unexpected analyst argument: ${token}`);
1689
+ },
1690
+ onDoubleDash(rest) {
1691
+ if (rest.length > 0) {
1692
+ throw new CliUsageError(`unexpected analyst argument: ${rest[0]}`);
1693
+ }
1694
+ },
1695
+ });
2440
1696
  const counts = new Map();
2441
1697
  for (const [id, values] of valueLists) {
2442
1698
  counts.set(id, values.length);