@akagilnc/pi-workflow-roles 0.1.2169 → 0.1.2173

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.
@@ -14505,6 +14505,9 @@ async function savePublicCliConfig(config, home = homedir()) {
14505
14505
  function setPersistentSeatConfig(config, seat, selection) {
14506
14506
  const previous = config.seats[seat];
14507
14507
  return {
14508
+ // Spread preserves sibling top-level keys such as autoResumeLimit (#422):
14509
+ // a seat write must never silently drop them.
14510
+ ...config,
14508
14511
  seats: {
14509
14512
  ...config.seats,
14510
14513
  [seat]: {
@@ -14528,6 +14531,7 @@ function setPersistentSeatEngine(config, seat, engine) {
14528
14531
  if (engine === void 0) {
14529
14532
  const { engine: _dropped, ...modelOnly } = previous;
14530
14533
  return {
14534
+ ...config,
14531
14535
  seats: {
14532
14536
  ...config.seats,
14533
14537
  [seat]: modelOnly
@@ -14535,12 +14539,24 @@ function setPersistentSeatEngine(config, seat, engine) {
14535
14539
  };
14536
14540
  }
14537
14541
  return {
14542
+ ...config,
14538
14543
  seats: {
14539
14544
  ...config.seats,
14540
14545
  [seat]: { ...previous, engine }
14541
14546
  }
14542
14547
  };
14543
14548
  }
14549
+ function parseAutoResumeLimit(value) {
14550
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
14551
+ throw new Error(
14552
+ `auto-resume limit must be a non-negative integer, got ${JSON.stringify(value)}`
14553
+ );
14554
+ }
14555
+ return value;
14556
+ }
14557
+ function setAutoResumeLimit(config, limit) {
14558
+ return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
14559
+ }
14544
14560
  function seatModelOnly(seat) {
14545
14561
  return seat.thinking === void 0 ? { provider: seat.provider, model: seat.model } : { provider: seat.provider, model: seat.model, thinking: seat.thinking };
14546
14562
  }
@@ -14610,8 +14626,15 @@ function parsePublicCliConfig(value) {
14610
14626
  throw new Error("public CLI config must be an object");
14611
14627
  }
14612
14628
  const record4 = value;
14629
+ let autoResumeLimit;
14630
+ if (record4.autoResumeLimit !== void 0) {
14631
+ autoResumeLimit = parseAutoResumeLimit(record4.autoResumeLimit);
14632
+ }
14613
14633
  if (record4.seats === void 0) {
14614
- return { seats: {} };
14634
+ return {
14635
+ seats: {},
14636
+ ...autoResumeLimit === void 0 ? {} : { autoResumeLimit }
14637
+ };
14615
14638
  }
14616
14639
  if (record4.seats === null || typeof record4.seats !== "object" || Array.isArray(record4.seats)) {
14617
14640
  throw new Error("public CLI config.seats must be an object");
@@ -14625,7 +14648,10 @@ function parsePublicCliConfig(value) {
14625
14648
  }
14626
14649
  seats[key] = parseSeatModelConfig(raw, key);
14627
14650
  }
14628
- return { seats };
14651
+ return {
14652
+ seats,
14653
+ ...autoResumeLimit === void 0 ? {} : { autoResumeLimit }
14654
+ };
14629
14655
  }
14630
14656
  function parseSeatModelConfig(value, seat) {
14631
14657
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -16470,11 +16496,13 @@ var init_option_definitions = __esm({
16470
16496
  usage: [
16471
16497
  "ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
16472
16498
  "ak-role config set-engine <seat> <name>",
16473
- "ak-role config unset-engine <seat>"
16499
+ "ak-role config unset-engine <seat>",
16500
+ "ak-role config set-auto-resume-limit <N>"
16474
16501
  ],
16475
16502
  examples: [
16476
16503
  "ak-role config set judge openai-codex/gpt-5.6-sol:high",
16477
- "ak-role config set-engine judge opus"
16504
+ "ak-role config set-engine judge opus",
16505
+ "ak-role config set-auto-resume-limit 3"
16478
16506
  ]
16479
16507
  },
16480
16508
  help: {
@@ -22607,6 +22635,8 @@ function presentTerminal(terminal, io) {
22607
22635
  }
22608
22636
  }
22609
22637
  async function runWithAutoResumeLoop(options) {
22638
+ const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
22639
+ parseAutoResumeLimit(limit);
22610
22640
  let autoResumeAttempts = 0;
22611
22641
  let isFirst = true;
22612
22642
  let currentExtraArgs = options.buildInitialArgs();
@@ -22636,7 +22666,7 @@ async function runWithAutoResumeLoop(options) {
22636
22666
  }
22637
22667
  return result2;
22638
22668
  }
22639
- if (autoResumeAttempts >= AUTO_RESUME_LIMIT) {
22669
+ if (autoResumeAttempts >= limit) {
22640
22670
  if (terminal !== void 0) presentTerminal(terminal, options.io);
22641
22671
  return result2;
22642
22672
  }
@@ -22654,6 +22684,7 @@ var init_auto_resume = __esm({
22654
22684
  "src/public-cli/auto-resume.ts"() {
22655
22685
  "use strict";
22656
22686
  init_run_lifecycle();
22687
+ init_config2();
22657
22688
  init_terminal();
22658
22689
  init_settlement();
22659
22690
  dummyIo = { stdout: () => {
@@ -22930,6 +22961,8 @@ async function runPublicCoder(argv, env, io, parseCoderArgv2) {
22930
22961
  return runWithAutoResumeLoop({
22931
22962
  admitted,
22932
22963
  io,
22964
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
22965
+ autoResumeLimit: env.autoResumeLimit,
22933
22966
  buildInitialArgs: () => buildCoderActivationExtraArgs(admitted, {
22934
22967
  packageRoot: env.packageRoot,
22935
22968
  ...env.model === void 0 ? {} : { model: env.model },
@@ -23814,6 +23847,8 @@ async function runPublicFixer(argv, env, io, parseFixerArgv2) {
23814
23847
  return runWithAutoResumeLoop({
23815
23848
  admitted,
23816
23849
  io,
23850
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
23851
+ autoResumeLimit: env.autoResumeLimit,
23817
23852
  buildInitialArgs: () => buildFixerActivationExtraArgs(admitted, {
23818
23853
  packageRoot: env.packageRoot,
23819
23854
  ...env.model === void 0 ? {} : { model: env.model },
@@ -24178,6 +24213,8 @@ async function runPublicJudge(argv, env, io, parseJudgeArgv2) {
24178
24213
  return runWithAutoResumeLoop({
24179
24214
  admitted,
24180
24215
  io,
24216
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
24217
+ autoResumeLimit: env.autoResumeLimit,
24181
24218
  buildInitialArgs: () => buildJudgeActivationExtraArgs(admitted, {
24182
24219
  packageRoot: env.packageRoot,
24183
24220
  ...env.model === void 0 ? {} : { model: env.model },
@@ -24625,6 +24662,8 @@ async function runPublicMerger(argv, env, io, parseMergerArgv2) {
24625
24662
  return runWithAutoResumeLoop({
24626
24663
  admitted,
24627
24664
  io,
24665
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
24666
+ autoResumeLimit: env.autoResumeLimit,
24628
24667
  buildInitialArgs: () => buildMergerActivationExtraArgs(admitted, {
24629
24668
  packageRoot: env.packageRoot,
24630
24669
  ...env.model === void 0 ? {} : { model: env.model },
@@ -25041,6 +25080,8 @@ async function runPublicReviewer(argv, env, io, parseReviewerArgv2) {
25041
25080
  return runWithAutoResumeLoop({
25042
25081
  admitted,
25043
25082
  io,
25083
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
25084
+ autoResumeLimit: env.autoResumeLimit,
25044
25085
  buildInitialArgs: () => buildReviewerActivationExtraArgs(admitted, {
25045
25086
  packageRoot: env.packageRoot,
25046
25087
  ...env.model === void 0 ? {} : { model: env.model },
@@ -27594,6 +27635,7 @@ function renderConfig(config) {
27594
27635
  lines.push(`${seat} ${formatModelSpec(selection)} ${engine}`);
27595
27636
  }
27596
27637
  }
27638
+ lines.push(`autoResumeLimit ${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
27597
27639
  return `${lines.join("\n")}
27598
27640
  `;
27599
27641
  }
@@ -27687,6 +27729,30 @@ async function runConfigCommand(args, home, packageRoot2, io) {
27687
27729
  io.stdout(renderConfig(config));
27688
27730
  return 0;
27689
27731
  }
27732
+ if (args[0] === "set-auto-resume-limit") {
27733
+ if (args.length !== 2) {
27734
+ throw new CliUsageError(
27735
+ "usage: ak-role config set-auto-resume-limit <N>"
27736
+ );
27737
+ }
27738
+ const raw = args[1];
27739
+ if (!/^[0-9]+$/.test(raw)) {
27740
+ throw new CliUsageError(
27741
+ `auto-resume limit must be a non-negative integer, got ${raw}`
27742
+ );
27743
+ }
27744
+ const converted = Number(raw);
27745
+ if (!Number.isFinite(converted) || BigInt(converted) !== BigInt(raw)) {
27746
+ throw new CliUsageError(
27747
+ `auto-resume limit ${raw} is not exactly representable as a number; refusing to silently round the value`
27748
+ );
27749
+ }
27750
+ let config = await loadAndValidateConfig(home, packageRoot2);
27751
+ config = setAutoResumeLimit(config, converted);
27752
+ await savePublicCliConfig(config, home);
27753
+ io.stdout(renderConfig(config));
27754
+ return 0;
27755
+ }
27690
27756
  throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
27691
27757
  }
27692
27758
  async function runAkRole(argv, env) {
@@ -27896,7 +27962,9 @@ async function runAkRole(argv, env) {
27896
27962
  ...projectSeatEngine(seat),
27897
27963
  ...env.judgeExtraPiArgs === void 0 ? {} : { extraPiArgs: env.judgeExtraPiArgs },
27898
27964
  ...env.judgeTimeoutMs === void 0 ? {} : { timeoutMs: env.judgeTimeoutMs },
27899
- ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
27965
+ ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
27966
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
27967
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
27900
27968
  },
27901
27969
  io,
27902
27970
  PUBLIC_ROLE_ARGV.judge.parse
@@ -27931,7 +27999,9 @@ async function runAkRole(argv, env) {
27931
27999
  ...projectSeatEngine(seat),
27932
28000
  ...env.coderExtraPiArgs === void 0 ? {} : { extraPiArgs: env.coderExtraPiArgs },
27933
28001
  ...env.coderTimeoutMs === void 0 ? {} : { timeoutMs: env.coderTimeoutMs },
27934
- ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
28002
+ ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
28003
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
28004
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
27935
28005
  },
27936
28006
  io,
27937
28007
  PUBLIC_ROLE_ARGV.coder.parse
@@ -27966,7 +28036,9 @@ async function runAkRole(argv, env) {
27966
28036
  ...projectSeatEngine(seat),
27967
28037
  ...env.fixerExtraPiArgs === void 0 ? {} : { extraPiArgs: env.fixerExtraPiArgs },
27968
28038
  ...env.fixerTimeoutMs === void 0 ? {} : { timeoutMs: env.fixerTimeoutMs },
27969
- ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
28039
+ ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
28040
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
28041
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
27970
28042
  },
27971
28043
  io,
27972
28044
  PUBLIC_ROLE_ARGV.fixer.parse
@@ -28036,7 +28108,9 @@ async function runAkRole(argv, env) {
28036
28108
  ...projectSeatEngine(seat),
28037
28109
  ...env.reviewerExtraPiArgs === void 0 ? {} : { extraPiArgs: env.reviewerExtraPiArgs },
28038
28110
  ...env.reviewerTimeoutMs === void 0 ? {} : { timeoutMs: env.reviewerTimeoutMs },
28039
- ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
28111
+ ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
28112
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
28113
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
28040
28114
  },
28041
28115
  io,
28042
28116
  PUBLIC_ROLE_ARGV.reviewer.parse
@@ -28106,7 +28180,9 @@ async function runAkRole(argv, env) {
28106
28180
  ...projectSeatEngine(seat),
28107
28181
  ...env.mergerExtraPiArgs === void 0 ? {} : { extraPiArgs: env.mergerExtraPiArgs },
28108
28182
  ...env.mergerTimeoutMs === void 0 ? {} : { timeoutMs: env.mergerTimeoutMs },
28109
- ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
28183
+ ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
28184
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
28185
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
28110
28186
  },
28111
28187
  io,
28112
28188
  PUBLIC_ROLE_ARGV.merger.parse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2169",
3
+ "version": "0.1.2173",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Single generic auto-resume loop for #416 (owner scope = single LLM call).
3
- * Call-local retries, at most AUTO_RESUME_LIMIT times, in-place (same runId/session).
3
+ * Call-local retries, at most the effective autoResumeLimit times (injected once
4
+ * per call by the caller, #422 — never re-read from disk inside the loop),
5
+ * in-place (same runId/session).
4
6
  * Unifies presentation: intermediate attempts use dummyIo, only final Terminal is presented.
5
7
  */
6
8
  import { AUTO_RESUME_LIMIT, isSessionPrincipalAvailable, acquireRunWriterLease, RunWriterLeaseHeldError, type RunWriterLease } from "./run-lifecycle.ts";
9
+ import { parseAutoResumeLimit } from "./config.ts";
7
10
  import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalResult } from "./terminal.ts";
8
11
  import { presentFailureTerminal, presentStructuralRejection } from "./settlement.ts";
9
12
  import type { CliIo } from "./cli-io.ts";
@@ -26,10 +29,21 @@ export type AutoResumeDispatchResult = {
26
29
  export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(options: {
27
30
  admitted: { sessionFile: string; runDirectory: string };
28
31
  io: CliIo;
32
+ /**
33
+ * Effective ceiling (#422), resolved by the caller before the loop; never re-read
34
+ * per round. undefined = package default (AUTO_RESUME_LIMIT). Domain-validated at
35
+ * this single entry point (#422): NaN/negative/fractional/Infinity reject loudly
36
+ * before the first dispatch instead of silently bypassing the ceiling comparison.
37
+ */
38
+ autoResumeLimit?: number | undefined;
29
39
  buildInitialArgs: () => string[];
30
40
  buildResumeArgs: () => string[];
31
41
  dispatch: (extraArgs: string[], lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
32
42
  }): Promise<T> {
43
+ // #422 single-point resolution + domain validation. NaN would bypass every
44
+ // `attempts >= limit` comparison (always false) — reject here, before any dispatch.
45
+ const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
46
+ parseAutoResumeLimit(limit);
33
47
  let autoResumeAttempts = 0;
34
48
  let isFirst = true;
35
49
  let currentExtraArgs = options.buildInitialArgs();
@@ -64,7 +78,7 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
64
78
  return result;
65
79
  }
66
80
 
67
- if (autoResumeAttempts >= AUTO_RESUME_LIMIT) {
81
+ if (autoResumeAttempts >= limit) {
68
82
  if (terminal !== undefined) presentTerminal(terminal, options.io);
69
83
  return result;
70
84
  }
@@ -17,6 +17,7 @@ import {
17
17
  parseModelSpec,
18
18
  resolveEffectiveSeat,
19
19
  savePublicCliConfig,
20
+ setAutoResumeLimit,
20
21
  setPersistentSeatConfig,
21
22
  setPersistentSeatEngine,
22
23
  validatePublicCliConfigEngines,
@@ -58,7 +59,7 @@ import { runPublicJudge, runPublicResume } from "./judge-run.ts";
58
59
  import { runPublicMerger, runPublicMergerResume } from "./merger-run.ts";
59
60
  import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
60
61
  import { runPublicTaishi } from "./taishi-run.ts";
61
- import { peekRoleRunRole } from "./run-lifecycle.ts";
62
+ import { AUTO_RESUME_LIMIT, peekRoleRunRole } from "./run-lifecycle.ts";
62
63
  import {
63
64
  AUTOMATIC_NAVIGATOR_SEAT,
64
65
  INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
@@ -527,6 +528,8 @@ function renderConfig(config: PublicCliConfig): string {
527
528
  lines.push(`${seat}\t${formatModelSpec(selection)}\t${engine}`);
528
529
  }
529
530
  }
531
+ // #422: show the effective auto-resume ceiling (configured value or default).
532
+ lines.push(`autoResumeLimit\t${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
530
533
  return `${lines.join("\n")}\n`;
531
534
  }
532
535
 
@@ -631,6 +634,39 @@ async function runConfigCommand(
631
634
  return 0;
632
635
  }
633
636
 
637
+ // #422: standalone verb per the set-engine/unset-engine precedent — the
638
+ // existing `config set` grammar stays strictly even-position seat/spec pairs.
639
+ if (args[0] === "set-auto-resume-limit") {
640
+ if (args.length !== 2) {
641
+ throw new CliUsageError(
642
+ "usage: ak-role config set-auto-resume-limit <N>",
643
+ );
644
+ }
645
+ const raw = args[1]!;
646
+ if (!/^[0-9]+$/.test(raw)) {
647
+ throw new CliUsageError(
648
+ `auto-resume limit must be a non-negative integer, got ${raw}`,
649
+ );
650
+ }
651
+ // #422 fidelity boundary (not an upper bound — ADR 0035 stays intact): the
652
+ // persisted representation is a JS number, and Number() silently rounds
653
+ // integers beyond 2^53-1 (e.g. 9007199254740993 → 9007199254740992). Refuse
654
+ // loudly instead of persisting a different N; every exactly-representable
655
+ // non-negative integer remains legal. The regex above guarantees pure
656
+ // digits, so BigInt(raw) has no leading-zero ambiguity.
657
+ const converted = Number(raw);
658
+ if (!Number.isFinite(converted) || BigInt(converted) !== BigInt(raw)) {
659
+ throw new CliUsageError(
660
+ `auto-resume limit ${raw} is not exactly representable as a number; refusing to silently round the value`,
661
+ );
662
+ }
663
+ let config = await loadAndValidateConfig(home, packageRoot);
664
+ config = setAutoResumeLimit(config, converted);
665
+ await savePublicCliConfig(config, home);
666
+ io.stdout(renderConfig(config));
667
+ return 0;
668
+ }
669
+
634
670
  throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
635
671
  }
636
672
 
@@ -922,6 +958,10 @@ export async function runAkRole(
922
958
  ? {}
923
959
  : { timeoutMs: env.judgeTimeoutMs }),
924
960
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
961
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
962
+ ...(config.autoResumeLimit === undefined
963
+ ? {}
964
+ : { autoResumeLimit: config.autoResumeLimit }),
925
965
  },
926
966
  io,
927
967
  PUBLIC_ROLE_ARGV.judge.parse,
@@ -966,6 +1006,10 @@ export async function runAkRole(
966
1006
  ? {}
967
1007
  : { timeoutMs: env.coderTimeoutMs }),
968
1008
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
1009
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
1010
+ ...(config.autoResumeLimit === undefined
1011
+ ? {}
1012
+ : { autoResumeLimit: config.autoResumeLimit }),
969
1013
  },
970
1014
  io,
971
1015
  PUBLIC_ROLE_ARGV.coder.parse,
@@ -1010,6 +1054,10 @@ export async function runAkRole(
1010
1054
  ? {}
1011
1055
  : { timeoutMs: env.fixerTimeoutMs }),
1012
1056
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
1057
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
1058
+ ...(config.autoResumeLimit === undefined
1059
+ ? {}
1060
+ : { autoResumeLimit: config.autoResumeLimit }),
1013
1061
  },
1014
1062
  io,
1015
1063
  PUBLIC_ROLE_ARGV.fixer.parse,
@@ -1098,6 +1146,10 @@ export async function runAkRole(
1098
1146
  ? {}
1099
1147
  : { timeoutMs: env.reviewerTimeoutMs }),
1100
1148
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
1149
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
1150
+ ...(config.autoResumeLimit === undefined
1151
+ ? {}
1152
+ : { autoResumeLimit: config.autoResumeLimit }),
1101
1153
  },
1102
1154
  io,
1103
1155
  PUBLIC_ROLE_ARGV.reviewer.parse,
@@ -1186,6 +1238,10 @@ export async function runAkRole(
1186
1238
  ? {}
1187
1239
  : { timeoutMs: env.mergerTimeoutMs }),
1188
1240
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
1241
+ // #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
1242
+ ...(config.autoResumeLimit === undefined
1243
+ ? {}
1244
+ : { autoResumeLimit: config.autoResumeLimit }),
1189
1245
  },
1190
1246
  io,
1191
1247
  PUBLIC_ROLE_ARGV.merger.parse,
@@ -86,6 +86,8 @@ export type CoderRunEnv = {
86
86
  engine?: string;
87
87
  credentials?: CredentialProviders;
88
88
  createRunId?: () => string;
89
+ /** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
90
+ autoResumeLimit?: number;
89
91
  extraPiArgs?: readonly string[];
90
92
  timeoutMs?: number;
91
93
  };
@@ -475,6 +477,8 @@ export async function runPublicCoder(
475
477
  return runWithAutoResumeLoop({
476
478
  admitted,
477
479
  io,
480
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
481
+ autoResumeLimit: env.autoResumeLimit,
478
482
  buildInitialArgs: () =>
479
483
  buildCoderActivationExtraArgs(admitted, {
480
484
  packageRoot: env.packageRoot,
@@ -33,6 +33,13 @@ export type PersistentSeatConfig = SeatModelConfig & {
33
33
 
34
34
  export type PublicCliConfig = {
35
35
  seats: Partial<Record<PublicConfigurableSeat, PersistentSeatConfig>>;
36
+ /**
37
+ * #422: single-call auto-resume retry ceiling, sibling of `seats`.
38
+ * Non-negative integer; 0 disables auto-resume (one dispatch per call).
39
+ * undefined = package default (AUTO_RESUME_LIMIT). No package-local upper
40
+ * bound (ADR 0035).
41
+ */
42
+ autoResumeLimit?: number;
36
43
  };
37
44
 
38
45
  export type EffectiveSource = "persistent" | "startup" | "invocation" | "unconfigured";
@@ -108,6 +115,9 @@ export function setPersistentSeatConfig(
108
115
  ): PublicCliConfig {
109
116
  const previous = config.seats[seat];
110
117
  return {
118
+ // Spread preserves sibling top-level keys such as autoResumeLimit (#422):
119
+ // a seat write must never silently drop them.
120
+ ...config,
111
121
  seats: {
112
122
  ...config.seats,
113
123
  [seat]: {
@@ -146,6 +156,7 @@ export function setPersistentSeatEngine(
146
156
  if (engine === undefined) {
147
157
  const { engine: _dropped, ...modelOnly } = previous;
148
158
  return {
159
+ ...config,
149
160
  seats: {
150
161
  ...config.seats,
151
162
  [seat]: modelOnly,
@@ -155,6 +166,7 @@ export function setPersistentSeatEngine(
155
166
  // Engine-name path-safety syntax is owned solely by assertLegalEngineName
156
167
  // (call-request + config-parse seams). Setter is pure seat mutation.
157
168
  return {
169
+ ...config,
158
170
  seats: {
159
171
  ...config.seats,
160
172
  [seat]: { ...previous, engine },
@@ -162,6 +174,32 @@ export function setPersistentSeatEngine(
162
174
  };
163
175
  }
164
176
 
177
+ /**
178
+ * #422 value domain authority for the auto-resume ceiling: non-negative integer,
179
+ * no package-local upper bound (ADR 0035). `0` is legal and means auto-resume is
180
+ * disabled (a single dispatch, no in-place retry). Negative numbers, fractions,
181
+ * NaN, Infinity and non-number types are rejected loudly — never silently coerced.
182
+ */
183
+ export function parseAutoResumeLimit(value: unknown): number {
184
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
185
+ throw new Error(
186
+ `auto-resume limit must be a non-negative integer, got ${JSON.stringify(value)}`,
187
+ );
188
+ }
189
+ return value;
190
+ }
191
+
192
+ /**
193
+ * #422: set the persistent autoResumeLimit top-level key. Pure mutation that
194
+ * preserves all sibling keys (seats included).
195
+ */
196
+ export function setAutoResumeLimit(
197
+ config: PublicCliConfig,
198
+ limit: number,
199
+ ): PublicCliConfig {
200
+ return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
201
+ }
202
+
165
203
  /** Strip optional engine so activation model argv never sees the engine axis. */
166
204
  export function seatModelOnly(seat: PersistentSeatConfig): SeatModelConfig {
167
205
  return seat.thinking === undefined
@@ -261,9 +299,18 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
261
299
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
262
300
  throw new Error("public CLI config must be an object");
263
301
  }
264
- const record = value as { seats?: unknown };
302
+ const record = value as { seats?: unknown; autoResumeLimit?: unknown };
303
+ // #422 round-trip preservation: the sibling top-level key must survive every
304
+ // parse→save cycle; an unknown-key drop would silently erase it on any write.
305
+ let autoResumeLimit: number | undefined;
306
+ if (record.autoResumeLimit !== undefined) {
307
+ autoResumeLimit = parseAutoResumeLimit(record.autoResumeLimit);
308
+ }
265
309
  if (record.seats === undefined) {
266
- return { seats: {} };
310
+ return {
311
+ seats: {},
312
+ ...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
313
+ };
267
314
  }
268
315
  if (
269
316
  record.seats === null ||
@@ -281,7 +328,10 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
281
328
  }
282
329
  seats[key as PublicConfigurableSeat] = parseSeatModelConfig(raw, key);
283
330
  }
284
- return { seats };
331
+ return {
332
+ seats,
333
+ ...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
334
+ };
285
335
  }
286
336
 
287
337
  function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfig {
@@ -89,6 +89,8 @@ export type FixerRunEnv = {
89
89
  engine?: string;
90
90
  credentials?: CredentialProviders;
91
91
  createRunId?: () => string;
92
+ /** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
93
+ autoResumeLimit?: number;
92
94
  extraPiArgs?: readonly string[];
93
95
  timeoutMs?: number;
94
96
  };
@@ -494,6 +496,8 @@ export async function runPublicFixer(
494
496
  return runWithAutoResumeLoop({
495
497
  admitted,
496
498
  io,
499
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
500
+ autoResumeLimit: env.autoResumeLimit,
497
501
  buildInitialArgs: () =>
498
502
  buildFixerActivationExtraArgs(admitted, {
499
503
  packageRoot: env.packageRoot,
@@ -89,6 +89,8 @@ export type JudgeRunEnv = {
89
89
  */
90
90
  credentials?: CredentialProviders;
91
91
  createRunId?: () => string;
92
+ /** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
93
+ autoResumeLimit?: number;
92
94
  /** Extra Pi args inserted before the prompt (tests: faux provider extension). */
93
95
  extraPiArgs?: readonly string[];
94
96
  /** Override default role-run timeout. */
@@ -463,6 +465,8 @@ export async function runPublicJudge(
463
465
  return runWithAutoResumeLoop({
464
466
  admitted,
465
467
  io,
468
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
469
+ autoResumeLimit: env.autoResumeLimit,
466
470
  buildInitialArgs: () =>
467
471
  buildJudgeActivationExtraArgs(admitted, {
468
472
  packageRoot: env.packageRoot,
@@ -90,6 +90,8 @@ export type MergerRunEnv = {
90
90
  engine?: string;
91
91
  credentials?: CredentialProviders;
92
92
  createRunId?: () => string;
93
+ /** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
94
+ autoResumeLimit?: number;
93
95
  extraPiArgs?: readonly string[];
94
96
  timeoutMs?: number;
95
97
  };
@@ -575,6 +577,8 @@ export async function runPublicMerger(
575
577
  return runWithAutoResumeLoop({
576
578
  admitted,
577
579
  io,
580
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
581
+ autoResumeLimit: env.autoResumeLimit,
578
582
  buildInitialArgs: () =>
579
583
  buildMergerActivationExtraArgs(admitted, {
580
584
  packageRoot: env.packageRoot,
@@ -1098,10 +1098,12 @@ const SUPPORT_COMMAND_HELP = {
1098
1098
  "ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
1099
1099
  "ak-role config set-engine <seat> <name>",
1100
1100
  "ak-role config unset-engine <seat>",
1101
+ "ak-role config set-auto-resume-limit <N>",
1101
1102
  ],
1102
1103
  examples: [
1103
1104
  "ak-role config set judge openai-codex/gpt-5.6-sol:high",
1104
1105
  "ak-role config set-engine judge opus",
1106
+ "ak-role config set-auto-resume-limit 3",
1105
1107
  ],
1106
1108
  },
1107
1109
  help: {
@@ -91,6 +91,8 @@ export type ReviewerRunEnv = {
91
91
  engine?: string;
92
92
  credentials?: CredentialProviders;
93
93
  createRunId?: () => string;
94
+ /** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
95
+ autoResumeLimit?: number;
94
96
  extraPiArgs?: readonly string[];
95
97
  timeoutMs?: number;
96
98
  };
@@ -514,6 +516,8 @@ export async function runPublicReviewer(
514
516
  return runWithAutoResumeLoop({
515
517
  admitted,
516
518
  io,
519
+ // #422: pass-through only; the loop entry resolves the default and validates the domain once.
520
+ autoResumeLimit: env.autoResumeLimit,
517
521
  buildInitialArgs: () =>
518
522
  buildReviewerActivationExtraArgs(admitted, {
519
523
  packageRoot: env.packageRoot,
@@ -52,7 +52,10 @@ export type TypedHttp429Observation = {
52
52
  readonly provider: V1ResumableProvider;
53
53
  };
54
54
 
55
- /** Per single LLM call auto-resume retries (call-local, no persistence). See #416 scope correction 2026-08-22. */
55
+ /** Default for the #422 configurable single-call auto-resume ceiling
56
+ * (public-cli.json top-level `autoResumeLimit`). No longer the runtime truth
57
+ * source: runWithAutoResumeLoop receives the effective value once per call.
58
+ */
56
59
  export const AUTO_RESUME_LIMIT = 2 as const;
57
60
 
58
61
  export type RoleRunRecord = {