@event4u/agent-config 14.2.0 → 14.5.0

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +33 -252
  3. package/agents/templates/.ai-council.yml.example +35 -0
  4. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
  5. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
  6. package/dist/agent-src/scripts/resume_probe.ts +154 -9
  7. package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
  8. package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
  9. package/dist/agent-src/templates/agent-settings.md +70 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  12. package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  13. package/dist/cli/python/workspace_drive.js +8 -3
  14. package/dist/cli/python/workspace_drive.js.map +1 -1
  15. package/dist/cli/python/workspace_hosts.js +16 -12
  16. package/dist/cli/python/workspace_hosts.js.map +1 -1
  17. package/dist/cli/registry.js +2 -0
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
  20. package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
  21. package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
  22. package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +26 -11
  24. package/dist/cli-delegate/cmd_doctor.js +2 -2
  25. package/dist/cli-delegate/cmd_handoff.js +3 -3
  26. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  27. package/dist/cli-delegate/cmd_settings_set.js +17 -3
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +4 -4
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/dispatch.js +3754 -1721
  38. package/dist/mcp/registry-manifest.json +2 -2
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/mcp/server.mjs +1070 -408
  41. package/dist/scripts/_lib/host_launch.js +66 -0
  42. package/dist/scripts/_lib/host_launch.js.map +1 -0
  43. package/dist/server/io/yamlIO.js +175 -3
  44. package/dist/server/io/yamlIO.js.map +1 -1
  45. package/dist/server/routes/wizard.js +20 -1
  46. package/dist/server/routes/wizard.js.map +1 -1
  47. package/dist/shared/settingsConsent.js +25 -0
  48. package/dist/shared/settingsConsent.js.map +1 -1
  49. package/docs/contracts/settings-classes.md +11 -0
  50. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
  51. package/package.json +1 -1
  52. package/src/agent-src/scripts/resume_probe.ts +154 -9
  53. package/src/agent-src/scripts/roadmap_gates.ts +210 -6
  54. package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  55. package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  56. package/src/config/estate-count-budget.json +127 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-coverage.yml +32 -4
  59. package/src/config/gate-violation-baselines.json +63 -63
  60. package/src/config/gitignore-block.txt +5 -0
  61. package/src/config/hook-latency-budget.json +40 -1
  62. package/src/config/hook-token-budget.json +16 -0
  63. package/src/config/rule-stub-ceilings.json +11 -3
  64. package/src/scripts/_dispatch.bash +42 -1
  65. package/src/scripts/_lib/council_fallback_posture.ts +81 -0
  66. package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
  67. package/src/scripts/_lib/council_settings_block.ts +152 -0
  68. package/src/scripts/_lib/headless_invocation.ts +285 -0
  69. package/src/scripts/_lib/host_launch.ts +72 -0
  70. package/src/scripts/_lib/map_to_object.ts +16 -0
  71. package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
  72. package/src/scripts/_lib/run_checkpoint.ts +409 -0
  73. package/src/scripts/_lib/session_register.ts +32 -1
  74. package/src/scripts/_lib/stdin.ts +35 -1
  75. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  76. package/src/scripts/_lib/unattended_guard.ts +371 -0
  77. package/src/scripts/ai_council/config.ts +27 -1
  78. package/src/scripts/ai_council/events_log.ts +12 -1
  79. package/src/scripts/ai_council/fallback_config.ts +114 -0
  80. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  81. package/src/scripts/ai_council/orchestrator.ts +251 -292
  82. package/src/scripts/ai_council/py_parity.ts +139 -0
  83. package/src/scripts/ai_council/response_render.ts +99 -0
  84. package/src/scripts/ai_council/spend_gate.ts +123 -0
  85. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  86. package/src/scripts/bench_hook_injection.ts +118 -7
  87. package/src/scripts/bench_hook_latency.ts +442 -16
  88. package/src/scripts/check_estate_count.ts +924 -0
  89. package/src/scripts/compile_hook_manifest.ts +67 -0
  90. package/src/scripts/council_cli.ts +100 -125
  91. package/src/scripts/decision_memo.ts +325 -0
  92. package/src/scripts/hook_manifest.json +1 -0
  93. package/src/scripts/hook_manifest.yaml +172 -25
  94. package/src/scripts/hooks/concern_registry.ts +6 -0
  95. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  96. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  97. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  98. package/src/scripts/hooks/hook_stdin.ts +111 -5
  99. package/src/scripts/hooks/host_semantics.ts +28 -0
  100. package/src/scripts/hooks/injection_budget.ts +731 -0
  101. package/src/scripts/hooks/payload_stub.ts +446 -0
  102. package/src/scripts/hooks/run_continuation_hook.ts +683 -0
  103. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  104. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  105. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  106. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  107. package/src/scripts/hooks_doctor.ts +16 -2
  108. package/src/scripts/interruption_report.ts +330 -13
  109. package/src/scripts/lint_hook_manifest.ts +167 -0
  110. package/src/scripts/roadmap_progress_hook.ts +254 -46
  111. package/src/scripts/run_supervise.ts +623 -0
  112. package/src/scripts/session_register_hook.ts +146 -12
  113. package/src/scripts/sessions_cli.ts +18 -6
  114. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  115. package/src/server/io/yamlIO.ts +167 -3
  116. package/src/shared/settingsConsent.ts +49 -2
  117. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -12987,22 +12987,78 @@ var init_memory_lookup = __esm({
12987
12987
  }
12988
12988
  });
12989
12989
 
12990
- // src/scripts/hooks/hook_stdin.ts
12990
+ // src/scripts/_lib/stdin.ts
12991
12991
  import * as fs13 from "node:fs";
12992
+ function _sleepSync(ms) {
12993
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12994
+ }
12995
+ function readStdinText(fd = 0, opts = {}) {
12996
+ const chunks = [];
12997
+ const buf = Buffer.alloc(CHUNK);
12998
+ let retries = 0;
12999
+ let got_any = false;
13000
+ const first_byte_deadline = opts.firstByteTimeoutMs === void 0 ? null : Date.now() + opts.firstByteTimeoutMs;
13001
+ for (; ; ) {
13002
+ let read;
13003
+ try {
13004
+ read = fs13.readSync(fd, buf, 0, CHUNK, null);
13005
+ retries = 0;
13006
+ } catch (exc) {
13007
+ const code = exc.code;
13008
+ if (code === "EAGAIN") {
13009
+ if (!got_any && first_byte_deadline !== null && Date.now() >= first_byte_deadline) {
13010
+ return "";
13011
+ }
13012
+ retries += 1;
13013
+ if (retries > MAX_RETRIES) {
13014
+ throw new Error(
13015
+ `readStdinText: stdin stayed unreadable across ${String(MAX_RETRIES)} retries (~${String(MAX_RETRIES * RETRY_SLEEP_MS / 1e3)}s). Refusing to report an empty read as empty input.`
13016
+ );
13017
+ }
13018
+ _sleepSync(RETRY_SLEEP_MS);
13019
+ continue;
13020
+ }
13021
+ if (code === "EOF") break;
13022
+ throw exc;
13023
+ }
13024
+ if (read === 0) break;
13025
+ got_any = true;
13026
+ chunks.push(Buffer.from(buf.subarray(0, read)));
13027
+ }
13028
+ return Buffer.concat(chunks).toString("utf-8");
13029
+ }
13030
+ var CHUNK, RETRY_SLEEP_MS, MAX_RETRIES;
13031
+ var init_stdin = __esm({
13032
+ "src/scripts/_lib/stdin.ts"() {
13033
+ "use strict";
13034
+ CHUNK = 64 * 1024;
13035
+ RETRY_SLEEP_MS = 5;
13036
+ MAX_RETRIES = 2e3;
13037
+ }
13038
+ });
13039
+
13040
+ // src/scripts/hooks/hook_stdin.ts
13041
+ import * as tty from "node:tty";
13042
+ function readFd0ToEnd() {
13043
+ void process.stdin;
13044
+ return readStdinText(0, { firstByteTimeoutMs: HOOK_FIRST_BYTE_TIMEOUT_MS });
13045
+ }
12992
13046
  function readHookStdin() {
12993
13047
  if (_override !== null) return _override;
12994
13048
  try {
12995
- if (process.stdin.isTTY) return "";
12996
- return fs13.readFileSync(0, "utf-8");
13049
+ if (tty.isatty(0)) return "";
13050
+ return readFd0ToEnd();
12997
13051
  } catch {
12998
13052
  return "";
12999
13053
  }
13000
13054
  }
13001
- var _override;
13055
+ var _override, HOOK_FIRST_BYTE_TIMEOUT_MS;
13002
13056
  var init_hook_stdin = __esm({
13003
13057
  "src/scripts/hooks/hook_stdin.ts"() {
13004
13058
  "use strict";
13059
+ init_stdin();
13005
13060
  _override = null;
13061
+ HOOK_FIRST_BYTE_TIMEOUT_MS = 25;
13006
13062
  }
13007
13063
  });
13008
13064
 
