@akagilnc/pi-workflow-roles 0.1.2033 → 0.1.2043

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.
@@ -48,6 +48,15 @@ import {
48
48
  import { sha256Hex } from "../sha256.ts";
49
49
  import { uuidv7 } from "../uuidv7.ts";
50
50
  import { CliUsageError } from "./cli-errors.ts";
51
+ import {
52
+ REJECTED_PUBLIC_SPELLINGS,
53
+ createTypedOptionConsumer,
54
+ evaluateTaishiModeOptionContract,
55
+ optionsForOwner,
56
+ resolveTaishiMode,
57
+ type OptionOwner,
58
+ type PublicOptionDefinition,
59
+ } from "./option-definitions.ts";
51
60
 
52
61
  export type FrozenAttachment = {
53
62
  /** Original caller path retained only as provenance. */
@@ -448,13 +457,7 @@ export class MergerEnvelopeDerivationError extends Error {
448
457
 
449
458
  /** Reject missing/blank path values so empty overrides cannot silently degrade. */
450
459
  function requireOptionPath(
451
- flag:
452
- | "--project"
453
- | "--attach"
454
- | "--prerequisites"
455
- | "--request-manifest"
456
- | "--base"
457
- | "--project-root",
460
+ flag: string,
458
461
  value: string | undefined,
459
462
  ): string {
460
463
  if (value === undefined || value.trim() === "") {
@@ -467,6 +470,22 @@ function requireOptionPath(
467
470
  return value;
468
471
  }
469
472
 
473
+ /** True when token is a retained rejected spelling for the owner (#342). */
474
+ function isRejectedPublicSpelling(owner: OptionOwner, token: string): boolean {
475
+ for (const entry of REJECTED_PUBLIC_SPELLINGS) {
476
+ if (entry.owner !== owner) continue;
477
+ for (const spelling of entry.spellings) {
478
+ if (token === spelling || token.startsWith(`${spelling}=`)) return true;
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+
484
+ /** Role option definitions — sole spelling source for the matching parser. */
485
+ function roleOptions(owner: Exclude<OptionOwner, "global">): readonly PublicOptionDefinition[] {
486
+ return optionsForOwner(owner);
487
+ }
488
+
470
489
  /**
471
490
  * Public --authority-ref admission grammar (refs-only).
472
491
  * Unique owner for fresh argv and durable resume restore — no string-only parallel.
@@ -488,47 +507,37 @@ export function requireAuthorityRef(value: string | undefined): string {
488
507
 
489
508
  /**
490
509
  * Parse Judge-specific argv after the `judge` token.
491
- * Rejects any public burden selector/hint and unknown flags.
510
+ * Spellings from PUBLIC_OPTION_TABLE.judge; rejects burden family (#342).
492
511
  */
493
512
  export function parseJudgeArgv(args: readonly string[]): ParseJudgeArgvResult {
494
513
  const attachmentPaths: string[] = [];
495
514
  let project: string | undefined;
496
515
  const positional: string[] = [];
497
516
  const tokens = [...args];
517
+ const definitions = roleOptions("judge");
518
+ const options = createTypedOptionConsumer(definitions);
498
519
 
499
520
  while (tokens.length > 0) {
500
- const token = tokens.shift()!;
501
- if (token === "--") {
521
+ if (tokens[0] === "--") {
522
+ tokens.shift();
502
523
  positional.push(...tokens);
503
524
  break;
504
525
  }
505
- if (token === "--attach") {
506
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
507
- continue;
508
- }
509
- if (token.startsWith("--attach=")) {
510
- attachmentPaths.push(
511
- requireOptionPath("--attach", token.slice("--attach=".length)),
512
- );
513
- continue;
514
- }
515
- if (token === "--project") {
516
- project = requireOptionPath("--project", tokens.shift());
517
- continue;
518
- }
519
- if (token.startsWith("--project=")) {
520
- project = requireOptionPath("--project", token.slice("--project=".length));
521
- continue;
526
+ const taken = options.takeDashed(tokens);
527
+ if (taken !== undefined) {
528
+ if (taken.def.id === "attach") {
529
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
530
+ continue;
531
+ }
532
+ if (taken.def.id === "project") {
533
+ project = requireOptionPath(taken.def.canonical, taken.value);
534
+ continue;
535
+ }
536
+ throw new CliUsageError(`unknown judge option: ${taken.def.canonical}`);
522
537
  }
523
- // Judge owns burden inference — no public burden selector or hint.
524
- if (
525
- token === "--burden" ||
526
- token.startsWith("--burden=") ||
527
- token === "--ak-judge-burden" ||
528
- token.startsWith("--ak-judge-burden=") ||
529
- token === "--judge-burden" ||
530
- token.startsWith("--judge-burden=")
531
- ) {
538
+ const token = tokens.shift()!;
539
+ // Judge owns burden inference — rejected spellings from REJECTED_PUBLIC_SPELLINGS.
540
+ if (isRejectedPublicSpelling("judge", token)) {
532
541
  throw new CliUsageError(
533
542
  "judge does not accept a public burden selector; Judge infers its own burden",
534
543
  );
@@ -539,6 +548,7 @@ export function parseJudgeArgv(args: readonly string[]): ParseJudgeArgvResult {
539
548
  positional.push(token);
540
549
  }
541
550
 
551
+ options.assertRequired();
542
552
  return {
543
553
  instruction: positional.join(" "),
544
554
  attachmentPaths,
@@ -548,49 +558,44 @@ export function parseJudgeArgv(args: readonly string[]): ParseJudgeArgvResult {
548
558
 
549
559
  /**
550
560
  * Parse Coder-specific argv after the `coder` token.
551
- * Phase defaults to apply; explicit `plan` or `apply` as the first positional is preserved.
552
- * Common Invocation flags: --attach / --project.
561
+ * Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.coder (#342).
553
562
  */
554
563
  export function parseCoderArgv(args: readonly string[]): ParseCoderArgvResult {
555
564
  const attachmentPaths: string[] = [];
556
565
  let project: string | undefined;
557
566
  const positional: string[] = [];
558
567
  const tokens = [...args];
568
+ const definitions = roleOptions("coder");
569
+ const options = createTypedOptionConsumer(definitions);
559
570
 
560
571
  while (tokens.length > 0) {
561
- const token = tokens.shift()!;
562
- if (token === "--") {
572
+ if (tokens[0] === "--") {
573
+ tokens.shift();
563
574
  positional.push(...tokens);
564
575
  break;
565
576
  }
566
- if (token === "--attach") {
567
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
568
- continue;
569
- }
570
- if (token.startsWith("--attach=")) {
571
- attachmentPaths.push(
572
- requireOptionPath("--attach", token.slice("--attach=".length)),
573
- );
574
- continue;
575
- }
576
- if (token === "--project") {
577
- project = requireOptionPath("--project", tokens.shift());
578
- continue;
579
- }
580
- if (token.startsWith("--project=")) {
581
- project = requireOptionPath("--project", token.slice("--project=".length));
582
- continue;
577
+ const taken = options.takeDashed(tokens);
578
+ if (taken !== undefined) {
579
+ if (taken.def.id === "attach") {
580
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
581
+ continue;
582
+ }
583
+ if (taken.def.id === "project") {
584
+ project = requireOptionPath(taken.def.canonical, taken.value);
585
+ continue;
586
+ }
587
+ throw new CliUsageError(`unknown coder option: ${taken.def.canonical}`);
583
588
  }
589
+ const token = tokens.shift()!;
584
590
  if (token.startsWith("-") && token !== "-") {
585
591
  throw new CliUsageError(`unknown coder option: ${token}`);
586
592
  }
587
593
  positional.push(token);
588
594
  }
589
595
 
590
- let phase: CoderPhase = "apply";
591
- if (positional[0] === "plan" || positional[0] === "apply") {
592
- phase = positional.shift() as CoderPhase;
593
- }
596
+ // Phase aliases + default come solely from the typed coder phase row (#342).
597
+ const phase = options.consumeLeadingPhase(positional);
598
+ options.assertRequired();
594
599
 
595
600
  return {
596
601
  phase,
@@ -602,8 +607,7 @@ export function parseCoderArgv(args: readonly string[]): ParseCoderArgvResult {
602
607
 
603
608
  /**
604
609
  * Parse Fixer-specific argv after the `fixer` token.
605
- * Phase defaults to apply; explicit `plan` or `apply` as the first positional is preserved.
606
- * Common Invocation flags: --attach / --project. Role-specific: optional --prerequisites.
610
+ * Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.fixer (#342).
607
611
  */
608
612
  export function parseFixerArgv(args: readonly string[]): ParseFixerArgvResult {
609
613
  const attachmentPaths: string[] = [];
@@ -611,52 +615,41 @@ export function parseFixerArgv(args: readonly string[]): ParseFixerArgvResult {
611
615
  let prerequisitesPath: string | undefined;
612
616
  const positional: string[] = [];
613
617
  const tokens = [...args];
618
+ const definitions = roleOptions("fixer");
619
+ const options = createTypedOptionConsumer(definitions);
614
620
 
615
621
  while (tokens.length > 0) {
616
- const token = tokens.shift()!;
617
- if (token === "--") {
622
+ if (tokens[0] === "--") {
623
+ tokens.shift();
618
624
  positional.push(...tokens);
619
625
  break;
620
626
  }
621
- if (token === "--attach") {
622
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
623
- continue;
624
- }
625
- if (token.startsWith("--attach=")) {
626
- attachmentPaths.push(
627
- requireOptionPath("--attach", token.slice("--attach=".length)),
628
- );
629
- continue;
630
- }
631
- if (token === "--project") {
632
- project = requireOptionPath("--project", tokens.shift());
633
- continue;
634
- }
635
- if (token.startsWith("--project=")) {
636
- project = requireOptionPath("--project", token.slice("--project=".length));
637
- continue;
638
- }
639
- if (token === "--prerequisites") {
640
- prerequisitesPath = requireOptionPath("--prerequisites", tokens.shift());
641
- continue;
642
- }
643
- if (token.startsWith("--prerequisites=")) {
644
- prerequisitesPath = requireOptionPath(
645
- "--prerequisites",
646
- token.slice("--prerequisites=".length),
647
- );
648
- continue;
627
+ const taken = options.takeDashed(tokens);
628
+ if (taken !== undefined) {
629
+ if (taken.def.id === "attach") {
630
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
631
+ continue;
632
+ }
633
+ if (taken.def.id === "project") {
634
+ project = requireOptionPath(taken.def.canonical, taken.value);
635
+ continue;
636
+ }
637
+ if (taken.def.id === "prerequisites") {
638
+ prerequisitesPath = requireOptionPath(taken.def.canonical, taken.value);
639
+ continue;
640
+ }
641
+ throw new CliUsageError(`unknown fixer option: ${taken.def.canonical}`);
649
642
  }
643
+ const token = tokens.shift()!;
650
644
  if (token.startsWith("-") && token !== "-") {
651
645
  throw new CliUsageError(`unknown fixer option: ${token}`);
652
646
  }
653
647
  positional.push(token);
654
648
  }
655
649
 
656
- let phase: FixerPhase = "apply";
657
- if (positional[0] === "plan" || positional[0] === "apply") {
658
- phase = positional.shift() as FixerPhase;
659
- }
650
+ // Phase aliases + default come solely from the typed fixer phase row (#342).
651
+ const phase = options.consumeLeadingPhase(positional);
652
+ options.assertRequired();
660
653
 
661
654
  return {
662
655
  phase,
@@ -1139,6 +1132,7 @@ function parseRepoOption(raw: string | undefined): string {
1139
1132
  return raw;
1140
1133
  }
1141
1134
  export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvResult {
1135
+ // Spellings from PUBLIC_OPTION_TABLE.collector (#342).
1142
1136
  const attachmentPaths: string[] = [];
1143
1137
  let project: string | undefined;
1144
1138
  let repo: string | undefined;
@@ -1146,24 +1140,54 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
1146
1140
  let requestManifestPath: string | undefined;
1147
1141
  const positional: string[] = [];
1148
1142
  const tokens = [...args];
1143
+ const definitions = roleOptions("collector");
1144
+ const options = createTypedOptionConsumer(definitions);
1149
1145
  while (tokens.length > 0) {
1146
+ if (tokens[0] === "--") {
1147
+ tokens.shift();
1148
+ positional.push(...tokens);
1149
+ break;
1150
+ }
1151
+ const taken = options.takeDashed(tokens);
1152
+ if (taken !== undefined) {
1153
+ if (taken.def.id === "attach") {
1154
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
1155
+ continue;
1156
+ }
1157
+ if (taken.def.id === "project") {
1158
+ project = requireOptionPath(taken.def.canonical, taken.value);
1159
+ continue;
1160
+ }
1161
+ if (taken.def.id === "pr") {
1162
+ prNumber = parsePositivePrOption(taken.value);
1163
+ continue;
1164
+ }
1165
+ if (taken.def.id === "repo") {
1166
+ repo = parseRepoOption(taken.value);
1167
+ continue;
1168
+ }
1169
+ if (taken.def.id === "request-manifest") {
1170
+ requestManifestPath = requireOptionPath(taken.def.canonical, taken.value);
1171
+ continue;
1172
+ }
1173
+ throw new CliUsageError(`unknown collector option: ${taken.def.canonical}`);
1174
+ }
1150
1175
  const token = tokens.shift()!;
1151
- if (token === "--") { positional.push(...tokens); break; }
1152
- if (token === "--attach") { attachmentPaths.push(requireOptionPath("--attach", tokens.shift())); continue; }
1153
- if (token.startsWith("--attach=")) { attachmentPaths.push(requireOptionPath("--attach", token.slice(9))); continue; }
1154
- if (token === "--project") { project = requireOptionPath("--project", tokens.shift()); continue; }
1155
- if (token.startsWith("--project=")) { project = requireOptionPath("--project", token.slice(10)); continue; }
1156
- if (token === "--pr") { prNumber = parsePositivePrOption(tokens.shift()); continue; }
1157
- if (token.startsWith("--pr=")) { prNumber = parsePositivePrOption(token.slice(5)); continue; }
1158
- if (token === "--repo") { repo = parseRepoOption(tokens.shift()); continue; }
1159
- if (token.startsWith("--repo=")) { repo = parseRepoOption(token.slice(7)); continue; }
1160
- if (token === "--request-manifest") { requestManifestPath = requireOptionPath("--request-manifest", tokens.shift()); continue; }
1161
- if (token.startsWith("--request-manifest=")) { requestManifestPath = requireOptionPath("--request-manifest", token.slice(19)); continue; }
1162
- if (token.startsWith("-") && token !== "-") throw new CliUsageError(`unknown collector option: ${token}`);
1176
+ if (token.startsWith("-") && token !== "-") {
1177
+ throw new CliUsageError(`unknown collector option: ${token}`);
1178
+ }
1163
1179
  positional.push(token);
1164
1180
  }
1165
- if (prNumber === undefined) throw new CliUsageError("collector requires --pr <positive-integer>");
1166
- return { prNumber, instruction: positional.join(" "), attachmentPaths, ...(project === undefined ? {} : { project }), ...(repo === undefined ? {} : { repo }), ...(requestManifestPath === undefined ? {} : { requestManifestPath }) };
1181
+ // Unconditional required (e.g. --pr) from typed table via shared consumer (#342).
1182
+ options.assertRequired();
1183
+ return {
1184
+ prNumber: prNumber!,
1185
+ instruction: positional.join(" "),
1186
+ attachmentPaths,
1187
+ ...(project === undefined ? {} : { project }),
1188
+ ...(repo === undefined ? {} : { repo }),
1189
+ ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1190
+ };
1167
1191
  }
1168
1192
 
1169
1193
  /**
@@ -1409,78 +1433,58 @@ export function parseDoctorIssueNumber(raw: string): number {
1409
1433
  * Requires --issue; optional confined --runs override; common --attach/--project.
1410
1434
  */
1411
1435
  export function parseDoctorArgv(args: readonly string[]): ParseDoctorArgvResult {
1436
+ // Spellings from PUBLIC_OPTION_TABLE.doctor (#342).
1412
1437
  const attachmentPaths: string[] = [];
1413
1438
  let project: string | undefined;
1414
1439
  let issueRaw: string | undefined;
1415
1440
  let runs: string | undefined;
1416
1441
  const positional: string[] = [];
1417
1442
  const tokens = [...args];
1443
+ const definitions = roleOptions("doctor");
1444
+ const options = createTypedOptionConsumer(definitions);
1418
1445
 
1419
1446
  while (tokens.length > 0) {
1420
- const token = tokens.shift()!;
1421
- if (token === "--") {
1447
+ if (tokens[0] === "--") {
1448
+ tokens.shift();
1422
1449
  positional.push(...tokens);
1423
1450
  break;
1424
1451
  }
1425
- if (token === "--issue") {
1426
- const value = tokens.shift();
1427
- if (value === undefined || value.trim() === "") {
1428
- throw new CliUsageError("doctor --issue requires a positive integer");
1452
+ const taken = options.takeDashed(tokens);
1453
+ if (taken !== undefined) {
1454
+ if (taken.def.id === "issue") {
1455
+ if (taken.value === undefined || taken.value.trim() === "") {
1456
+ throw new CliUsageError("doctor --issue requires a positive integer");
1457
+ }
1458
+ issueRaw = taken.value;
1459
+ continue;
1429
1460
  }
1430
- issueRaw = value;
1431
- continue;
1432
- }
1433
- if (token.startsWith("--issue=")) {
1434
- issueRaw = token.slice("--issue=".length);
1435
- if (issueRaw.trim() === "") {
1436
- throw new CliUsageError("doctor --issue requires a positive integer");
1461
+ if (taken.def.id === "runs") {
1462
+ if (taken.value === undefined || taken.value.trim() === "") {
1463
+ throw new CliUsageError("doctor --runs requires a path");
1464
+ }
1465
+ runs = taken.value;
1466
+ continue;
1437
1467
  }
1438
- continue;
1439
- }
1440
- if (token === "--runs") {
1441
- const value = tokens.shift();
1442
- if (value === undefined || value.trim() === "") {
1443
- throw new CliUsageError("doctor --runs requires a path");
1468
+ if (taken.def.id === "attach") {
1469
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
1470
+ continue;
1444
1471
  }
1445
- runs = value;
1446
- continue;
1447
- }
1448
- if (token.startsWith("--runs=")) {
1449
- const value = token.slice("--runs=".length);
1450
- if (value.trim() === "") {
1451
- throw new CliUsageError("doctor --runs requires a path");
1472
+ if (taken.def.id === "project") {
1473
+ project = requireOptionPath(taken.def.canonical, taken.value);
1474
+ continue;
1452
1475
  }
1453
- runs = value;
1454
- continue;
1455
- }
1456
- if (token === "--attach") {
1457
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
1458
- continue;
1459
- }
1460
- if (token.startsWith("--attach=")) {
1461
- attachmentPaths.push(
1462
- requireOptionPath("--attach", token.slice("--attach=".length)),
1463
- );
1464
- continue;
1465
- }
1466
- if (token === "--project") {
1467
- project = requireOptionPath("--project", tokens.shift());
1468
- continue;
1469
- }
1470
- if (token.startsWith("--project=")) {
1471
- project = requireOptionPath("--project", token.slice("--project=".length));
1472
- continue;
1476
+ throw new CliUsageError(`unknown doctor option: ${taken.def.canonical}`);
1473
1477
  }
1478
+ const token = tokens.shift()!;
1474
1479
  if (token.startsWith("-") && token !== "-") {
1475
1480
  throw new CliUsageError(`unknown doctor option: ${token}`);
1476
1481
  }
1477
1482
  positional.push(token);
1478
1483
  }
1479
1484
 
1480
- if (issueRaw === undefined) {
1481
- throw new CliUsageError("doctor requires --issue <positive-integer>");
1482
- }
1483
- const issueNumber = parseDoctorIssueNumber(issueRaw);
1485
+ // Unconditional required (e.g. --issue) from typed table via shared consumer (#342).
1486
+ options.assertRequired();
1487
+ const issueNumber = parseDoctorIssueNumber(issueRaw!);
1484
1488
 
1485
1489
  if (runs !== undefined && runs.trim() === "") {
1486
1490
  throw new CliUsageError("doctor --runs requires a path");
@@ -1731,56 +1735,51 @@ export function buildDoctorTransportPrompt(
1731
1735
  export function parseReviewerArgv(
1732
1736
  args: readonly string[],
1733
1737
  ): ParseReviewerArgvResult {
1738
+ // Spellings from PUBLIC_OPTION_TABLE.reviewer (#342). No --attach face.
1734
1739
  const attachmentPaths: string[] = [];
1735
1740
  const authorityRefs: string[] = [];
1736
1741
  let project: string | undefined;
1737
1742
  let baseRevision: string | undefined;
1738
1743
  const positional: string[] = [];
1739
1744
  const tokens = [...args];
1745
+ const definitions = roleOptions("reviewer");
1746
+ const options = createTypedOptionConsumer(definitions);
1740
1747
 
1741
1748
  while (tokens.length > 0) {
1742
- const token = tokens.shift()!;
1743
- if (token === "--") {
1749
+ if (tokens[0] === "--") {
1750
+ tokens.shift();
1744
1751
  positional.push(...tokens);
1745
1752
  break;
1746
1753
  }
1747
- if (token === "--project") {
1748
- project = requireOptionPath("--project", tokens.shift());
1749
- continue;
1750
- }
1751
- if (token.startsWith("--project=")) {
1752
- project = requireOptionPath("--project", token.slice("--project=".length));
1753
- continue;
1754
- }
1755
- if (token === "--base") {
1756
- baseRevision = requireOptionPath("--base", tokens.shift());
1757
- continue;
1758
- }
1759
- if (token.startsWith("--base=")) {
1760
- baseRevision = requireOptionPath("--base", token.slice("--base=".length));
1761
- continue;
1762
- }
1763
- if (token === "--authority-ref") {
1764
- authorityRefs.push(requireAuthorityRef(tokens.shift()));
1765
- continue;
1766
- }
1767
- if (token.startsWith("--authority-ref=")) {
1768
- authorityRefs.push(requireAuthorityRef(token.slice("--authority-ref=".length)));
1769
- continue;
1754
+ const taken = options.takeDashed(tokens);
1755
+ if (taken !== undefined) {
1756
+ if (taken.def.id === "project") {
1757
+ project = requireOptionPath(taken.def.canonical, taken.value);
1758
+ continue;
1759
+ }
1760
+ if (taken.def.id === "base") {
1761
+ baseRevision = requireOptionPath(taken.def.canonical, taken.value);
1762
+ continue;
1763
+ }
1764
+ if (taken.def.id === "authority-ref") {
1765
+ authorityRefs.push(requireAuthorityRef(taken.value));
1766
+ continue;
1767
+ }
1768
+ throw new CliUsageError(`unknown reviewer option: ${taken.def.canonical}`);
1770
1769
  }
1770
+ const token = tokens.shift()!;
1771
1771
  if (token.startsWith("-") && token !== "-") {
1772
1772
  throw new CliUsageError(`unknown reviewer option: ${token}`);
1773
1773
  }
1774
1774
  positional.push(token);
1775
1775
  }
1776
1776
 
1777
- if (baseRevision === undefined) {
1778
- throw new CliUsageError("reviewer requires --base <revision>; canonical code-review requires the caller to select a fixed point");
1779
- }
1777
+ // Unconditional required (e.g. --base) from typed table via shared consumer (#342).
1778
+ options.assertRequired();
1780
1779
  return {
1781
1780
  instruction: positional.join(" "),
1782
1781
  attachmentPaths,
1783
- baseRevision,
1782
+ baseRevision: baseRevision!,
1784
1783
  authorityRefs,
1785
1784
  ...(project === undefined ? {} : { project }),
1786
1785
  };
@@ -1898,43 +1897,38 @@ export function buildReviewerTransportPrompt(
1898
1897
 
1899
1898
  /**
1900
1899
  * Parse Merger-specific argv after the `merger` token.
1901
- * Common Invocation flags only: --attach / --project.
1902
- * Parents, conflicts, scope, and internal merger-input are never public fields.
1900
+ * Spellings from PUBLIC_OPTION_TABLE.merger; internal packet fields rejected (#342).
1903
1901
  */
1904
1902
  export function parseMergerArgv(args: readonly string[]): ParseMergerArgvResult {
1905
1903
  const attachmentPaths: string[] = [];
1906
1904
  let project: string | undefined;
1907
1905
  const positional: string[] = [];
1908
1906
  const tokens = [...args];
1907
+ const definitions = roleOptions("merger");
1908
+ const options = createTypedOptionConsumer(definitions);
1909
1909
 
1910
1910
  while (tokens.length > 0) {
1911
- const token = tokens.shift()!;
1912
- if (token === "--") {
1911
+ if (tokens[0] === "--") {
1912
+ tokens.shift();
1913
1913
  positional.push(...tokens);
1914
1914
  break;
1915
1915
  }
1916
- if (token === "--attach") {
1917
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
1918
- continue;
1919
- }
1920
- if (token.startsWith("--attach=")) {
1921
- attachmentPaths.push(
1922
- requireOptionPath("--attach", token.slice("--attach=".length)),
1923
- );
1924
- continue;
1925
- }
1926
- if (token === "--project") {
1927
- project = requireOptionPath("--project", tokens.shift());
1928
- continue;
1929
- }
1930
- if (token.startsWith("--project=")) {
1931
- project = requireOptionPath("--project", token.slice("--project=".length));
1932
- continue;
1916
+ const taken = options.takeDashed(tokens);
1917
+ if (taken !== undefined) {
1918
+ if (taken.def.id === "attach") {
1919
+ attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
1920
+ continue;
1921
+ }
1922
+ if (taken.def.id === "project") {
1923
+ project = requireOptionPath(taken.def.canonical, taken.value);
1924
+ continue;
1925
+ }
1926
+ throw new CliUsageError(`unknown merger option: ${taken.def.canonical}`);
1933
1927
  }
1934
- // Reject internal / mechanical packet fields on the public face.
1928
+ const token = tokens.shift()!;
1929
+ // Rejected public spellings (#342) plus other internal packet field faces.
1935
1930
  if (
1936
- token === "--ak-merger-input" ||
1937
- token.startsWith("--ak-merger-input=") ||
1931
+ isRejectedPublicSpelling("merger", token) ||
1938
1932
  token === "--targetObjectId" ||
1939
1933
  token.startsWith("--targetObjectId=") ||
1940
1934
  token === "--sourceObjectId" ||
@@ -1954,6 +1948,7 @@ export function parseMergerArgv(args: readonly string[]): ParseMergerArgvResult
1954
1948
  positional.push(token);
1955
1949
  }
1956
1950
 
1951
+ options.assertRequired();
1957
1952
  return {
1958
1953
  instruction: positional.join(" "),
1959
1954
  attachmentPaths,
@@ -2216,178 +2211,110 @@ function requireOptionValue(
2216
2211
 
2217
2212
  /**
2218
2213
  * Parse taishi-specific argv after the `taishi` token (#336/#337/#338).
2219
- * Issue: at least one of --ticket / --project-root.
2220
- * Sweep: positional `sweep` and/or --attach; payload is the attachment body only.
2221
- * Cohort / model-groups: explicit query flags with their own required faces.
2222
- * Faces are mutually exclusive.
2214
+ * Spellings + mode relation contracts from PUBLIC_OPTION_TABLE.taishi / TAISHI_* (#342).
2215
+ * Mode exclusion, conditional requiredness, cardinality, and at-least-one are
2216
+ * table-driven — do not restate them as parallel handwritten branches here.
2217
+ * Unconditional required:true also goes through the shared consumer.
2223
2218
  */
2224
2219
  export function parseTaishiArgv(args: readonly string[]): ParseTaishiArgvResult {
2225
- let query: "issue" | "cohort" | "model-groups" = "issue";
2226
- let ticketRaw: string | undefined;
2227
- const projectRoots: string[] = [];
2228
- let groupALabel: string | undefined;
2229
- let groupAIssuesRaw: string | undefined;
2230
- let groupBLabel: string | undefined;
2231
- let groupBIssuesRaw: string | undefined;
2232
- let sweepToken = false;
2233
- const attachmentPaths: string[] = [];
2220
+ const valueLists = new Map<string, string[]>();
2234
2221
  const tokens = [...args];
2222
+ const definitions = roleOptions("taishi");
2223
+ // Shared typed consumer: dashed + positional take, repeatable, required (#342).
2224
+ const options = createTypedOptionConsumer(definitions);
2225
+
2226
+ const pushValue = (id: string, value: string): void => {
2227
+ const existing = valueLists.get(id);
2228
+ if (existing === undefined) valueLists.set(id, [value]);
2229
+ else existing.push(value);
2230
+ };
2235
2231
 
2236
2232
  while (tokens.length > 0) {
2237
- const token = tokens.shift()!;
2238
- if (token === "--") {
2233
+ if (tokens[0] === "--") {
2234
+ tokens.shift();
2239
2235
  if (tokens.length > 0) {
2240
2236
  throw new CliUsageError(`unexpected taishi argument: ${tokens[0]}`);
2241
2237
  }
2242
2238
  break;
2243
2239
  }
2244
- if (token === "--cohort") {
2245
- if (query !== "issue") {
2246
- throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
2240
+ const taken = options.takeDashed(tokens);
2241
+ if (taken !== undefined) {
2242
+ if (taken.def.valueMetavar === null) {
2243
+ pushValue(taken.def.id, "");
2244
+ continue;
2247
2245
  }
2248
- query = "cohort";
2249
- continue;
2250
- }
2251
- if (token === "--model-groups") {
2252
- if (query !== "issue") {
2253
- throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
2246
+ if (taken.def.id === "ticket") {
2247
+ if (taken.value === undefined || taken.value.trim() === "") {
2248
+ throw new CliUsageError("taishi --ticket requires a positive integer");
2249
+ }
2250
+ pushValue("ticket", taken.value);
2251
+ continue;
2254
2252
  }
2255
- query = "model-groups";
2256
- continue;
2257
- }
2258
- if (token === "--ticket") {
2259
- const value = tokens.shift();
2260
- if (value === undefined || value.trim() === "") {
2261
- throw new CliUsageError("taishi --ticket requires a positive integer");
2253
+ if (taken.def.id === "project-root") {
2254
+ pushValue(
2255
+ "project-root",
2256
+ requireOptionPath(taken.def.canonical, taken.value),
2257
+ );
2258
+ continue;
2262
2259
  }
2263
- ticketRaw = value;
2264
- continue;
2265
- }
2266
- if (token.startsWith("--ticket=")) {
2267
- ticketRaw = token.slice("--ticket=".length);
2268
- if (ticketRaw.trim() === "") {
2269
- throw new CliUsageError("taishi --ticket requires a positive integer");
2260
+ if (taken.def.id === "attach") {
2261
+ pushValue(
2262
+ "attach",
2263
+ requireOptionPath(taken.def.canonical, taken.value),
2264
+ );
2265
+ continue;
2270
2266
  }
2271
- continue;
2272
- }
2273
- if (token === "--project-root") {
2274
- projectRoots.push(requireOptionPath("--project-root", tokens.shift()));
2275
- continue;
2276
- }
2277
- if (token.startsWith("--project-root=")) {
2278
- projectRoots.push(
2279
- requireOptionPath("--project-root", token.slice("--project-root=".length)),
2280
- );
2281
- continue;
2282
- }
2283
- if (token === "--group-a-label") {
2284
- groupALabel = requireOptionValue("--group-a-label", tokens.shift(), "a label");
2285
- continue;
2286
- }
2287
- if (token.startsWith("--group-a-label=")) {
2288
- groupALabel = requireOptionValue(
2289
- "--group-a-label",
2290
- token.slice("--group-a-label=".length),
2291
- "a label",
2292
- );
2293
- continue;
2294
- }
2295
- if (token === "--group-a-issues") {
2296
- groupAIssuesRaw = requireOptionValue(
2297
- "--group-a-issues",
2298
- tokens.shift(),
2299
- "a comma-separated positive integer list",
2300
- );
2301
- continue;
2302
- }
2303
- if (token.startsWith("--group-a-issues=")) {
2304
- groupAIssuesRaw = requireOptionValue(
2305
- "--group-a-issues",
2306
- token.slice("--group-a-issues=".length),
2307
- "a comma-separated positive integer list",
2308
- );
2309
- continue;
2310
- }
2311
- if (token === "--group-b-label") {
2312
- groupBLabel = requireOptionValue("--group-b-label", tokens.shift(), "a label");
2313
- continue;
2314
- }
2315
- if (token.startsWith("--group-b-label=")) {
2316
- groupBLabel = requireOptionValue(
2317
- "--group-b-label",
2318
- token.slice("--group-b-label=".length),
2319
- "a label",
2320
- );
2321
- continue;
2322
- }
2323
- if (token === "--group-b-issues") {
2324
- groupBIssuesRaw = requireOptionValue(
2325
- "--group-b-issues",
2326
- tokens.shift(),
2327
- "a comma-separated positive integer list",
2328
- );
2329
- continue;
2330
- }
2331
- if (token.startsWith("--group-b-issues=")) {
2332
- groupBIssuesRaw = requireOptionValue(
2333
- "--group-b-issues",
2334
- token.slice("--group-b-issues=".length),
2335
- "a comma-separated positive integer list",
2336
- );
2337
- continue;
2338
- }
2339
- if (token === "--attach") {
2340
- attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
2341
- continue;
2342
- }
2343
- if (token.startsWith("--attach=")) {
2344
- attachmentPaths.push(
2345
- requireOptionPath("--attach", token.slice("--attach=".length)),
2346
- );
2347
- continue;
2267
+ if (taken.def.id === "group-a-label" || taken.def.id === "group-b-label") {
2268
+ pushValue(
2269
+ taken.def.id,
2270
+ requireOptionValue(taken.def.canonical, taken.value, "a label"),
2271
+ );
2272
+ continue;
2273
+ }
2274
+ if (
2275
+ taken.def.id === "group-a-issues" || taken.def.id === "group-b-issues"
2276
+ ) {
2277
+ pushValue(
2278
+ taken.def.id,
2279
+ requireOptionValue(
2280
+ taken.def.canonical,
2281
+ taken.value,
2282
+ "a comma-separated positive integer list",
2283
+ ),
2284
+ );
2285
+ continue;
2286
+ }
2287
+ throw new CliUsageError(`unknown taishi option: ${taken.def.canonical}`);
2348
2288
  }
2289
+ const token = tokens.shift()!;
2349
2290
  if (token.startsWith("-") && token !== "-") {
2350
2291
  throw new CliUsageError(`unknown taishi option: ${token}`);
2351
2292
  }
2352
- // Optional sweep mode token (like coder plan/apply); only once, no other positionals.
2353
- if (token === "sweep") {
2354
- if (sweepToken) {
2355
- throw new CliUsageError("unexpected taishi argument: sweep");
2356
- }
2357
- sweepToken = true;
2293
+ // Positional selectors (e.g. sweep) via shared typed consumer — not a parallel list.
2294
+ const positional = options.takePositional(token);
2295
+ if (positional !== undefined) {
2296
+ pushValue(positional.id, "");
2358
2297
  continue;
2359
2298
  }
2360
2299
  throw new CliUsageError(`unexpected taishi argument: ${token}`);
2361
2300
  }
2362
2301
 
2363
- const hasSweepFace = sweepToken || attachmentPaths.length > 0;
2364
- const hasCohortFlags =
2365
- groupALabel !== undefined
2366
- || groupAIssuesRaw !== undefined
2367
- || groupBLabel !== undefined
2368
- || groupBIssuesRaw !== undefined;
2302
+ const counts = new Map<string, number>();
2303
+ for (const [id, values] of valueLists) {
2304
+ counts.set(id, values.length);
2305
+ }
2306
+ options.assertRequired();
2307
+ const mode = resolveTaishiMode(new Set(counts.keys()));
2308
+ const verdict = evaluateTaishiModeOptionContract(mode, counts);
2309
+ if (!verdict.ok) {
2310
+ throw new CliUsageError(verdict.message);
2311
+ }
2369
2312
 
2370
- if (query === "cohort") {
2371
- if (
2372
- groupALabel === undefined
2373
- || groupAIssuesRaw === undefined
2374
- || groupBLabel === undefined
2375
- || groupBIssuesRaw === undefined
2376
- ) {
2377
- throw new CliUsageError(
2378
- "usage: ak-role taishi --cohort --group-a-label <L> --group-a-issues <N[,N...]> --group-b-label <L> --group-b-issues <N[,N...]>",
2379
- );
2380
- }
2381
- if (ticketRaw !== undefined || projectRoots.length > 0) {
2382
- throw new CliUsageError(
2383
- "taishi --cohort does not accept --ticket or --project-root",
2384
- );
2385
- }
2386
- if (hasSweepFace) {
2387
- throw new CliUsageError(
2388
- "taishi --cohort does not accept sweep --attach",
2389
- );
2390
- }
2313
+ if (mode === "cohort") {
2314
+ const groupALabel = valueLists.get("group-a-label")![0]!;
2315
+ const groupAIssuesRaw = valueLists.get("group-a-issues")![0]!;
2316
+ const groupBLabel = valueLists.get("group-b-label")![0]!;
2317
+ const groupBIssuesRaw = valueLists.get("group-b-issues")![0]!;
2391
2318
  return {
2392
2319
  query: "cohort",
2393
2320
  groups: [
@@ -2403,58 +2330,22 @@ export function parseTaishiArgv(args: readonly string[]): ParseTaishiArgvResult
2403
2330
  };
2404
2331
  }
2405
2332
 
2406
- if (query === "model-groups") {
2407
- if (projectRoots.length === 0) {
2408
- throw new CliUsageError(
2409
- "usage: ak-role taishi --model-groups --project-root <P> [--project-root <P> ...]",
2410
- );
2411
- }
2412
- if (ticketRaw !== undefined) {
2413
- throw new CliUsageError("taishi --model-groups does not accept --ticket");
2414
- }
2415
- if (hasCohortFlags) {
2416
- throw new CliUsageError("taishi --model-groups does not accept cohort group flags");
2417
- }
2418
- if (hasSweepFace) {
2419
- throw new CliUsageError(
2420
- "taishi --model-groups does not accept sweep --attach",
2421
- );
2422
- }
2333
+ if (mode === "model-groups") {
2423
2334
  return {
2424
2335
  query: "model-groups",
2425
- projectRoots,
2336
+ projectRoots: valueLists.get("project-root") ?? [],
2426
2337
  };
2427
2338
  }
2428
2339
 
2429
- // default issue or sweep (#336/#337 faces)
2430
- if (hasCohortFlags) {
2431
- throw new CliUsageError("taishi issue query does not accept cohort group flags");
2432
- }
2433
-
2434
- if (hasSweepFace) {
2435
- if (ticketRaw !== undefined || projectRoots.length > 0) {
2436
- throw new CliUsageError(
2437
- "taishi sweep --attach cannot combine with --ticket or --project-root",
2438
- );
2439
- }
2340
+ if (mode === "sweep") {
2440
2341
  return {
2441
2342
  query: "sweep",
2442
- attachmentPaths,
2343
+ attachmentPaths: valueLists.get("attach") ?? [],
2443
2344
  };
2444
2345
  }
2445
2346
 
2446
- if (projectRoots.length > 1) {
2447
- throw new CliUsageError(
2448
- "taishi issue query accepts at most one --project-root (use --model-groups for many)",
2449
- );
2450
- }
2451
- const projectRoot = projectRoots[0];
2452
- if (ticketRaw === undefined && projectRoot === undefined) {
2453
- throw new CliUsageError(
2454
- "usage: ak-role taishi ((--ticket <N> | --project-root <P>) | [sweep] --attach <sweep.json> | --cohort ... | --model-groups ...)",
2455
- );
2456
- }
2457
-
2347
+ const ticketRaw = valueLists.get("ticket")?.[0];
2348
+ const projectRoot = valueLists.get("project-root")?.[0];
2458
2349
  return {
2459
2350
  query: "issue",
2460
2351
  ...(ticketRaw === undefined