@@ -20475,7 +20531,7 @@ import {
20475
20531
  fsyncSync,
20476
20532
  mkdirSync as mkdirSync8,
20477
20533
  openSync,
20478
- readFileSync as readFileSync22,
20534
+ readFileSync as readFileSync21,
20479
20535
  renameSync as renameSync4,
20480
20536
  unlinkSync as unlinkSync2,
20481
20537
  writeSync
@@ -20512,7 +20568,7 @@ function atomicWriteFile(target, data, options = {}) {
20512
20568
  function atomicAppendLine(target, line) {
20513
20569
  let existing = "";
20514
20570
  try {
20515
- existing = readFileSync22(target, "utf8");
20571
+ existing = readFileSync21(target, "utf8");
20516
20572
  } catch {
20517
20573
  existing = "";
20518
20574
  }
@@ -26081,7 +26137,13 @@ var init_events_log = __esm({
26081
26137
  "proceed",
26082
26138
  "skip_necessity",
26083
26139
  "block_quota",
26084
- "quorum_result"
26140
+ "quorum_result",
26141
+ // A seat lost its cli transport mid-pass and was retried on the metered
26142
+ // api rung. Without this action, attendance analysis cannot tell a seat
26143
+ // SAVED by the fallback from a seat that was natively api all along — the
26144
+ // two are indistinguishable in the response set, and only one of them
26145
+ // costs money that the operator did not plan.
26146
+ "transport_fallback"
26085
26147
  ]);
26086
26148
  _KILL_SWITCH_ENV = "AGENT_CONFIG_NO_EVENTS_LOG";
26087
26149
  _HERE15 = fileURLToPath28(import.meta.url);
@@ -28101,6 +28163,46 @@ Type your follow-up question, end with a line containing only: ${this._end_marke
28101
28163
  }
28102
28164
  });
28103
28165
 
28166
+ // src/scripts/ai_council/fallback_config.ts
28167
+ function buildFallback(raw, d) {
28168
+ if (!d.isDict(raw)) return { api_on_quota: false };
28169
+ const v = raw["api_on_quota"];
28170
+ if (v === void 0) return { api_on_quota: false };
28171
+ if (!d.isBool(v)) {
28172
+ throw d.error(
28173
+ `\`fallback.api_on_quota\`=${d.repr(v)} must be a boolean (got ${d.typeName(v)}).`
28174
+ );
28175
+ }
28176
+ return { api_on_quota: v };
28177
+ }
28178
+ function buildSecondModel(entry, cls, locked, d) {
28179
+ if (!("second_model" in entry)) return null;
28180
+ if (locked.has(cls)) {
28181
+ throw d.error(
28182
+ `decision_resolution.classes.${cls}.second_model=${d.repr(entry["second_model"])}: a second-model rung is not configurable for high-impact / user-required decisions \u2014 those classes are LOCKED to \`user\` (Iron Law) and never resolve on a model of any kind.`
28183
+ );
28184
+ }
28185
+ const raw = entry["second_model"];
28186
+ if (raw === null) return null;
28187
+ if (!(d.isStr(raw) && SECOND_MODEL_PROVIDERS.has(raw))) {
28188
+ throw d.error(
28189
+ `decision_resolution.classes.${cls}.second_model=${d.repr(raw)} not in ${d.sortedListRepr(SECOND_MODEL_PROVIDERS)} \u2014 the rung must be a provider whose local CLI runs under subscription auth, so a resolution can never become metered USD spend.`
28190
+ );
28191
+ }
28192
+ return raw;
28193
+ }
28194
+ var SECOND_MODEL_PROVIDERS;
28195
+ var init_fallback_config = __esm({
28196
+ "src/scripts/ai_council/fallback_config.ts"() {
28197
+ "use strict";
28198
+ SECOND_MODEL_PROVIDERS = /* @__PURE__ */ new Set([
28199
+ "anthropic",
28200
+ "openai",
28201
+ "gemini"
28202
+ ]);
28203
+ }
28204
+ });
28205
+
28104
28206
  // src/scripts/ai_council/config.ts
28105
28207
  var config_exports = {};
28106
28208
  __export(config_exports, {
@@ -28403,6 +28505,7 @@ function _build_config(raw, source_path) {
28403
28505
  const quorum_min_present = _build_quorum_min_present(
28404
28506
  _get(raw, "quorum_min_present", SOLO_FLOOR_MIN_PRESENT)
28405
28507
  );
28508
+ const fallback = buildFallback(_get(raw, "fallback", {}), _FALLBACK_DEPS);
28406
28509
  const necessity_classifier = _build_necessity_classifier(
28407
28510
  _asDict(_getOr(raw, "necessity_classifier", {}))
28408
28511
  );
@@ -28452,6 +28555,7 @@ function _build_config(raw, source_path) {
28452
28555
  cli_call_budget,
28453
28556
  quorum,
28454
28557
  quorum_min_present,
28558
+ fallback,
28455
28559
  necessity_classifier,
28456
28560
  model_downgrade,
28457
28561
  debate,
@@ -28718,7 +28822,13 @@ function _build_decision_resolution(d) {
28718
28822
  `decision_resolution.classes.${cls}.confidence_threshold must be in [0.0, 1.0] (got ${_pyRepr6(_f(threshold))}).`
28719
28823
  );
28720
28824
  }
28721
- classes.set(cls, { mode, confidence_threshold: threshold });
28825
+ const second_model = buildSecondModel(
28826
+ entry_raw,
28827
+ cls,
28828
+ _LOCKED_IMPACT_CLASSES,
28829
+ _FALLBACK_DEPS
28830
+ );
28831
+ classes.set(cls, { mode, confidence_threshold: threshold, second_model });
28722
28832
  }
28723
28833
  const fast_path_raw = _getOr(d, "fast_path", {});
28724
28834
  if (!_isDict(fast_path_raw)) {
@@ -29377,7 +29487,7 @@ function _pyTruthy3(value) {
29377
29487
  }
29378
29488
  return Object.keys(value).length > 0;
29379
29489
  }
29380
- var import_yaml5, _VALID_PROVIDERS, _RAW_KEY_PREFIXES, CouncilConfigError, _Float, _VALID_NECESSITY_MODES, _VALID_DISCLOSURE_MODES, _VALID_CHAIRMAN_MODES, COUNCIL_CONFIG_RELNAME, COUNCIL_CONFIG_USER_GLOBAL_REL, COUNCIL_CONFIG_ENV, _VALID_CRITIC_PROTOCOLS, _VALID_RESOLUTION_MODES, _IMPACT_CLASSES, _LOCKED_IMPACT_CLASSES, _DEFAULT_RESOLUTION_MODES, _VALID_MEMBER_MODES, _VALID_DISPATCH_MODES, DEFAULT_CLI_CALLS_PER_DAY2;
29490
+ var import_yaml5, _VALID_PROVIDERS, _FALLBACK_DEPS, _RAW_KEY_PREFIXES, CouncilConfigError, _Float, _VALID_NECESSITY_MODES, _VALID_DISCLOSURE_MODES, _VALID_CHAIRMAN_MODES, COUNCIL_CONFIG_RELNAME, COUNCIL_CONFIG_USER_GLOBAL_REL, COUNCIL_CONFIG_ENV, _VALID_CRITIC_PROTOCOLS, _VALID_RESOLUTION_MODES, _IMPACT_CLASSES, _LOCKED_IMPACT_CLASSES, _DEFAULT_RESOLUTION_MODES, _VALID_MEMBER_MODES, _VALID_DISPATCH_MODES, DEFAULT_CLI_CALLS_PER_DAY2;
29381
29491
  var init_config = __esm({
29382
29492
  "src/scripts/ai_council/config.ts"() {
29383
29493
  "use strict";
@@ -29386,6 +29496,7 @@ var init_config = __esm({
29386
29496
  init_cli_call_budget();
29387
29497
  init_clients();
29388
29498
  init_quorum();
29499
+ init_fallback_config();
29389
29500
  _VALID_PROVIDERS = /* @__PURE__ */ new Set([
29390
29501
  "anthropic",
29391
29502
  "openai",
@@ -29393,6 +29504,15 @@ var init_config = __esm({
29393
29504
  "xai",
29394
29505
  "perplexity"
29395
29506
  ]);
29507
+ _FALLBACK_DEPS = {
29508
+ isDict: _isDict,
29509
+ isBool: _isBool,
29510
+ isStr: _isStr,
29511
+ repr: _pyRepr6,
29512
+ typeName: _pyTypeName,
29513
+ sortedListRepr: _sortedListRepr,
29514
+ error: (msg) => new CouncilConfigError(msg)
29515
+ };
29396
29516
  _RAW_KEY_PREFIXES = [
29397
29517
  "sk-",
29398
29518
  "sk-ant-",
@@ -30035,7 +30155,15 @@ function classifyCliFailure(raw) {
30035
30155
  if (s === "cli_unsupported" || s === "unsupported" || s === "parse_failed") {
30036
30156
  return "cli_unsupported";
30037
30157
  }
30038
- if (s.startsWith("model_unservable") || s.includes("is not supported when using codex")) {
30158
+ if (s.startsWith("model_unservable") || // The token the PRODUCERS actually write. R2 round 6, finding 4:
30159
+ // `clients.ts` emits `model_unsupported_on_transport` at both sites
30160
+ // (the pre-spend gate at :1857 and the turn-failed path at :2002) and
30161
+ // puts the vendor sentence in `metadata.detail`, never in `error` — so
30162
+ // the class matched nothing a caller could produce and the contract's
30163
+ // fourth eligible class was dead. The token is matched here rather
30164
+ // than renamed at the producers because `code:` values reach the
30165
+ // events log and a rename would break every reader of the history.
30166
+ s.startsWith("model_unsupported_on_transport") || s.includes("is not supported when using codex")) {
30039
30167
  return "model_unservable";
30040
30168
  }
30041
30169
  if (s === "timeout") return "timeout";
@@ -30043,6 +30171,10 @@ function classifyCliFailure(raw) {
30043
30171
  if (/^exit_5\d\d$/.test(s) || s === "server_error" || s === "os") return "server_error";
30044
30172
  return "other";
30045
30173
  }
30174
+ function isFallbackEligibleUnder(failure, policy) {
30175
+ if (FALLBACK_ELIGIBLE.has(failure)) return true;
30176
+ return policy.apiOnQuota && failure === "quota_exhausted";
30177
+ }
30046
30178
  function absentReasonFromCliFailure(failure) {
30047
30179
  switch (failure) {
30048
30180
  case "binary_missing":
@@ -30057,7 +30189,7 @@ function absentReasonFromCliFailure(failure) {
30057
30189
  return null;
30058
30190
  }
30059
30191
  }
30060
- var VALID_TRANSPORT_MODES, CLI_AUTH_SOURCES, KEY_AUTH_SOURCES;
30192
+ var VALID_TRANSPORT_MODES, CLI_AUTH_SOURCES, KEY_AUTH_SOURCES, FALLBACK_ELIGIBLE, DEFAULT_FALLBACK_POLICY, MidFlightFallback;
30061
30193
  var init_transport_resolver = __esm({
30062
30194
  "src/scripts/ai_council/transport_resolver.ts"() {
30063
30195
  "use strict";
@@ -30071,6 +30203,50 @@ var init_transport_resolver = __esm({
30071
30203
  ]);
30072
30204
  CLI_AUTH_SOURCES = /* @__PURE__ */ new Set(["cli-subscription", "cli-api-key"]);
30073
30205
  KEY_AUTH_SOURCES = /* @__PURE__ */ new Set(["key-file", "env-key"]);
30206
+ FALLBACK_ELIGIBLE = /* @__PURE__ */ new Set([
30207
+ "binary_missing",
30208
+ "auth_rejected",
30209
+ "cli_unsupported",
30210
+ // `model_unservable` satisfies the same no-double-charge property in both
30211
+ // of its shapes: the pre-spend gate refuses before any subprocess exists,
30212
+ // and the call-time shape is a provider 400 — rejected at the request
30213
+ // boundary, no generation performed.
30214
+ "model_unservable"
30215
+ ]);
30216
+ DEFAULT_FALLBACK_POLICY = { apiOnQuota: false };
30217
+ MidFlightFallback = class {
30218
+ used = /* @__PURE__ */ new Set();
30219
+ /** `'api'` to retry on the api rung, `'stop'` to surface the failure. */
30220
+ attempt(provider, failure, policy = DEFAULT_FALLBACK_POLICY) {
30221
+ if (!isFallbackEligibleUnder(failure, policy)) return "stop";
30222
+ if (this.used.has(provider)) return "stop";
30223
+ this.used.add(provider);
30224
+ return "api";
30225
+ }
30226
+ /**
30227
+ * Give a provider its one fallback back, because the twin never ran.
30228
+ *
30229
+ * R2 round 2, finding 8. `attempt` claims the slot at DECISION time, which
30230
+ * is correct — the claim is what stops a dead binary being retried once per
30231
+ * round. But a claim that is never CONSUMED must not stay spent: when the
30232
+ * projected-spend gate refuses the escalation, no api call happens, and
30233
+ * leaving the provider marked means the seat can never re-decide. Every
30234
+ * later round then re-spawns the dead cli binary and gets `'stop'`, which
30235
+ * is the exact per-round-ledger behaviour the invocation-scoped ledger was
30236
+ * chosen to avoid.
30237
+ *
30238
+ * Called ONLY on a path where the twin was built and then not called.
30239
+ * A provider that actually retried keeps its claim spent — that is the cap
30240
+ * working.
30241
+ */
30242
+ release(provider) {
30243
+ this.used.delete(provider);
30244
+ }
30245
+ /** Providers that have already spent their one fallback. */
30246
+ spent() {
30247
+ return [...this.used].sort();
30248
+ }
30249
+ };
30074
30250
  }
30075
30251
  });
30076
30252
 
@@ -33615,12 +33791,12 @@ var init_cmd_doctor = __esm({
33615
33791
  });
33616
33792
 
33617
33793
  // src/install/txlog.ts
33618
- import { createReadStream, createWriteStream, existsSync as existsSync25, readFileSync as readFileSync44, renameSync as renameSync8, statSync as statSync32, unlinkSync as unlinkSync7 } from "node:fs";
33794
+ import { createReadStream, createWriteStream, existsSync as existsSync25, readFileSync as readFileSync43, renameSync as renameSync8, statSync as statSync32, unlinkSync as unlinkSync7 } from "node:fs";
33619
33795
  function readRecentEntries(logPath) {
33620
33796
  if (!existsSync25(logPath)) {
33621
33797
  return [];
33622
33798
  }
33623
- const raw = readFileSync44(logPath, "utf8");
33799
+ const raw = readFileSync43(logPath, "utf8");
33624
33800
  const lines = raw.split("\n").filter((l) => l.length > 0);
33625
33801
  const tail = lines.slice(-RECOVERY_DEPTH_CAP);
33626
33802
  const out = [];
@@ -33655,7 +33831,7 @@ var init_txlog = __esm({
33655
33831
  });
33656
33832
 
33657
33833
  // src/install/detect.ts
33658
- import { existsSync as existsSync26, readFileSync as readFileSync45, readdirSync as readdirSync28, statSync as statSync33 } from "node:fs";
33834
+ import { existsSync as existsSync26, readFileSync as readFileSync44, readdirSync as readdirSync28, statSync as statSync33 } from "node:fs";
33659
33835
  import { join as join49, resolve as resolve30 } from "node:path";
33660
33836
  function detectToolPresence(root) {
33661
33837
  return {
@@ -35233,12 +35409,22 @@ function untrackedNonDocFiles(cwd) {
35233
35409
  }
35234
35410
  function untrackedFileLineCount(cwd, relPath) {
35235
35411
  try {
35236
- const r = spawnSync9("git", ["diff", "--numstat", "--no-index", "/dev/null", relPath], {
35237
- cwd,
35238
- encoding: "utf8",
35239
- maxBuffer: 16 * 1024 * 1024
35240
- });
35241
- return parseNumstat(r.stdout ?? "").reduce((sum, row) => sum + row.added + row.deleted, 0);
35412
+ const abs = path59.resolve(cwd, relPath);
35413
+ const size = fs58.statSync(abs).size;
35414
+ if (size === 0) return 0;
35415
+ if (size > UNTRACKED_FILE_READ_CAP_BYTES) {
35416
+ return MUTATION_LINE_THRESHOLD + 1;
35417
+ }
35418
+ const buf = fs58.readFileSync(abs);
35419
+ if (buf.length === 0) return 0;
35420
+ const probe = buf.subarray(0, Math.min(buf.length, BINARY_PROBE_BYTES));
35421
+ if (probe.includes(0)) return 0;
35422
+ let lines = 0;
35423
+ for (const byte of buf) {
35424
+ if (byte === 10) lines += 1;
35425
+ }
35426
+ if (buf[buf.length - 1] !== 10) lines += 1;
35427
+ return lines;
35242
35428
  } catch {
35243
35429
  return 0;
35244
35430
  }
@@ -35421,7 +35607,7 @@ function _isCliEntry21() {
35421
35607
  return false;
35422
35608
  }
35423
35609
  }
35424
- var MUTATION_LINE_THRESHOLD, UNTRACKED_FILE_CAP, TRANSCRIPT_SCAN_MAX_BYTES, EXIT_WARN2, DOC_PATH_RE, DISPATCH_TOOL_RE, REVIEW_SHAPED_LABEL_RE, REVIEW_SHAPED_PROMPT_RE, REVIEW_SKILL_NAME_RE;
35610
+ var MUTATION_LINE_THRESHOLD, UNTRACKED_FILE_CAP, TRANSCRIPT_SCAN_MAX_BYTES, EXIT_WARN2, DOC_PATH_RE, BINARY_PROBE_BYTES, UNTRACKED_FILE_READ_CAP_BYTES, DISPATCH_TOOL_RE, REVIEW_SHAPED_LABEL_RE, REVIEW_SHAPED_PROMPT_RE, REVIEW_SKILL_NAME_RE;
35425
35611
  var init_end_review_nudge_hook = __esm({
35426
35612
  "src/scripts/hooks/end_review_nudge_hook.ts"() {
35427
35613
  "use strict";
@@ -35434,6 +35620,8 @@ var init_end_review_nudge_hook = __esm({
35434
35620
  TRANSCRIPT_SCAN_MAX_BYTES = 50 * 1024 * 1024;
35435
35621
  EXIT_WARN2 = 2;
35436
35622
  DOC_PATH_RE = /\.md$/i;
35623
+ BINARY_PROBE_BYTES = 8e3;
35624
+ UNTRACKED_FILE_READ_CAP_BYTES = 8 * 1024 * 1024;
35437
35625
  DISPATCH_TOOL_RE = /^(agent|task)$/i;
35438
35626
  REVIEW_SHAPED_LABEL_RE = /\b(review|judge|adversarial|critique|second[- ]opinion|blind\s+(pass|review)|cross-model)\b/i;
35439
35627
  REVIEW_SHAPED_PROMPT_RE = /\b(code\s+review|neutral\s+review|adversarial|judge|critique|second[- ]opinion|blind\s+(pass|review)|cross-model)\b/i;
@@ -36364,8 +36552,11 @@ function main25() {
36364
36552
  dispatchOpen ? null : detectPromissory(lastAssistant),
36365
36553
  detectLanguage(lastAssistant, readLanguagePin(workspaceRoot)),
36366
36554
  detectUnverifiedEdit(toolCalls),
36367
- // Round 7 § Phase 1 — detector D, in the same unconditional list as the
36368
- // other three. It shipped with its own settings flag one commit earlier
36555
+ // Round 7 § Phase 1 — detector D. It is NOT unconditional, and this
36556
+ // comment said it was while sitting one line above the `dispatchOpen`
36557
+ // ternary that conditions it: A and D are both excused by an open
36558
+ // dispatch, per the note at the top of this loop. Corrected 2026-08-18.
36559
+ // It shipped with its own settings flag one commit earlier
36369
36560
  // and that flag is gone: `hooks.turn_end_gate.*` was deleted on
36370
36561
  // 2026-08-12 because a default-off safety gate is an absent one. Its
36371
36562
  // gating is where the comment above says gating belongs — inside the
@@ -39503,12 +39694,21 @@ var init_cmd_conformance = __esm({
39503
39694
  var settings_exports = {};
39504
39695
  __export(settings_exports, {
39505
39696
  ALLOWED_GRANULARITIES: () => ALLOWED_GRANULARITIES,
39697
+ ALLOWED_REMOTE_FLUSH: () => ALLOWED_REMOTE_FLUSH,
39506
39698
  DEFAULT_GRANULARITY: () => DEFAULT_GRANULARITY,
39507
39699
  DEFAULT_LOG_PATH: () => DEFAULT_LOG_PATH,
39700
+ DEFAULT_REMOTE_FLUSH: () => DEFAULT_REMOTE_FLUSH,
39701
+ DEFAULT_REMOTE_LOG_PATH: () => DEFAULT_REMOTE_LOG_PATH,
39702
+ DEFAULT_REMOTE_RETENTION: () => DEFAULT_REMOTE_RETENTION,
39508
39703
  DEFAULT_TIER_USAGE_LOG_PATH: () => DEFAULT_TIER_USAGE_LOG_PATH,
39509
39704
  DEFAULT_TIER_USAGE_RETIER: () => DEFAULT_TIER_USAGE_RETIER,
39705
+ MIN_RETENTION_MAX_AGE_DAYS: () => MIN_RETENTION_MAX_AGE_DAYS,
39706
+ MIN_RETENTION_MAX_BYTES: () => MIN_RETENTION_MAX_BYTES,
39707
+ REMOTE_REQUIRED_FIELDS: () => REMOTE_REQUIRED_FIELDS,
39708
+ RemoteTelemetrySettings: () => RemoteTelemetrySettings,
39510
39709
  TelemetrySettings: () => TelemetrySettings,
39511
39710
  TierUsageSettings: () => TierUsageSettings,
39711
+ read_remote_settings: () => read_remote_settings,
39512
39712
  read_settings: () => read_settings,
39513
39713
  read_tier_usage_settings: () => read_tier_usage_settings
39514
39714
  });
@@ -39555,30 +39755,34 @@ function _isFile13(p) {
39555
39755
  return false;
39556
39756
  }
39557
39757
  }
39558
- function read_settings(p) {
39559
- let section = {};
39560
- let section_present = false;
39561
- if (_isFile13(p)) {
39562
- let raw = {};
39563
- try {
39564
- raw = YAML8.parse(fs69.readFileSync(p, "utf-8"), { version: "1.1" });
39565
- if (raw === null || raw === void 0) {
39566
- raw = {};
39567
- }
39568
- } catch {
39758
+ function _read_telemetry_section(p, key) {
39759
+ if (!_isFile13(p)) {
39760
+ return { section: {}, present: false };
39761
+ }
39762
+ let raw = {};
39763
+ try {
39764
+ raw = YAML8.parse(fs69.readFileSync(p, "utf-8"), { version: "1.1" });
39765
+ if (raw === null || raw === void 0) {
39569
39766
  raw = {};
39570
39767
  }
39571
- if (_isPlainObject8(raw)) {
39572
- const tele = raw["telemetry"];
39573
- if (_isPlainObject8(tele)) {
39574
- const artefact = tele["artifact_engagement"];
39575
- if (_isPlainObject8(artefact)) {
39576
- section = artefact;
39577
- section_present = true;
39578
- }
39579
- }
39580
- }
39768
+ } catch {
39769
+ raw = {};
39770
+ }
39771
+ if (!_isPlainObject8(raw)) {
39772
+ return { section: {}, present: false };
39773
+ }
39774
+ const tele = raw["telemetry"];
39775
+ if (!_isPlainObject8(tele)) {
39776
+ return { section: {}, present: false };
39581
39777
  }
39778
+ const sub = tele[key];
39779
+ if (!_isPlainObject8(sub)) {
39780
+ return { section: {}, present: false };
39781
+ }
39782
+ return { section: sub, present: true };
39783
+ }
39784
+ function read_settings(p) {
39785
+ const { section, present: section_present } = _read_telemetry_section(p, "artifact_engagement");
39582
39786
  const record = _isPlainObject8(section["record"]) ? section["record"] : {};
39583
39787
  const output = _isPlainObject8(section["output"]) ? section["output"] : {};
39584
39788
  return new TelemetrySettings({
@@ -39591,27 +39795,7 @@ function read_settings(p) {
39591
39795
  });
39592
39796
  }
39593
39797
  function read_tier_usage_settings(p) {
39594
- let section = {};
39595
- if (_isFile13(p)) {
39596
- let raw = {};
39597
- try {
39598
- raw = YAML8.parse(fs69.readFileSync(p, "utf-8"), { version: "1.1" });
39599
- if (raw === null || raw === void 0) {
39600
- raw = {};
39601
- }
39602
- } catch {
39603
- raw = {};
39604
- }
39605
- if (_isPlainObject8(raw)) {
39606
- const tele = raw["telemetry"];
39607
- if (_isPlainObject8(tele)) {
39608
- const tu = tele["tier_usage"];
39609
- if (_isPlainObject8(tu)) {
39610
- section = tu;
39611
- }
39612
- }
39613
- }
39614
- }
39798
+ const { section } = _read_telemetry_section(p, "tier_usage");
39615
39799
  const output = _isPlainObject8(section["output"]) ? section["output"] : {};
39616
39800
  const retier = _isPlainObject8(section["retier"]) ? section["retier"] : {};
39617
39801
  const defaults = DEFAULT_TIER_USAGE_RETIER;
@@ -39632,7 +39816,39 @@ function read_tier_usage_settings(p) {
39632
39816
  min_distinct_users: _coerce_int(retier["min_distinct_users"], defaults.min_distinct_users)
39633
39817
  });
39634
39818
  }
39635
- var YAML8, DEFAULT_LOG_PATH, DEFAULT_GRANULARITY, ALLOWED_GRANULARITIES, DEFAULT_TIER_USAGE_LOG_PATH, DEFAULT_TIER_USAGE_RETIER, TelemetrySettings, TierUsageSettings;
39819
+ function read_remote_settings(p) {
39820
+ const { section } = _read_telemetry_section(p, "remote");
39821
+ const output = _isPlainObject8(section["output"]) ? section["output"] : {};
39822
+ const retention = _isPlainObject8(section["retention"]) ? section["retention"] : {};
39823
+ const _coerce_bounded_int = (value, def, floor) => {
39824
+ if (typeof value === "boolean") {
39825
+ return def;
39826
+ }
39827
+ if (typeof value === "number" && Number.isInteger(value) && value >= floor) {
39828
+ return value;
39829
+ }
39830
+ return def;
39831
+ };
39832
+ return new RemoteTelemetrySettings({
39833
+ enabled: _coerce_bool(section["enabled"], false),
39834
+ endpoint: _coerce_str(section["endpoint"], ""),
39835
+ org_id: _coerce_str(section["org_id"], ""),
39836
+ salt: _coerce_str(section["salt"], ""),
39837
+ flush: _coerce_str(section["flush"], DEFAULT_REMOTE_FLUSH, ALLOWED_REMOTE_FLUSH),
39838
+ log_path: _coerce_path(output["path"], DEFAULT_REMOTE_LOG_PATH),
39839
+ retention_max_age_days: _coerce_bounded_int(
39840
+ retention["max_age_days"],
39841
+ DEFAULT_REMOTE_RETENTION.max_age_days,
39842
+ MIN_RETENTION_MAX_AGE_DAYS
39843
+ ),
39844
+ retention_max_bytes: _coerce_bounded_int(
39845
+ retention["max_bytes"],
39846
+ DEFAULT_REMOTE_RETENTION.max_bytes,
39847
+ MIN_RETENTION_MAX_BYTES
39848
+ )
39849
+ });
39850
+ }
39851
+ var YAML8, DEFAULT_LOG_PATH, DEFAULT_GRANULARITY, ALLOWED_GRANULARITIES, DEFAULT_TIER_USAGE_LOG_PATH, DEFAULT_TIER_USAGE_RETIER, TelemetrySettings, TierUsageSettings, DEFAULT_REMOTE_LOG_PATH, DEFAULT_REMOTE_FLUSH, ALLOWED_REMOTE_FLUSH, DEFAULT_REMOTE_RETENTION, MIN_RETENTION_MAX_AGE_DAYS, MIN_RETENTION_MAX_BYTES, REMOTE_REQUIRED_FIELDS, RemoteTelemetrySettings;
39636
39852
  var init_settings = __esm({
39637
39853
  "src/agent-src/templates/scripts/telemetry/settings.ts"() {
39638
39854
  "use strict";
@@ -39679,6 +39895,62 @@ var init_settings = __esm({
39679
39895
  this.min_distinct_users = init2.min_distinct_users;
39680
39896
  }
39681
39897
  };
39898
+ DEFAULT_REMOTE_LOG_PATH = ".agent-telemetry.jsonl";
39899
+ DEFAULT_REMOTE_FLUSH = "session-end";
39900
+ ALLOWED_REMOTE_FLUSH = ["session-end", "never"];
39901
+ DEFAULT_REMOTE_RETENTION = {
39902
+ max_age_days: 90,
39903
+ max_bytes: 2 * 1024 * 1024
39904
+ };
39905
+ MIN_RETENTION_MAX_AGE_DAYS = 1;
39906
+ MIN_RETENTION_MAX_BYTES = 4096;
39907
+ REMOTE_REQUIRED_FIELDS = ["endpoint", "org_id", "salt"];
39908
+ RemoteTelemetrySettings = class {
39909
+ /** The raw `enabled:` value. NOT the switch — see `active`. */
39910
+ enabled;
39911
+ endpoint;
39912
+ org_id;
39913
+ /** Org-pack secret. Never written into a record, never logged. */
39914
+ salt;
39915
+ flush;
39916
+ log_path;
39917
+ /**
39918
+ * Growth budget for `log_path`. Always present — an install that
39919
+ * declares no `retention:` gets the defaults rather than no policy,
39920
+ * because "unbounded" is the state R-A7 forbids and a settings file is
39921
+ * not a place to opt back into it.
39922
+ */
39923
+ retention_max_age_days;
39924
+ retention_max_bytes;
39925
+ constructor(init2) {
39926
+ this.enabled = init2.enabled;
39927
+ this.endpoint = init2.endpoint;
39928
+ this.org_id = init2.org_id;
39929
+ this.salt = init2.salt;
39930
+ this.flush = init2.flush;
39931
+ this.log_path = init2.log_path;
39932
+ this.retention_max_age_days = init2.retention_max_age_days;
39933
+ this.retention_max_bytes = init2.retention_max_bytes;
39934
+ }
39935
+ /**
39936
+ * Which required fields are absent. Empty array + `enabled` ⇒ `active`.
39937
+ * Exposed so a doctor command can say WHICH field is missing without
39938
+ * printing the salt, rather than reporting a bare "disabled".
39939
+ */
39940
+ get missing() {
39941
+ const out = [];
39942
+ for (const field of REMOTE_REQUIRED_FIELDS) {
39943
+ if (!this[field]) {
39944
+ out.push(field);
39945
+ }
39946
+ }
39947
+ return out;
39948
+ }
39949
+ /** The real switch: opted in AND fully configured by the org pack. */
39950
+ get active() {
39951
+ return this.enabled && this.missing.length === 0;
39952
+ }
39953
+ };
39682
39954
  }
39683
39955
  });
39684
39956
 
@@ -42152,6 +42424,242 @@ var init_qualification_wiring = __esm({
42152
42424
  }
42153
42425
  });
42154
42426
 
42427
+ // src/scripts/_lib/map_to_object.ts
42428
+ function _mapToObject(m) {
42429
+ const out = {};
42430
+ for (const [k, v] of m) {
42431
+ out[k] = v;
42432
+ }
42433
+ return out;
42434
+ }
42435
+ var init_map_to_object = __esm({
42436
+ "src/scripts/_lib/map_to_object.ts"() {
42437
+ "use strict";
42438
+ }
42439
+ });
42440
+
42441
+ // src/scripts/_lib/council_settings_block.ts
42442
+ function synthesizeAiCouncilBlock(cfg) {
42443
+ const members = {};
42444
+ for (const [name, m] of cfg.members) {
42445
+ const entry = { enabled: m.enabled, model: m.model };
42446
+ if (m.api_key_ref !== null) {
42447
+ entry["api_key_ref"] = m.api_key_ref;
42448
+ }
42449
+ if (m.mode !== null) {
42450
+ entry["mode"] = m.mode;
42451
+ }
42452
+ if (m.binary !== null) {
42453
+ entry["binary"] = m.binary;
42454
+ }
42455
+ if (m.model_ladder && m.model_ladder.length > 0) {
42456
+ entry["model_ladder"] = [...m.model_ladder];
42457
+ }
42458
+ if (m.prompt_cache_ttl !== "5m") {
42459
+ entry["prompt_cache_ttl"] = m.prompt_cache_ttl;
42460
+ }
42461
+ members[name] = entry;
42462
+ }
42463
+ const advisors = {};
42464
+ for (const [name, a] of cfg.advisors) {
42465
+ const entry = {
42466
+ enabled: a.enabled,
42467
+ member: a.member,
42468
+ persona: a.persona
42469
+ };
42470
+ if (a.model !== null) {
42471
+ entry["model"] = a.model;
42472
+ }
42473
+ advisors[name] = entry;
42474
+ }
42475
+ const lensCostDisclosure = {};
42476
+ for (const [lens, cd] of cfg.lens_overrides.cost_disclosure) {
42477
+ lensCostDisclosure[lens] = {
42478
+ mode: cd.mode,
42479
+ threshold_usd: cd.threshold_usd,
42480
+ show_per_member: cd.show_per_member
42481
+ };
42482
+ }
42483
+ const lensModelDowngrade = {};
42484
+ for (const [lens, md] of cfg.lens_overrides.model_downgrade) {
42485
+ lensModelDowngrade[lens] = { enabled: md.enabled, auto_apply: md.auto_apply };
42486
+ }
42487
+ return {
42488
+ enabled: cfg.enabled,
42489
+ mode: cfg.defaults.mode,
42490
+ min_rounds: cfg.defaults.min_rounds,
42491
+ deep_min_rounds: cfg.defaults.deep_min_rounds,
42492
+ max_output_tokens: cfg.defaults.max_output_tokens,
42493
+ session_retention_days: cfg.defaults.session_retention_days,
42494
+ debate_max_rounds: cfg.defaults.debate_max_rounds,
42495
+ cost_budget: {
42496
+ max_input_tokens: cfg.cost_budget.max_input_tokens,
42497
+ max_output_tokens: cfg.cost_budget.max_output_tokens,
42498
+ max_calls: cfg.cost_budget.max_calls,
42499
+ max_total_usd: cfg.cost_budget.max_total_usd,
42500
+ daily_limit_usd: cfg.cost_budget.daily_limit_usd
42501
+ },
42502
+ consensus_scoring: {
42503
+ enabled: cfg.consensus_scoring.enabled,
42504
+ strong_threshold: cfg.consensus_scoring.strong_threshold,
42505
+ minority_threshold: cfg.consensus_scoring.minority_threshold,
42506
+ lenses: [...cfg.consensus_scoring.lenses]
42507
+ },
42508
+ cli_call_budget: {
42509
+ max_calls_per_day: _mapToObject(cfg.cli_call_budget.max_calls_per_day),
42510
+ warn_at: cfg.cli_call_budget.warn_at
42511
+ },
42512
+ // Phase 3.3 (road-to-always-on-orchestration): was validated by
42513
+ // `_build_config`/`_build_quorum` but never forwarded into the
42514
+ // synthesized block, so `build_members` always saw `undefined` and
42515
+ // silently fell back to `'majority'` regardless of the user's
42516
+ // configured `quorum: <k>`. Additive key — callers that never read
42517
+ // it observe no change.
42518
+ quorum: cfg.quorum,
42519
+ // Forwarded for the same reason `quorum` above had to be: a key that
42520
+ // `_build_config` validates and this block drops is a key the loader
42521
+ // enforces and the runtime never sees. That defect shipped once here
42522
+ // already; repeating it for the shadow floor would silently pin every
42523
+ // `floor_would_hold` to the default no matter what the operator set.
42524
+ quorum_min_present: cfg.quorum_min_present,
42525
+ // Third instance of the same defect the two comments above describe,
42526
+ // and the most expensive of the three: `build_members` reads
42527
+ // `ai_council.fallback.api_on_quota` off THIS block, so before the key
42528
+ // was forwarded no config file could turn quota fall-through on. The
42529
+ // documentation, the contract section and the tests all described a
42530
+ // switch that production could not flip.
42531
+ fallback: { api_on_quota: cfg.fallback.api_on_quota },
42532
+ necessity_classifier: {
42533
+ enabled: cfg.necessity_classifier.enabled,
42534
+ mode: cfg.necessity_classifier.mode,
42535
+ user_explicit_mode: cfg.necessity_classifier.user_explicit_mode
42536
+ },
42537
+ model_downgrade: {
42538
+ enabled: cfg.model_downgrade.enabled,
42539
+ auto_apply: cfg.model_downgrade.auto_apply
42540
+ },
42541
+ debate: {
42542
+ max_cost_usd: cfg.debate.max_cost_usd,
42543
+ cost_disclosure: {
42544
+ mode: cfg.debate.cost_disclosure.mode,
42545
+ threshold_usd: cfg.debate.cost_disclosure.threshold_usd,
42546
+ show_per_member: cfg.debate.cost_disclosure.show_per_member
42547
+ }
42548
+ },
42549
+ lens_overrides: {
42550
+ necessity_classifier_mode: _mapToObject(
42551
+ cfg.lens_overrides.necessity_classifier_mode
42552
+ ),
42553
+ necessity_classifier_user_explicit_mode: _mapToObject(
42554
+ cfg.lens_overrides.necessity_classifier_user_explicit_mode
42555
+ ),
42556
+ model_downgrade: lensModelDowngrade,
42557
+ cost_disclosure: lensCostDisclosure
42558
+ },
42559
+ members,
42560
+ advisors
42561
+ };
42562
+ }
42563
+ var init_council_settings_block = __esm({
42564
+ "src/scripts/_lib/council_settings_block.ts"() {
42565
+ "use strict";
42566
+ init_map_to_object();
42567
+ }
42568
+ });
42569
+
42570
+ // src/scripts/_lib/council_fallback_posture.ts
42571
+ function fallbackPostureFor(input) {
42572
+ if (input.transport !== "cli") return "n/a";
42573
+ if (!input.hasApiRung) return "none";
42574
+ if (input.apiKeyRef !== null) return input.refResolves === true ? "api" : "none";
42575
+ if (input.keylessResolves === void 0) return "none";
42576
+ try {
42577
+ return input.keylessResolves() ? "api" : "none";
42578
+ } catch {
42579
+ return "none";
42580
+ }
42581
+ }
42582
+ function renderPostureLines(postures, apiOnQuota) {
42583
+ const live = postures.filter(([, p]) => p !== "n/a");
42584
+ if (live.length === 0) return [];
42585
+ const lines = live.map(
42586
+ ([name, p]) => ` fallback ${name}: ` + (p === "api" ? "would fall back to api" : "no api rung \u2014 a lost cli transport loses the seat")
42587
+ );
42588
+ lines.push(
42589
+ ` fallback quota api_on_quota: ${apiOnQuota ? "on" : "off"} \u2014 an exhausted cli quota ${apiOnQuota ? "MAY" : "may not"} retry on the metered rung`
42590
+ );
42591
+ return lines;
42592
+ }
42593
+ var init_council_fallback_posture = __esm({
42594
+ "src/scripts/_lib/council_fallback_posture.ts"() {
42595
+ "use strict";
42596
+ }
42597
+ });
42598
+
42599
+ // src/scripts/_lib/council_fallback_wiring.ts
42600
+ function isRefusal(exc) {
42601
+ if (exc instanceof CouncilConfigError) return true;
42602
+ return exc instanceof Error && REFUSAL_NAMES.has(exc.constructor.name);
42603
+ }
42604
+ function buildFallbackOptions(deps) {
42605
+ return {
42606
+ api_on_quota: deps.apiOnQuota,
42607
+ // The orchestrator is a pure library and holds no sink; this is it.
42608
+ // One line per establishing escalation, so attendance analysis can
42609
+ // tell a seat SAVED by the fallback from one that was natively api.
42610
+ on_event: (e) => {
42611
+ try {
42612
+ deps.emit({
42613
+ action: "transport_fallback",
42614
+ provider: e.provider,
42615
+ failure_class: e.failure,
42616
+ outcome: e.outcome,
42617
+ api_on_quota: e.api_on_quota
42618
+ });
42619
+ } catch {
42620
+ }
42621
+ },
42622
+ construct: (provider) => {
42623
+ if (!deps.hasApiRung(provider)) return null;
42624
+ const cfg = deps.memberConfig(provider);
42625
+ const model = deps.modelOverride(provider) ?? cfg["model"] ?? null;
42626
+ try {
42627
+ return deps.constructApi(provider, model, {
42628
+ api_key_ref: cfg["api_key_ref"] ?? null,
42629
+ enable_prompt_cache: cfg["prompt_cache"] !== false,
42630
+ prompt_cache_ttl: cfg["prompt_cache_ttl"]
42631
+ });
42632
+ } catch (exc) {
42633
+ if (isRefusal(exc)) return null;
42634
+ throw exc;
42635
+ }
42636
+ }
42637
+ };
42638
+ }
42639
+ var REFUSAL_NAMES;
42640
+ var init_council_fallback_wiring = __esm({
42641
+ "src/scripts/_lib/council_fallback_wiring.ts"() {
42642
+ "use strict";
42643
+ init_config();
42644
+ REFUSAL_NAMES = /* @__PURE__ */ new Set([
42645
+ "CouncilDisabledError",
42646
+ "CliClientError",
42647
+ "CouncilConfigError",
42648
+ // R2 round 6, critical finding 1 — and the docblock above already told the
42649
+ // story one name short. `KeyGateError` is what an api-twin construction
42650
+ // raises when the provider key is absent or violates the 0600 contract,
42651
+ // which is the ORDINARY shape of the very configuration this fallback
42652
+ // targets: a cli member under subscription auth, no `api_key_ref`, no
42653
+ // local key. It escaped `construct()`, escaped `consult()`, and is not in
42654
+ // `main`'s catch list — so instead of losing one seat, the whole pass died
42655
+ // with an unhandled exception. A mechanism that turns a recoverable seat
42656
+ // loss into a total loss is worse than no mechanism, which is the sentence
42657
+ // directly above this list.
42658
+ "KeyGateError"
42659
+ ]);
42660
+ }
42661
+ });
42662
+
42155
42663
  // src/scripts/ai_council/low_impact_corpus.ts
42156
42664
  var low_impact_corpus_exports = {};
42157
42665
  __export(low_impact_corpus_exports, {
@@ -44190,6 +44698,293 @@ var init_stance_tally = __esm({
44190
44698
  }
44191
44699
  });
44192
44700
 
44701
+ // src/scripts/ai_council/mid_flight_fallback.ts
44702
+ function establishTwin(input) {
44703
+ const { member, error, fallback, ledger } = input;
44704
+ const failure = classifyCliFailure(error);
44705
+ const policy = { apiOnQuota: fallback.api_on_quota };
44706
+ if (!isFallbackEligibleUnder(failure, policy)) return null;
44707
+ if (ledger.attempt(member.name, failure, policy) !== "api") return null;
44708
+ const twin = fallback.construct(member.name);
44709
+ if (twin === null) {
44710
+ fallback.on_event?.({
44711
+ provider: member.name,
44712
+ failure,
44713
+ outcome: "no_twin",
44714
+ api_on_quota: fallback.api_on_quota
44715
+ });
44716
+ return null;
44717
+ }
44718
+ return { client: twin, reason: failure, original_error: error };
44719
+ }
44720
+ function emitOutcome(fallback, provider, twin, outcome) {
44721
+ fallback.on_event?.({
44722
+ provider,
44723
+ failure: twin.reason,
44724
+ outcome,
44725
+ api_on_quota: fallback.api_on_quota
44726
+ });
44727
+ }
44728
+ function stampFallback(response, twin, sticky) {
44729
+ response.metadata = {
44730
+ ...response.metadata ?? {},
44731
+ fallback_from: "cli",
44732
+ fallback_reason: twin.reason,
44733
+ fallback_original_error: twin.original_error,
44734
+ ...sticky ? { fallback_sticky: true } : {}
44735
+ };
44736
+ }
44737
+ function isFailedCliCall(member, error, transport) {
44738
+ return error !== null && String(error) !== "" && transport === "cli";
44739
+ }
44740
+ function runGatedRetry(original, member, twin, fallback, deps, ledger = null) {
44741
+ if (!deps.gate(twin.client)) {
44742
+ original.metadata = { ...original.metadata ?? {}, fallback_skipped: "cost_budget" };
44743
+ emitOutcome(fallback, member.name, twin, "cost_budget");
44744
+ ledger?.release(member.name);
44745
+ return { response: original, effective: member, retried: false };
44746
+ }
44747
+ const response = deps.call(twin.client);
44748
+ stampFallback(response, twin, false);
44749
+ emitOutcome(fallback, member.name, twin, "retried");
44750
+ return { response, effective: twin.client, retried: true };
44751
+ }
44752
+ function escalateUnmetered(member, response, transport, fallback, ledger) {
44753
+ if (fallback === null || ledger === null) return null;
44754
+ if (!isFailedCliCall(member, response.error, transport)) return null;
44755
+ const twin = establishTwin({
44756
+ member,
44757
+ error: String(response.error),
44758
+ fallback,
44759
+ ledger
44760
+ });
44761
+ if (twin === null) return null;
44762
+ return twin;
44763
+ }
44764
+ function refuseUnmeteredEscalation(args) {
44765
+ const { original, declared, twin, fallback, ledger, twins } = args;
44766
+ original.metadata = { ...original.metadata ?? {}, fallback_skipped: "cost_budget" };
44767
+ if (fallback !== null) emitOutcome(fallback, declared.name, twin, "cost_budget");
44768
+ twins?.delete(declared.name);
44769
+ ledger?.release(declared.name);
44770
+ return original;
44771
+ }
44772
+ var init_mid_flight_fallback = __esm({
44773
+ "src/scripts/ai_council/mid_flight_fallback.ts"() {
44774
+ "use strict";
44775
+ init_transport_resolver();
44776
+ }
44777
+ });
44778
+
44779
+ // src/scripts/ai_council/spend_gate.ts
44780
+ function _total_usd(e) {
44781
+ return e.input_usd + e.output_usd;
44782
+ }
44783
+ function _breach(est, spent, budget) {
44784
+ const usd = est ? _total_usd(est) : 0;
44785
+ if (spent.input + (est ? est.input_tokens : 0) > budget.max_input_tokens || spent.output + (est ? est.output_tokens : 0) > budget.max_output_tokens) {
44786
+ return "tokens";
44787
+ }
44788
+ if (budget.daily_limit_usd > 0 && would_exceed(budget.daily_limit_usd, usd)) {
44789
+ return "daily";
44790
+ }
44791
+ if (budget.max_total_usd > 0 && spent.usd + usd > budget.max_total_usd) return "session";
44792
+ return null;
44793
+ }
44794
+ var CostBudget, OverrunEvent;
44795
+ var init_spend_gate = __esm({
44796
+ "src/scripts/ai_council/spend_gate.ts"() {
44797
+ "use strict";
44798
+ init_budget_guard();
44799
+ CostBudget = class {
44800
+ max_input_tokens;
44801
+ max_output_tokens;
44802
+ max_calls;
44803
+ max_total_usd;
44804
+ // 0 = USD ceiling disabled (token caps still apply)
44805
+ daily_limit_usd;
44806
+ // 0 = rolling 24h cap disabled (D3)
44807
+ constructor(args = {}) {
44808
+ this.max_input_tokens = args.max_input_tokens ?? 5e4;
44809
+ this.max_output_tokens = args.max_output_tokens ?? 2e4;
44810
+ this.max_calls = args.max_calls ?? 10;
44811
+ this.max_total_usd = args.max_total_usd ?? 0;
44812
+ this.daily_limit_usd = args.daily_limit_usd ?? 0;
44813
+ }
44814
+ };
44815
+ OverrunEvent = class {
44816
+ member_index;
44817
+ member;
44818
+ next_estimate;
44819
+ // this member's projected cost
44820
+ spent_input_tokens;
44821
+ // already-billed totals BEFORE this member
44822
+ spent_output_tokens;
44823
+ spent_usd;
44824
+ projected_total_usd;
44825
+ // spent_usd + next_estimate.total_usd
44826
+ daily_spent_usd;
44827
+ // rolling 24h spend BEFORE this member (D3)
44828
+ daily_limit_usd;
44829
+ // the configured daily cap (0 = disabled)
44830
+ breach_kind;
44831
+ // "session" | "daily" | "tokens"
44832
+ constructor(args) {
44833
+ this.member_index = args.member_index;
44834
+ this.member = args.member;
44835
+ this.next_estimate = args.next_estimate;
44836
+ this.spent_input_tokens = args.spent_input_tokens;
44837
+ this.spent_output_tokens = args.spent_output_tokens;
44838
+ this.spent_usd = args.spent_usd;
44839
+ this.projected_total_usd = args.projected_total_usd;
44840
+ this.daily_spent_usd = args.daily_spent_usd ?? 0;
44841
+ this.daily_limit_usd = args.daily_limit_usd ?? 0;
44842
+ this.breach_kind = args.breach_kind ?? "session";
44843
+ }
44844
+ };
44845
+ }
44846
+ });
44847
+
44848
+ // src/scripts/ai_council/py_parity.ts
44849
+ function _pyInt5(x) {
44850
+ return Math.trunc(x);
44851
+ }
44852
+ function _pyReprInt(x) {
44853
+ return String(x);
44854
+ }
44855
+ function _excTag(exc) {
44856
+ if (exc instanceof Error) {
44857
+ return `${exc.name}: ${exc.message}`;
44858
+ }
44859
+ return `Error: ${String(exc)}`;
44860
+ }
44861
+ function _setdefault(obj, key, value) {
44862
+ if (!(key in obj)) {
44863
+ obj[key] = value;
44864
+ }
44865
+ }
44866
+ function _metaGet(obj, key, fallback) {
44867
+ return key in obj ? obj[key] : fallback;
44868
+ }
44869
+ function _pyLStrip(s) {
44870
+ return s.replace(/^\s+/, "");
44871
+ }
44872
+ function _pyRStrip2(s) {
44873
+ return s.replace(/\s+$/, "");
44874
+ }
44875
+ function _pyLen5(s) {
44876
+ let n = 0;
44877
+ for (const _ of s) {
44878
+ n += 1;
44879
+ }
44880
+ return n;
44881
+ }
44882
+ function _pySlice(s, start, end) {
44883
+ return [...s].slice(start, end).join("");
44884
+ }
44885
+ function _pyFixed3(x, ndigits) {
44886
+ if (!Number.isFinite(x)) {
44887
+ return String(x);
44888
+ }
44889
+ const neg = x < 0 || Object.is(x, -0);
44890
+ const abs = Math.abs(x);
44891
+ const factor = Math.pow(10, ndigits);
44892
+ const scaled = abs * factor;
44893
+ const floor = Math.floor(scaled);
44894
+ const frac = scaled - floor;
44895
+ const tol = Math.max(Math.abs(scaled), 1) * 2 ** -40;
44896
+ let rounded;
44897
+ if (Math.abs(frac - 0.5) <= tol) {
44898
+ rounded = floor % 2 === 0 ? floor : floor + 1;
44899
+ } else {
44900
+ rounded = Math.round(scaled);
44901
+ }
44902
+ let intStr = String(rounded);
44903
+ let result;
44904
+ if (ndigits === 0) {
44905
+ result = intStr;
44906
+ } else {
44907
+ if (intStr.length <= ndigits) {
44908
+ intStr = "0".repeat(ndigits - intStr.length + 1) + intStr;
44909
+ }
44910
+ const whole = intStr.slice(0, intStr.length - ndigits);
44911
+ const dec = intStr.slice(intStr.length - ndigits);
44912
+ result = `${whole}.${dec}`;
44913
+ }
44914
+ return neg ? `-${result}` : result;
44915
+ }
44916
+ function _pyStrip5(s) {
44917
+ return s.trim();
44918
+ }
44919
+ function _pySplitWhitespace(s) {
44920
+ const trimmed = s.trim();
44921
+ if (trimmed === "") {
44922
+ return [];
44923
+ }
44924
+ return trimmed.split(/\s+/);
44925
+ }
44926
+ var init_py_parity = __esm({
44927
+ "src/scripts/ai_council/py_parity.ts"() {
44928
+ "use strict";
44929
+ }
44930
+ });
44931
+
44932
+ // src/scripts/ai_council/response_render.ts
44933
+ function renderResponseMeta(r, deps) {
44934
+ const meta_dict = r.metadata ?? {};
44935
+ const billable = Boolean(_metaGet(meta_dict, "billable", true));
44936
+ const estimated = Boolean(_metaGet(meta_dict, "tokens_estimated", false));
44937
+ const parts = [];
44938
+ if (!billable) {
44939
+ const label = _metaGet(meta_dict, "subscription_label", null) || "flat-rate";
44940
+ parts.push(`cost: subscription (${label})`);
44941
+ } else {
44942
+ const cost_usd = _metaGet(meta_dict, "cost_usd", void 0);
44943
+ if (typeof cost_usd === "number" && !(typeof cost_usd === "boolean")) {
44944
+ parts.push(`cost: $${deps.pyFixed(cost_usd, 4)}`);
44945
+ }
44946
+ const prefix = estimated ? "~" : "";
44947
+ parts.push(
44948
+ `tokens: ${prefix}${r.input_tokens} in / ${prefix}${r.output_tokens} out`
44949
+ );
44950
+ }
44951
+ parts.push(`${r.latency_ms} ms`);
44952
+ const fallback_from = _metaGet(meta_dict, "fallback_from", null);
44953
+ if (typeof fallback_from === "string" && fallback_from !== "") {
44954
+ const reason = _metaGet(meta_dict, "fallback_reason", null);
44955
+ const sticky = _metaGet(meta_dict, "fallback_sticky", false) === true;
44956
+ const why = typeof reason === "string" && reason !== "" ? `: ${reason}` : "";
44957
+ parts.push(
44958
+ sticky ? `transport: api (${fallback_from} lost earlier this pass${why})` : `transport: api (fell back from ${fallback_from}${why})`
44959
+ );
44960
+ }
44961
+ return `*${parts.join(" \xB7 ")}*`;
44962
+ }
44963
+ function callMember(client, question, opts, systemFor, excTag) {
44964
+ try {
44965
+ return opts.split ? client.ask_split(
44966
+ systemFor(client),
44967
+ opts.split.stable,
44968
+ opts.split.suffix,
44969
+ question.max_tokens
44970
+ ) : client.ask(systemFor(client), question.user_prompt, question.max_tokens);
44971
+ } catch (exc) {
44972
+ return new CouncilResponse({
44973
+ provider: client.name,
44974
+ model: client.model,
44975
+ text: "",
44976
+ error: excTag(exc)
44977
+ });
44978
+ }
44979
+ }
44980
+ var init_response_render = __esm({
44981
+ "src/scripts/ai_council/response_render.ts"() {
44982
+ "use strict";
44983
+ init_clients();
44984
+ init_py_parity();
44985
+ }
44986
+ });
44987
+
44193
44988
  // src/scripts/ai_council/blind_review.ts
44194
44989
  import { createHash as createHash12 } from "node:crypto";
44195
44990
  function _sha256_hex(s) {
@@ -44345,6 +45140,8 @@ __export(orchestrator_exports, {
44345
45140
  DebateCostEstimate: () => DebateCostEstimate,
44346
45141
  OverrunEvent: () => OverrunEvent,
44347
45142
  PeerReviewResult: () => PeerReviewResult,
45143
+ _breach: () => _breach,
45144
+ _total_usd: () => _total_usd,
44348
45145
  consult: () => consult,
44349
45146
  estimate: () => estimate,
44350
45147
  estimate_debate_cost: () => estimate_debate_cost,
@@ -44353,47 +45150,6 @@ __export(orchestrator_exports, {
44353
45150
  run_debate: () => run_debate,
44354
45151
  run_peer_review: () => run_peer_review
44355
45152
  });
44356
- function _pyFixed3(x, ndigits) {
44357
- if (!Number.isFinite(x)) {
44358
- return String(x);
44359
- }
44360
- const neg = x < 0 || Object.is(x, -0);
44361
- const abs = Math.abs(x);
44362
- const factor = Math.pow(10, ndigits);
44363
- const scaled = abs * factor;
44364
- const floor = Math.floor(scaled);
44365
- const frac = scaled - floor;
44366
- const tol = Math.max(Math.abs(scaled), 1) * 2 ** -40;
44367
- let rounded;
44368
- if (Math.abs(frac - 0.5) <= tol) {
44369
- rounded = floor % 2 === 0 ? floor : floor + 1;
44370
- } else {
44371
- rounded = Math.round(scaled);
44372
- }
44373
- let intStr = String(rounded);
44374
- let result;
44375
- if (ndigits === 0) {
44376
- result = intStr;
44377
- } else {
44378
- if (intStr.length <= ndigits) {
44379
- intStr = "0".repeat(ndigits - intStr.length + 1) + intStr;
44380
- }
44381
- const whole = intStr.slice(0, intStr.length - ndigits);
44382
- const dec = intStr.slice(intStr.length - ndigits);
44383
- result = `${whole}.${dec}`;
44384
- }
44385
- return neg ? `-${result}` : result;
44386
- }
44387
- function _pyStrip5(s) {
44388
- return s.trim();
44389
- }
44390
- function _pySplitWhitespace(s) {
44391
- const trimmed = s.trim();
44392
- if (trimmed === "") {
44393
- return [];
44394
- }
44395
- return trimmed.split(/\s+/);
44396
- }
44397
45153
  function _getattr2(obj, name, fallback) {
44398
45154
  if (obj === null || obj === void 0) {
44399
45155
  return fallback;
@@ -44457,9 +45213,6 @@ function estimate_debate_cost(question, members, table, opts) {
44457
45213
  subscription_members: sub_members
44458
45214
  });
44459
45215
  }
44460
- function _total_usd(e) {
44461
- return e.input_usd + e.output_usd;
44462
- }
44463
45216
  function estimate(question, members, table, opts = {}) {
44464
45217
  const project = opts.project ?? null;
44465
45218
  const original_ask = opts.original_ask ?? "";
@@ -44508,6 +45261,9 @@ function consult(members, question, budget = null, opts = {}) {
44508
45261
  );
44509
45262
  }
44510
45263
  const spent = { input: 0, output: 0, usd: 0 };
45264
+ const cli_fallback = opts.cli_fallback ?? null;
45265
+ const fallback_ledger = cli_fallback !== null ? new MidFlightFallback() : null;
45266
+ const fallback_twins = cli_fallback !== null ? /* @__PURE__ */ new Map() : null;
44511
45267
  let last_results = [];
44512
45268
  let current_suffix = "";
44513
45269
  const stance_tally = opts.stance_tally ?? false;
@@ -44530,6 +45286,9 @@ ${STANCE_LINE_CONTRACT}` : current_suffix;
44530
45286
  project,
44531
45287
  original_ask,
44532
45288
  advisor_plans,
45289
+ cli_fallback,
45290
+ fallback_ledger,
45291
+ fallback_twins,
44533
45292
  member_prompt_suffix: opts.member_prompt_suffix ?? null,
44534
45293
  no_project_context_members: opts.no_project_context_members ?? null,
44535
45294
  split: suffix_for_round ? { stable: question.user_prompt, suffix: suffix_for_round } : null
@@ -44564,7 +45323,10 @@ ${STANCE_LINE_CONTRACT}`,
44564
45323
  on_overrun,
44565
45324
  project,
44566
45325
  original_ask,
44567
- advisor_plans
45326
+ advisor_plans,
45327
+ cli_fallback,
45328
+ fallback_ledger,
45329
+ fallback_twins
44568
45330
  });
44569
45331
  const rep = rr[0];
44570
45332
  if (rep !== void 0) {
@@ -44611,55 +45373,78 @@ ${suffix}` : base;
44611
45373
  original_ask,
44612
45374
  advisor_plans: opts.advisor_plans ?? null
44613
45375
  }) : null;
45376
+ const twins = opts.fallback_twins ?? null;
44614
45377
  for (let idx = 0; idx < members.length; idx++) {
44615
- const member = members[idx];
45378
+ const declared = members[idx];
45379
+ let established = twins?.get(declared.name) ?? null;
45380
+ const substituted_on_entry = established !== null;
45381
+ let member = established !== null ? established.client : declared;
45382
+ let unmetered_original = null;
45383
+ let unmetered_twin = null;
44616
45384
  if (!_getattr2(member, "billable", true)) {
44617
- let response2;
44618
- try {
44619
- response2 = opts.split ? member.ask_split(
44620
- _system_prompt_for_member(member),
44621
- opts.split.stable,
44622
- opts.split.suffix,
44623
- question.max_tokens
44624
- ) : member.ask(
44625
- _system_prompt_for_member(member),
44626
- question.user_prompt,
44627
- question.max_tokens
44628
- );
44629
- } catch (exc) {
44630
- response2 = new CouncilResponse({
44631
- provider: member.name,
44632
- model: member.model,
44633
- text: "",
44634
- error: _excTag(exc)
44635
- });
45385
+ let response2 = callMember(member, question, opts, _system_prompt_for_member, _excTag);
45386
+ const nb_twin = escalateUnmetered(
45387
+ member,
45388
+ response2,
45389
+ _getattr2(member, "transport", "api"),
45390
+ opts.cli_fallback ?? null,
45391
+ opts.fallback_ledger ?? null
45392
+ );
45393
+ const escalated = nb_twin !== null;
45394
+ if (nb_twin !== null) {
45395
+ established = nb_twin;
45396
+ twins?.set(member.name, nb_twin);
45397
+ member = nb_twin.client;
45398
+ unmetered_original = response2;
45399
+ unmetered_twin = nb_twin;
45400
+ }
45401
+ if (!escalated) {
45402
+ _stamp_transport_metadata(response2, member);
45403
+ results.push(response2);
45404
+ spent.input += response2.input_tokens;
45405
+ spent.output += response2.output_tokens;
45406
+ continue;
44636
45407
  }
44637
- _stamp_transport_metadata(response2, member);
44638
- results.push(response2);
44639
- spent.input += response2.input_tokens;
44640
- spent.output += response2.output_tokens;
44641
- continue;
44642
- }
44643
- const est = estimates ? estimates[idx] : null;
44644
- const proj_input = spent.input + (est ? est.input_tokens : 0);
44645
- const proj_output = spent.output + (est ? est.output_tokens : 0);
44646
- const proj_usd = spent.usd + (est ? _total_usd(est) : 0);
44647
- const next_call_usd = est ? _total_usd(est) : 0;
44648
- const breaches_tokens = proj_input > budget.max_input_tokens || proj_output > budget.max_output_tokens;
44649
- const breaches_usd = budget.max_total_usd > 0 && proj_usd > budget.max_total_usd;
44650
- const breaches_daily = budget.daily_limit_usd > 0 && would_exceed(budget.daily_limit_usd, next_call_usd);
44651
- if (breaches_tokens || breaches_usd || breaches_daily) {
44652
- const breach_kind = breaches_tokens ? "tokens" : breaches_daily ? "daily" : "session";
45408
+ }
45409
+ const est = established !== null && table !== null ? estimate(question, [member], table, {
45410
+ project,
45411
+ original_ask,
45412
+ advisor_plans: opts.advisor_plans ?? null
45413
+ })[0] : estimates ? estimates[idx] : null;
45414
+ const breach = _breach(est, spent, budget);
45415
+ if (breach !== null) {
45416
+ const breach_kind = breach;
44653
45417
  const error_tag = breach_kind === "daily" ? "daily_budget_exceeded" : "cost_budget_exceeded";
45418
+ if (unmetered_original !== null && unmetered_twin !== null) {
45419
+ const refused = refuseUnmeteredEscalation({
45420
+ original: unmetered_original,
45421
+ declared,
45422
+ twin: unmetered_twin,
45423
+ fallback: opts.cli_fallback ?? null,
45424
+ ledger: opts.fallback_ledger ?? null,
45425
+ twins
45426
+ });
45427
+ _stamp_transport_metadata(refused, declared);
45428
+ results.push(refused);
45429
+ spent.input += refused.input_tokens;
45430
+ spent.output += refused.output_tokens;
45431
+ continue;
45432
+ }
44654
45433
  if (on_overrun !== null && estimates !== null) {
44655
45434
  const event = new OverrunEvent({
44656
45435
  member_index: idx,
44657
45436
  member,
44658
- next_estimate: estimates[idx],
45437
+ // `est`, not `estimates[idx]`. The breach decision two
45438
+ // lines up was already re-priced for a substituted twin;
45439
+ // the event that ASKS the operator to approve it showed
45440
+ // the pre-loop row for the unmetered DECLARED seat, i.e. a
45441
+ // ~$0 next-call estimate for the very call the gate just
45442
+ // refused as over budget. R2 review, finding 8.
45443
+ next_estimate: est ?? estimates[idx],
44659
45444
  spent_input_tokens: _pyInt5(spent.input),
44660
45445
  spent_output_tokens: _pyInt5(spent.output),
44661
45446
  spent_usd: spent.usd,
44662
- projected_total_usd: proj_usd,
45447
+ projected_total_usd: spent.usd + (est ? _total_usd(est) : 0),
44663
45448
  daily_spent_usd: budget.daily_limit_usd > 0 ? today_spend_usd() : 0,
44664
45449
  daily_limit_usd: budget.daily_limit_usd,
44665
45450
  breach_kind
@@ -44675,34 +45460,48 @@ ${suffix}` : base;
44675
45460
  return results;
44676
45461
  }
44677
45462
  }
44678
- let response;
44679
- try {
44680
- response = opts.split ? member.ask_split(
44681
- _system_prompt_for_member(member),
44682
- opts.split.stable,
44683
- opts.split.suffix,
44684
- question.max_tokens
44685
- ) : member.ask(
44686
- _system_prompt_for_member(member),
44687
- question.user_prompt,
44688
- question.max_tokens
44689
- );
44690
- } catch (exc) {
44691
- response = new CouncilResponse({
44692
- provider: member.name,
44693
- model: member.model,
44694
- text: "",
44695
- error: _excTag(exc)
45463
+ if (unmetered_twin !== null) {
45464
+ const fb = opts.cli_fallback ?? null;
45465
+ if (fb !== null) emitOutcome(fb, declared.name, unmetered_twin, "retried");
45466
+ }
45467
+ let response = callMember(member, question, opts, _system_prompt_for_member, _excTag);
45468
+ let effective = member;
45469
+ const fallback = opts.cli_fallback ?? null;
45470
+ const ledger = opts.fallback_ledger ?? null;
45471
+ if (fallback !== null && ledger !== null && isFailedCliCall(member, response.error, _getattr2(member, "transport", "api"))) {
45472
+ const twin = establishTwin({
45473
+ member,
45474
+ error: String(response.error),
45475
+ fallback,
45476
+ ledger
44696
45477
  });
44697
- }
45478
+ if (twin !== null) {
45479
+ const out = runGatedRetry(response, member, twin, fallback, {
45480
+ gate: (client) => table === null || _breach(
45481
+ estimate(question, [client], table, {
45482
+ project,
45483
+ original_ask,
45484
+ advisor_plans: opts.advisor_plans ?? null
45485
+ })[0],
45486
+ spent,
45487
+ budget
45488
+ ) === null,
45489
+ call: (client) => callMember(client, question, opts, _system_prompt_for_member, _excTag)
45490
+ }, ledger);
45491
+ response = out.response;
45492
+ effective = out.effective;
45493
+ if (out.retried) twins?.set(member.name, twin);
45494
+ }
45495
+ }
45496
+ if (established !== null) stampFallback(response, established, substituted_on_entry);
44698
45497
  results.push(response);
44699
45498
  spent.input += response.input_tokens;
44700
45499
  spent.output += response.output_tokens;
44701
45500
  let actual_usd = null;
44702
45501
  if (estimates !== null && table !== null) {
44703
45502
  const actual = reprice_with_cache(
44704
- member.name,
44705
- member.model,
45503
+ effective.name,
45504
+ effective.model,
44706
45505
  {
44707
45506
  input_tokens: response.input_tokens,
44708
45507
  cache_read_input_tokens: response.cache_read_input_tokens,
@@ -44715,14 +45514,14 @@ ${suffix}` : base;
44715
45514
  actual_usd = _total_usd(actual);
44716
45515
  spent.usd += _total_usd(actual);
44717
45516
  if (budget.daily_limit_usd > 0 && !response.error) {
44718
- record_spend(_total_usd(actual), member.name, member.model, {
45517
+ record_spend(_total_usd(actual), effective.name, effective.model, {
44719
45518
  cache_read_input_tokens: response.cache_read_input_tokens,
44720
45519
  cache_creation_input_tokens: response.cache_creation_input_tokens,
44721
45520
  cache_ttl: "5m"
44722
45521
  });
44723
45522
  }
44724
45523
  }
44725
- _stamp_transport_metadata(response, member, actual_usd);
45524
+ _stamp_transport_metadata(response, effective, actual_usd);
44726
45525
  }
44727
45526
  return results;
44728
45527
  }
@@ -44902,6 +45701,9 @@ function run_debate(members, question, opts = {}) {
44902
45701
  );
44903
45702
  }
44904
45703
  const spent = { input: 0, output: 0, usd: 0 };
45704
+ const cli_fallback = opts.cli_fallback ?? null;
45705
+ const fallback_ledger = cli_fallback !== null ? new MidFlightFallback() : null;
45706
+ const fallback_twins = cli_fallback !== null ? /* @__PURE__ */ new Map() : null;
44905
45707
  const all_rounds = [];
44906
45708
  let current_suffix = "";
44907
45709
  let current_user_prompt = question.user_prompt;
@@ -44921,7 +45723,10 @@ ${question.user_prompt}`,
44921
45723
  on_overrun,
44922
45724
  project,
44923
45725
  original_ask,
44924
- advisor_plans
45726
+ advisor_plans,
45727
+ cli_fallback,
45728
+ fallback_ledger,
45729
+ fallback_twins
44925
45730
  });
44926
45731
  opts.on_restate?.(restated);
44927
45732
  }
@@ -44944,6 +45749,9 @@ ${question.user_prompt}`,
44944
45749
  project,
44945
45750
  original_ask,
44946
45751
  advisor_plans,
45752
+ cli_fallback,
45753
+ fallback_ledger,
45754
+ fallback_twins,
44947
45755
  split: current_suffix ? { stable: question.user_prompt, suffix: current_suffix } : null
44948
45756
  });
44949
45757
  }
@@ -44960,7 +45768,16 @@ ${question.user_prompt}`,
44960
45768
  }),
44961
45769
  budget,
44962
45770
  spent,
44963
- { table, on_overrun, project, original_ask, advisor_plans },
45771
+ {
45772
+ table,
45773
+ on_overrun,
45774
+ project,
45775
+ original_ask,
45776
+ advisor_plans,
45777
+ cli_fallback,
45778
+ fallback_ledger,
45779
+ fallback_twins
45780
+ },
44964
45781
  opts.on_repair ?? null
44965
45782
  );
44966
45783
  }
@@ -45209,27 +46026,6 @@ function run_consensus_scoring(members, deliberation_responses, opts = {}) {
45209
46026
  scoring_responses
45210
46027
  });
45211
46028
  }
45212
- function _render_response_meta(r) {
45213
- const meta_dict = r.metadata ?? {};
45214
- const billable = Boolean(_metaGet(meta_dict, "billable", true));
45215
- const estimated = Boolean(_metaGet(meta_dict, "tokens_estimated", false));
45216
- const parts = [];
45217
- if (!billable) {
45218
- const label = _metaGet(meta_dict, "subscription_label", null) || "flat-rate";
45219
- parts.push(`cost: subscription (${label})`);
45220
- } else {
45221
- const cost_usd = _metaGet(meta_dict, "cost_usd", void 0);
45222
- if (typeof cost_usd === "number" && !(typeof cost_usd === "boolean")) {
45223
- parts.push(`cost: $${_pyFixed3(cost_usd, 4)}`);
45224
- }
45225
- const prefix = estimated ? "~" : "";
45226
- parts.push(
45227
- `tokens: ${prefix}${r.input_tokens} in / ${prefix}${r.output_tokens} out`
45228
- );
45229
- }
45230
- parts.push(`${r.latency_ms} ms`);
45231
- return `*${parts.join(" \xB7 ")}*`;
45232
- }
45233
46029
  function _default_explain_confidence(mode) {
45234
46030
  if (mode === null) {
45235
46031
  return true;
@@ -45260,7 +46056,7 @@ function render7(responses, opts = {}) {
45260
46056
  *ERROR:* \`${r.error}\``);
45261
46057
  continue;
45262
46058
  }
45263
- const meta = _render_response_meta(r);
46059
+ const meta = renderResponseMeta(r, { pyFixed: _pyFixed3 });
45264
46060
  blocks.push(`${header}
45265
46061
 
45266
46062
  ${meta}
@@ -45452,43 +46248,7 @@ function _render_bucket(items, explain = true) {
45452
46248
  }
45453
46249
  return lines.join("\n");
45454
46250
  }
45455
- function _pyInt5(x) {
45456
- return Math.trunc(x);
45457
- }
45458
- function _pyReprInt(x) {
45459
- return String(x);
45460
- }
45461
- function _excTag(exc) {
45462
- if (exc instanceof Error) {
45463
- return `${exc.name}: ${exc.message}`;
45464
- }
45465
- return `Error: ${String(exc)}`;
45466
- }
45467
- function _setdefault(obj, key, value) {
45468
- if (!(key in obj)) {
45469
- obj[key] = value;
45470
- }
45471
- }
45472
- function _metaGet(obj, key, fallback) {
45473
- return key in obj ? obj[key] : fallback;
45474
- }
45475
- function _pyLStrip(s) {
45476
- return s.replace(/^\s+/, "");
45477
- }
45478
- function _pyRStrip2(s) {
45479
- return s.replace(/\s+$/, "");
45480
- }
45481
- function _pyLen5(s) {
45482
- let n = 0;
45483
- for (const _ of s) {
45484
- n += 1;
45485
- }
45486
- return n;
45487
- }
45488
- function _pySlice(s, start, end) {
45489
- return [...s].slice(start, end).join("");
45490
- }
45491
- var CostBudget, CouncilQuestion, OverrunEvent, DebateCostEstimate, DebateCheckpoint, DebateCapExceeded, PeerReviewResult, ConsensusResult, _DEFAULT_EXPLAIN_LENSES;
46251
+ var CouncilQuestion, DebateCostEstimate, DebateCheckpoint, DebateCapExceeded, PeerReviewResult, ConsensusResult, _DEFAULT_EXPLAIN_LENSES;
45492
46252
  var init_orchestrator = __esm({
45493
46253
  "src/scripts/ai_council/orchestrator.ts"() {
45494
46254
  "use strict";
@@ -45499,25 +46259,15 @@ var init_orchestrator = __esm({
45499
46259
  init_prompts2();
45500
46260
  init_debate_gates();
45501
46261
  init_stance_tally();
46262
+ init_transport_resolver();
46263
+ init_mid_flight_fallback();
46264
+ init_spend_gate();
46265
+ init_spend_gate();
46266
+ init_response_render();
46267
+ init_py_parity();
45502
46268
  init_blind_review();
45503
46269
  init_quorum();
45504
46270
  init_handoff();
45505
- CostBudget = class {
45506
- max_input_tokens;
45507
- max_output_tokens;
45508
- max_calls;
45509
- max_total_usd;
45510
- // 0 = USD ceiling disabled (token caps still apply)
45511
- daily_limit_usd;
45512
- // 0 = rolling 24h cap disabled (D3)
45513
- constructor(args = {}) {
45514
- this.max_input_tokens = args.max_input_tokens ?? 5e4;
45515
- this.max_output_tokens = args.max_output_tokens ?? 2e4;
45516
- this.max_calls = args.max_calls ?? 10;
45517
- this.max_total_usd = args.max_total_usd ?? 0;
45518
- this.daily_limit_usd = args.daily_limit_usd ?? 0;
45519
- }
45520
- };
45521
46271
  CouncilQuestion = class {
45522
46272
  mode;
45523
46273
  // one of: prompt, roadmap, diff, files
@@ -45530,36 +46280,6 @@ var init_orchestrator = __esm({
45530
46280
  this.max_tokens = args.max_tokens ?? DEFAULT_MAX_TOKENS;
45531
46281
  }
45532
46282
  };
45533
- OverrunEvent = class {
45534
- member_index;
45535
- member;
45536
- next_estimate;
45537
- // this member's projected cost
45538
- spent_input_tokens;
45539
- // already-billed totals BEFORE this member
45540
- spent_output_tokens;
45541
- spent_usd;
45542
- projected_total_usd;
45543
- // spent_usd + next_estimate.total_usd
45544
- daily_spent_usd;
45545
- // rolling 24h spend BEFORE this member (D3)
45546
- daily_limit_usd;
45547
- // the configured daily cap (0 = disabled)
45548
- breach_kind;
45549
- // "session" | "daily" | "tokens"
45550
- constructor(args) {
45551
- this.member_index = args.member_index;
45552
- this.member = args.member;
45553
- this.next_estimate = args.next_estimate;
45554
- this.spent_input_tokens = args.spent_input_tokens;
45555
- this.spent_output_tokens = args.spent_output_tokens;
45556
- this.spent_usd = args.spent_usd;
45557
- this.projected_total_usd = args.projected_total_usd;
45558
- this.daily_spent_usd = args.daily_spent_usd ?? 0;
45559
- this.daily_limit_usd = args.daily_limit_usd ?? 0;
45560
- this.breach_kind = args.breach_kind ?? "session";
45561
- }
45562
- };
45563
46283
  DebateCostEstimate = class {
45564
46284
  rounds;
45565
46285
  low_usd;
@@ -47091,7 +47811,7 @@ __export(council_cli_exports, {
47091
47811
  _parse_model_overrides: () => _parse_model_overrides,
47092
47812
  _parse_siblings_overrides: () => _parse_siblings_overrides,
47093
47813
  _postRunQuorum: () => _postRunQuorum,
47094
- _synthesize_ai_council_block: () => _synthesize_ai_council_block,
47814
+ _synthesize_ai_council_block: () => synthesizeAiCouncilBlock,
47095
47815
  build_members: () => build_members,
47096
47816
  build_question: () => build_question,
47097
47817
  cmd_debate: () => cmd_debate,
@@ -47318,131 +48038,10 @@ function load_settings(p = SETTINGS_FILE2, opts = {}) {
47318
48038
  const settings = load_agent_settings({ project_path: p });
47319
48039
  if (fs78.existsSync(ai_council_path)) {
47320
48040
  const cfg = load_council_config(ai_council_path);
47321
- settings["ai_council"] = _synthesize_ai_council_block(cfg);
48041
+ settings["ai_council"] = synthesizeAiCouncilBlock(cfg);
47322
48042
  }
47323
48043
  return settings;
47324
48044
  }
47325
- function _synthesize_ai_council_block(cfg) {
47326
- const members = {};
47327
- for (const [name, m] of cfg.members) {
47328
- const entry = { enabled: m.enabled, model: m.model };
47329
- if (m.api_key_ref !== null) {
47330
- entry["api_key_ref"] = m.api_key_ref;
47331
- }
47332
- if (m.mode !== null) {
47333
- entry["mode"] = m.mode;
47334
- }
47335
- if (m.binary !== null) {
47336
- entry["binary"] = m.binary;
47337
- }
47338
- if (m.model_ladder && m.model_ladder.length > 0) {
47339
- entry["model_ladder"] = [...m.model_ladder];
47340
- }
47341
- if (m.prompt_cache_ttl !== "5m") {
47342
- entry["prompt_cache_ttl"] = m.prompt_cache_ttl;
47343
- }
47344
- members[name] = entry;
47345
- }
47346
- const advisors = {};
47347
- for (const [name, a] of cfg.advisors) {
47348
- const entry = {
47349
- enabled: a.enabled,
47350
- member: a.member,
47351
- persona: a.persona
47352
- };
47353
- if (a.model !== null) {
47354
- entry["model"] = a.model;
47355
- }
47356
- advisors[name] = entry;
47357
- }
47358
- const lensCostDisclosure = {};
47359
- for (const [lens, cd] of cfg.lens_overrides.cost_disclosure) {
47360
- lensCostDisclosure[lens] = {
47361
- mode: cd.mode,
47362
- threshold_usd: cd.threshold_usd,
47363
- show_per_member: cd.show_per_member
47364
- };
47365
- }
47366
- const lensModelDowngrade = {};
47367
- for (const [lens, md] of cfg.lens_overrides.model_downgrade) {
47368
- lensModelDowngrade[lens] = { enabled: md.enabled, auto_apply: md.auto_apply };
47369
- }
47370
- return {
47371
- enabled: cfg.enabled,
47372
- mode: cfg.defaults.mode,
47373
- min_rounds: cfg.defaults.min_rounds,
47374
- deep_min_rounds: cfg.defaults.deep_min_rounds,
47375
- max_output_tokens: cfg.defaults.max_output_tokens,
47376
- session_retention_days: cfg.defaults.session_retention_days,
47377
- debate_max_rounds: cfg.defaults.debate_max_rounds,
47378
- cost_budget: {
47379
- max_input_tokens: cfg.cost_budget.max_input_tokens,
47380
- max_output_tokens: cfg.cost_budget.max_output_tokens,
47381
- max_calls: cfg.cost_budget.max_calls,
47382
- max_total_usd: cfg.cost_budget.max_total_usd,
47383
- daily_limit_usd: cfg.cost_budget.daily_limit_usd
47384
- },
47385
- consensus_scoring: {
47386
- enabled: cfg.consensus_scoring.enabled,
47387
- strong_threshold: cfg.consensus_scoring.strong_threshold,
47388
- minority_threshold: cfg.consensus_scoring.minority_threshold,
47389
- lenses: [...cfg.consensus_scoring.lenses]
47390
- },
47391
- cli_call_budget: {
47392
- max_calls_per_day: _mapToObject(cfg.cli_call_budget.max_calls_per_day),
47393
- warn_at: cfg.cli_call_budget.warn_at
47394
- },
47395
- // Phase 3.3 (road-to-always-on-orchestration): was validated by
47396
- // `_build_config`/`_build_quorum` but never forwarded into the
47397
- // synthesized block, so `build_members` always saw `undefined` and
47398
- // silently fell back to `'majority'` regardless of the user's
47399
- // configured `quorum: <k>`. Additive key — callers that never read
47400
- // it observe no change.
47401
- quorum: cfg.quorum,
47402
- // Forwarded for the same reason `quorum` above had to be: a key that
47403
- // `_build_config` validates and this block drops is a key the loader
47404
- // enforces and the runtime never sees. That defect shipped once here
47405
- // already; repeating it for the shadow floor would silently pin every
47406
- // `floor_would_hold` to the default no matter what the operator set.
47407
- quorum_min_present: cfg.quorum_min_present,
47408
- necessity_classifier: {
47409
- enabled: cfg.necessity_classifier.enabled,
47410
- mode: cfg.necessity_classifier.mode,
47411
- user_explicit_mode: cfg.necessity_classifier.user_explicit_mode
47412
- },
47413
- model_downgrade: {
47414
- enabled: cfg.model_downgrade.enabled,
47415
- auto_apply: cfg.model_downgrade.auto_apply
47416
- },
47417
- debate: {
47418
- max_cost_usd: cfg.debate.max_cost_usd,
47419
- cost_disclosure: {
47420
- mode: cfg.debate.cost_disclosure.mode,
47421
- threshold_usd: cfg.debate.cost_disclosure.threshold_usd,
47422
- show_per_member: cfg.debate.cost_disclosure.show_per_member
47423
- }
47424
- },
47425
- lens_overrides: {
47426
- necessity_classifier_mode: _mapToObject(
47427
- cfg.lens_overrides.necessity_classifier_mode
47428
- ),
47429
- necessity_classifier_user_explicit_mode: _mapToObject(
47430
- cfg.lens_overrides.necessity_classifier_user_explicit_mode
47431
- ),
47432
- model_downgrade: lensModelDowngrade,
47433
- cost_disclosure: lensCostDisclosure
47434
- },
47435
- members,
47436
- advisors
47437
- };
47438
- }
47439
- function _mapToObject(m) {
47440
- const out = {};
47441
- for (const [k, v] of m) {
47442
- out[k] = v;
47443
- }
47444
- return out;
47445
- }
47446
48045
  function _member_api_key_present(cfg) {
47447
48046
  const ref = cfg["api_key_ref"];
47448
48047
  if (typeof ref !== "string" || ref.trim() === "") {
@@ -47463,6 +48062,7 @@ function build_members(settings, opts = {}) {
47463
48062
  const quorum_out = opts.quorum_out ?? null;
47464
48063
  const probe_store = opts.probe_store ?? null;
47465
48064
  const qualification_out = opts.qualification_out ?? null;
48065
+ const fallback_out = opts.fallback_out ?? null;
47466
48066
  const qualifications = [];
47467
48067
  const report2 = opts.environment_report ?? detectEnvironment();
47468
48068
  const ai = _isDict3(settings) ? settings["ai_council"] || {} : {};
@@ -47667,6 +48267,17 @@ function build_members(settings, opts = {}) {
47667
48267
  `no council member has \`enabled: true\` \u2014 enable at least one under members.* in ${COUNCIL_CONFIG_USER_GLOBAL_REL} (user-global root; never the project tree, ADR-104). \`agent-config council:status\` prints the resolved path.`
47668
48268
  );
47669
48269
  }
48270
+ if (fallback_out !== null) {
48271
+ const fallback_cfg = _isDict3(ai["fallback"]) ? ai["fallback"] : {};
48272
+ fallback_out.options = buildFallbackOptions({
48273
+ apiOnQuota: _pyBool2(fallback_cfg["api_on_quota"]),
48274
+ hasApiRung: (p) => _API_PROVIDERS.has(p),
48275
+ memberConfig: (p) => members_cfg[p] || {},
48276
+ modelOverride: (p) => overrides[p] ?? null,
48277
+ constructApi: _construct_api_member,
48278
+ emit: appendEvent
48279
+ });
48280
+ }
47670
48281
  return members;
47671
48282
  }
47672
48283
  function _setDifference(items, known) {
@@ -47968,7 +48579,7 @@ function _peer_review_cost_delta(ai_cfg, args, estimates, n_billable) {
47968
48579
  const extra_usd = estimates.reduce((acc, e) => acc + _total_usd2(e), 0);
47969
48580
  return [extra_calls, extra_usd];
47970
48581
  }
47971
- function _maybe_run_chairman(ai_cfg, question, members, responses, budget, table, project, args) {
48582
+ function _maybe_run_chairman(ai_cfg, question, members, responses, budget, table, project, args, cli_fallback = null) {
47972
48583
  const ch = (_isDict3(ai_cfg) ? ai_cfg["chairman"] : null) || {};
47973
48584
  const mode = typeof ch["mode"] === "string" ? ch["mode"] : "host";
47974
48585
  if (mode === "host") {
@@ -48037,7 +48648,8 @@ ${transcript}`;
48037
48648
  const out = consult([client], synthQ, budget, {
48038
48649
  table,
48039
48650
  project,
48040
- original_ask: args.original_ask
48651
+ original_ask: args.original_ask,
48652
+ cli_fallback
48041
48653
  });
48042
48654
  const r = out[0] ?? null;
48043
48655
  if (r === null || r.error !== null || r.text.trim() === "") {
@@ -48624,6 +49236,20 @@ function _resolvedTransportFor(name, member, report2) {
48624
49236
  binaryOverride: member.binary
48625
49237
  });
48626
49238
  }
49239
+ function _posture(name, member) {
49240
+ const t = _resolvedTransportFor(name, member);
49241
+ const keyless = {
49242
+ anthropic: () => Boolean(load_anthropic_key()),
49243
+ openai: () => Boolean(load_openai_key())
49244
+ };
49245
+ return fallbackPostureFor({
49246
+ transport: t.available ? t.transport : null,
49247
+ hasApiRung: _API_PROVIDERS.has(name),
49248
+ apiKeyRef: member.api_key_ref,
49249
+ refResolves: member.api_key_ref === null ? void 0 : _member_api_key_present({ api_key_ref: member.api_key_ref }),
49250
+ keylessResolves: keyless[name]
49251
+ });
49252
+ }
48627
49253
  function cmd_status(args, opts = {}) {
48628
49254
  const env = opts.env ?? process.env;
48629
49255
  const override = env[COUNCIL_CONFIG_ENV];
@@ -48670,6 +49296,12 @@ function cmd_status(args, opts = {}) {
48670
49296
  ];
48671
49297
  })
48672
49298
  ),
49299
+ fallback: {
49300
+ api_on_quota: cfg?.fallback.api_on_quota ?? false,
49301
+ posture: Object.fromEntries(
49302
+ enabledMembers.map(([n, m]) => [n, _posture(n, m)])
49303
+ )
49304
+ },
48673
49305
  ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
48674
49306
  qualification: qualificationJson(qualifications),
48675
49307
  qualified_members: countableSeats(qualifications),
@@ -48710,6 +49342,13 @@ function cmd_status(args, opts = {}) {
48710
49342
  const billing = t.available ? ` \xB7 ${t.billing}` : "";
48711
49343
  const detail = t.available ? t.transport : `unavailable \u2014 ${t.reason ?? "no usable transport"}`;
48712
49344
  _stdout2(` transport ${name}: ${detail}${billing}
49345
+ `);
49346
+ }
49347
+ for (const line of renderPostureLines(
49348
+ enabledMembers.map(([n, m]) => [n, _posture(n, m)]),
49349
+ cfg.fallback.api_on_quota
49350
+ )) {
49351
+ _stdout2(`${line}
48713
49352
  `);
48714
49353
  }
48715
49354
  for (const line of qualificationStatusLines(qualifications)) {
@@ -48799,6 +49438,7 @@ function cmd_run(args, opts = {}) {
48799
49438
  const explicit_overrides = _parse_model_overrides(_getattr3(args, "model", null));
48800
49439
  const skipped = [];
48801
49440
  const quorum_out = { result: null };
49441
+ const fallback_out = { options: null };
48802
49442
  if (members === null) {
48803
49443
  members = build_members(settings, {
48804
49444
  invocation_mode: args.mode_override,
@@ -48806,7 +49446,8 @@ function cmd_run(args, opts = {}) {
48806
49446
  siblings_overrides: _parse_siblings_overrides(_getattr3(args, "siblings", null)),
48807
49447
  skipped,
48808
49448
  quorum_out,
48809
- probe_store: readProbeStore(REPO_ROOT11)
49449
+ probe_store: readProbeStore(REPO_ROOT11),
49450
+ fallback_out
48810
49451
  });
48811
49452
  }
48812
49453
  const roster_before_dispatch = members.length;
@@ -48939,6 +49580,10 @@ function cmd_run(args, opts = {}) {
48939
49580
  stance_tally: stance_tally_on,
48940
49581
  member_prompt_suffix,
48941
49582
  no_project_context_members,
49583
+ // Mid-flight cli→api fallback (ai-council-config.md § failure-class-
49584
+ // gated). `null` when the caller injected `members` directly — a
49585
+ // pre-built roster carries no config to derive the factory from.
49586
+ cli_fallback: fallback_out.options,
48942
49587
  // Interactive one-line confirm (cmd_run has no --auto-continue); the
48943
49588
  // repaired-call cost is collected so cost_usd_actual stays honest.
48944
49589
  on_stance_repair: stance_tally_on ? (member) => _make_repair_confirm(false)(member, "missing stance line") : null,
@@ -48982,7 +49627,17 @@ function cmd_run(args, opts = {}) {
48982
49627
  { persona_labels }
48983
49628
  );
48984
49629
  const consensus = _maybe_run_consensus(ai_cfg, question, members, responses, budget, table, project, args);
48985
- const chairman = _maybe_run_chairman(ai_cfg, question, members, responses, budget, table, project, args);
49630
+ const chairman = _maybe_run_chairman(
49631
+ ai_cfg,
49632
+ question,
49633
+ members,
49634
+ responses,
49635
+ budget,
49636
+ table,
49637
+ project,
49638
+ args,
49639
+ fallback_out.options
49640
+ );
48986
49641
  const blind_chairman_on = args.blind_chairman === true;
48987
49642
  const chairman_fields_on = args.chairman_fields === true;
48988
49643
  let blind_review_map = null;
@@ -49176,6 +49831,7 @@ function cmd_debate(args, opts = {}) {
49176
49831
  const explicit_overrides = _parse_model_overrides(_getattr3(args, "model", null));
49177
49832
  const skipped = [];
49178
49833
  const quorum_out = { result: null };
49834
+ const fallback_out = { options: null };
49179
49835
  if (members === null) {
49180
49836
  members = build_members(settings, {
49181
49837
  invocation_mode: args.mode_override,
@@ -49184,7 +49840,8 @@ function cmd_debate(args, opts = {}) {
49184
49840
  skipped,
49185
49841
  quorum_out,
49186
49842
  probe_store: readProbeStore(REPO_ROOT11),
49187
- command: "debate"
49843
+ command: "debate",
49844
+ fallback_out
49188
49845
  });
49189
49846
  }
49190
49847
  if (table === null) {
@@ -49355,7 +50012,8 @@ ${blocks.join("\n\n")}
49355
50012
  restate: restate_on,
49356
50013
  on_restate: restate_on ? on_restate : null,
49357
50014
  advisor_plans,
49358
- seed_round_1: seed
50015
+ seed_round_1: seed,
50016
+ cli_fallback: fallback_out.options
49359
50017
  });
49360
50018
  } catch (exc) {
49361
50019
  if (exc instanceof DebateCapExceeded) {
@@ -49976,6 +50634,10 @@ var init_council_cli = __esm({
49976
50634
  init_qualification_wiring();
49977
50635
  init_probe_store();
49978
50636
  init_events_log();
50637
+ init_map_to_object();
50638
+ init_council_settings_block();
50639
+ init_council_fallback_posture();
50640
+ init_council_fallback_wiring();
49979
50641
  init_necessity();
49980
50642
  init_orchestrator();
49981
50643
  init_pricing();