@event4u/agent-config 14.1.0 → 14.4.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 (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +291 -181
  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 +3773 -1731
  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 +120 -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 +22 -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/skill_catalogue_series.ts +91 -0
  75. package/src/scripts/_lib/stdin.ts +35 -1
  76. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  77. package/src/scripts/_lib/unattended_guard.ts +371 -0
  78. package/src/scripts/ai_council/config.ts +27 -1
  79. package/src/scripts/ai_council/events_log.ts +12 -1
  80. package/src/scripts/ai_council/fallback_config.ts +114 -0
  81. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  82. package/src/scripts/ai_council/orchestrator.ts +251 -292
  83. package/src/scripts/ai_council/py_parity.ts +139 -0
  84. package/src/scripts/ai_council/response_render.ts +99 -0
  85. package/src/scripts/ai_council/spend_gate.ts +123 -0
  86. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  87. package/src/scripts/bench_hook_injection.ts +100 -6
  88. package/src/scripts/bench_hook_latency.ts +442 -16
  89. package/src/scripts/check_estate_count.ts +924 -0
  90. package/src/scripts/compile_hook_manifest.ts +67 -0
  91. package/src/scripts/council_cli.ts +100 -125
  92. package/src/scripts/decision_memo.ts +325 -0
  93. package/src/scripts/dispatch_r2_reviewer.ts +169 -15
  94. package/src/scripts/hook_manifest.json +1 -0
  95. package/src/scripts/hook_manifest.yaml +172 -25
  96. package/src/scripts/hooks/concern_registry.ts +6 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  98. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  99. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  100. package/src/scripts/hooks/hook_stdin.ts +111 -5
  101. package/src/scripts/hooks/injection_budget.ts +595 -0
  102. package/src/scripts/hooks/payload_stub.ts +446 -0
  103. package/src/scripts/hooks/run_continuation_hook.ts +627 -0
  104. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  105. package/src/scripts/hooks/skill_route_hook.ts +172 -11
  106. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  107. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  108. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  109. package/src/scripts/hooks_doctor.ts +16 -2
  110. package/src/scripts/install-hooks.sh +26 -0
  111. package/src/scripts/interruption_report.ts +330 -13
  112. package/src/scripts/lint_hook_manifest.ts +167 -0
  113. package/src/scripts/roadmap_progress_hook.ts +254 -46
  114. package/src/scripts/run_supervise.ts +623 -0
  115. package/src/scripts/session_register_hook.ts +108 -8
  116. package/src/scripts/sessions_cli.ts +18 -6
  117. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  118. package/src/server/io/yamlIO.ts +167 -3
  119. package/src/shared/settingsConsent.ts +49 -2
  120. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * compile_hook_manifest — writes `src/scripts/hook_manifest.json` from
4
+ * `src/scripts/hook_manifest.yaml`.
5
+ *
6
+ * WHY the compiled sibling exists: `dispatch_hook._load_yaml` runs on EVERY
7
+ * hook dispatch, and parsing the ~61 kB YAML manifest measured 12 ms plus 8 ms
8
+ * to load the `yaml` module — a fifth of a ~103 ms dispatch spent re-deriving a
9
+ * table that does not change between runs. The compiled form is the same data
10
+ * with comments stripped (~15 kB) and parses in under a millisecond. Measured
11
+ * effect on `pre_tool_use` p50, one machine, n=50: 103 ms → 81 ms.
12
+ *
13
+ * The `fingerprint` field is what makes the fast path SAFE rather than merely
14
+ * fast, and it is deliberately content-derived. The first version compared
15
+ * mtimes and that was a measured defect: on a fresh `actions/checkout` both
16
+ * files carry the checkout timestamp in whatever order git wrote them, so
17
+ * whether the optimisation applied was a coin flip — it won on a PR run
18
+ * (p95 129 ms) and lost on the trunk (p95 186 ms) for the same commit.
19
+ *
20
+ * The fingerprint helper is IMPORTED from the reader rather than reimplemented
21
+ * here, so the writer and the reader cannot drift apart.
22
+ *
23
+ * Run after editing the manifest:
24
+ * ./scripts-run src/scripts/compile_hook_manifest
25
+ *
26
+ * `tests/hooks/hook_manifest_compiled.test.ts` fails if the committed JSON
27
+ * stops matching the YAML, so a forgotten run is caught rather than shipped.
28
+ *
29
+ * Exit codes: 0 written (or already current) · 2 internal error.
30
+ */
31
+ import * as fs from 'node:fs';
32
+ import * as path from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ import { parse as parseYaml } from 'yaml';
35
+
36
+ import { _manifest_fingerprint } from './hooks/dispatch_hook.js';
37
+
38
+ const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
39
+ const YAML_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'hook_manifest.yaml');
40
+ const JSON_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'hook_manifest.json');
41
+
42
+ export function compile(yamlText: string): string {
43
+ return JSON.stringify({
44
+ fingerprint: _manifest_fingerprint(yamlText),
45
+ manifest: parseYaml(yamlText, { version: '1.1' }) as unknown,
46
+ });
47
+ }
48
+
49
+ export function main(): number {
50
+ const text = fs.readFileSync(YAML_PATH, 'utf-8');
51
+ const next = compile(text);
52
+ const current = fs.existsSync(JSON_PATH) ? fs.readFileSync(JSON_PATH, 'utf-8') : null;
53
+ if (current === next) {
54
+ process.stdout.write('compile_hook_manifest: already current\n');
55
+ return 0;
56
+ }
57
+ fs.writeFileSync(JSON_PATH, next);
58
+ process.stdout.write(
59
+ `compile_hook_manifest: wrote ${path.relative(REPO_ROOT, JSON_PATH)} ` +
60
+ `(${String(next.length)} bytes, fingerprint ${_manifest_fingerprint(text)})\n`,
61
+ );
62
+ return 0;
63
+ }
64
+
65
+ if (process.argv[1] !== undefined && fs.realpathSync(process.argv[1]).includes('compile_hook_manifest')) {
66
+ process.exit(main());
67
+ }
@@ -98,6 +98,14 @@ import {
98
98
  } from './ai_council/qualification_wiring.js';
99
99
  import { PROBE_STORE_RELPATH, readProbeStore, type ProbeStore } from './ai_council/probe_store.js';
100
100
  import { appendEvent, type QuorumCommand, type QuorumDispatch } from './ai_council/events_log.js';
101
+ import { _mapToObject } from './_lib/map_to_object.js';
102
+ import { synthesizeAiCouncilBlock as _synthesize_ai_council_block } from './_lib/council_settings_block.js';
103
+ import {
104
+ fallbackPostureFor,
105
+ renderPostureLines,
106
+ type FallbackPosture,
107
+ } from './_lib/council_fallback_posture.js';
108
+ import { buildFallbackOptions } from './_lib/council_fallback_wiring.js';
101
109
  import {
102
110
  type ClassificationResult,
103
111
  type SizeFitVerdict,
@@ -110,6 +118,7 @@ import type {
110
118
  DebateCheckpoint,
111
119
  RenderAbsentMember} from './ai_council/orchestrator.js';
112
120
  import {
121
+ type CliFallbackOptions,
113
122
  ConsensusResult,
114
123
  CostBudget,
115
124
  CouncilQuestion,
@@ -458,131 +467,7 @@ function load_settings(
458
467
  return settings;
459
468
  }
460
469
 
461
- function _synthesize_ai_council_block(cfg: CouncilConfig): Dict {
462
- const members: Dict = {};
463
- for (const [name, m] of cfg.members) {
464
- const entry: Dict = { enabled: m.enabled, model: m.model };
465
- if (m.api_key_ref !== null) {
466
- entry['api_key_ref'] = m.api_key_ref;
467
- }
468
- if (m.mode !== null) {
469
- entry['mode'] = m.mode;
470
- }
471
- if (m.binary !== null) {
472
- entry['binary'] = m.binary;
473
- }
474
- if (m.model_ladder && m.model_ladder.length > 0) {
475
- entry['model_ladder'] = [...m.model_ladder];
476
- }
477
- // road-to-cache-economy Phase 4: forward only an explicit override —
478
- // the '5m' default needs no key, keeping the synthesized block
479
- // byte-identical to pre-Phase-4 output when nobody opted in.
480
- if (m.prompt_cache_ttl !== '5m') {
481
- entry['prompt_cache_ttl'] = m.prompt_cache_ttl;
482
- }
483
- members[name] = entry;
484
- }
485
- const advisors: Dict = {};
486
- for (const [name, a] of cfg.advisors) {
487
- const entry: Dict = {
488
- enabled: a.enabled,
489
- member: a.member,
490
- persona: a.persona,
491
- };
492
- if (a.model !== null) {
493
- entry['model'] = a.model;
494
- }
495
- advisors[name] = entry;
496
- }
497
- const lensCostDisclosure: Dict = {};
498
- for (const [lens, cd] of cfg.lens_overrides.cost_disclosure) {
499
- lensCostDisclosure[lens] = {
500
- mode: cd.mode,
501
- threshold_usd: cd.threshold_usd,
502
- show_per_member: cd.show_per_member,
503
- };
504
- }
505
- const lensModelDowngrade: Dict = {};
506
- for (const [lens, md] of cfg.lens_overrides.model_downgrade) {
507
- lensModelDowngrade[lens] = { enabled: md.enabled, auto_apply: md.auto_apply };
508
- }
509
- return {
510
- enabled: cfg.enabled,
511
- mode: cfg.defaults.mode,
512
- min_rounds: cfg.defaults.min_rounds,
513
- deep_min_rounds: cfg.defaults.deep_min_rounds,
514
- max_output_tokens: cfg.defaults.max_output_tokens,
515
- session_retention_days: cfg.defaults.session_retention_days,
516
- debate_max_rounds: cfg.defaults.debate_max_rounds,
517
- cost_budget: {
518
- max_input_tokens: cfg.cost_budget.max_input_tokens,
519
- max_output_tokens: cfg.cost_budget.max_output_tokens,
520
- max_calls: cfg.cost_budget.max_calls,
521
- max_total_usd: cfg.cost_budget.max_total_usd,
522
- daily_limit_usd: cfg.cost_budget.daily_limit_usd,
523
- },
524
- consensus_scoring: {
525
- enabled: cfg.consensus_scoring.enabled,
526
- strong_threshold: cfg.consensus_scoring.strong_threshold,
527
- minority_threshold: cfg.consensus_scoring.minority_threshold,
528
- lenses: [...cfg.consensus_scoring.lenses],
529
- },
530
- cli_call_budget: {
531
- max_calls_per_day: _mapToObject(cfg.cli_call_budget.max_calls_per_day),
532
- warn_at: cfg.cli_call_budget.warn_at,
533
- },
534
- // Phase 3.3 (road-to-always-on-orchestration): was validated by
535
- // `_build_config`/`_build_quorum` but never forwarded into the
536
- // synthesized block, so `build_members` always saw `undefined` and
537
- // silently fell back to `'majority'` regardless of the user's
538
- // configured `quorum: <k>`. Additive key — callers that never read
539
- // it observe no change.
540
- quorum: cfg.quorum,
541
- // Forwarded for the same reason `quorum` above had to be: a key that
542
- // `_build_config` validates and this block drops is a key the loader
543
- // enforces and the runtime never sees. That defect shipped once here
544
- // already; repeating it for the shadow floor would silently pin every
545
- // `floor_would_hold` to the default no matter what the operator set.
546
- quorum_min_present: cfg.quorum_min_present,
547
- necessity_classifier: {
548
- enabled: cfg.necessity_classifier.enabled,
549
- mode: cfg.necessity_classifier.mode,
550
- user_explicit_mode: cfg.necessity_classifier.user_explicit_mode,
551
- },
552
- model_downgrade: {
553
- enabled: cfg.model_downgrade.enabled,
554
- auto_apply: cfg.model_downgrade.auto_apply,
555
- },
556
- debate: {
557
- max_cost_usd: cfg.debate.max_cost_usd,
558
- cost_disclosure: {
559
- mode: cfg.debate.cost_disclosure.mode,
560
- threshold_usd: cfg.debate.cost_disclosure.threshold_usd,
561
- show_per_member: cfg.debate.cost_disclosure.show_per_member,
562
- },
563
- },
564
- lens_overrides: {
565
- necessity_classifier_mode: _mapToObject(
566
- cfg.lens_overrides.necessity_classifier_mode,
567
- ),
568
- necessity_classifier_user_explicit_mode: _mapToObject(
569
- cfg.lens_overrides.necessity_classifier_user_explicit_mode,
570
- ),
571
- model_downgrade: lensModelDowngrade,
572
- cost_disclosure: lensCostDisclosure,
573
- },
574
- members,
575
- advisors,
576
- };
577
- }
578
470
 
579
- function _mapToObject<V>(m: ReadonlyMap<string, V>): Record<string, V> {
580
- const out: Record<string, V> = {};
581
- for (const [k, v] of m) {
582
- out[k] = v;
583
- }
584
- return out;
585
- }
586
471
 
587
472
  // ── member construction ─────────────────────────────────────────────
588
473
 
@@ -634,6 +519,12 @@ interface BuildMembersOptions {
634
519
  * caller that passed no store means "not evaluated" — NOT "all qualified".
635
520
  */
636
521
  qualification_out?: MemberQualification[] | null;
522
+ /**
523
+ * Out-param for the mid-flight fallback wiring (the `quorum_out`
524
+ * convention). Populated by `council_fallback_wiring.buildFallbackOptions`;
525
+ * stays `null` when the caller passes no ref.
526
+ */
527
+ fallback_out?: { options: CliFallbackOptions | null } | null;
637
528
  }
638
529
 
639
530
  /**
@@ -666,6 +557,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
666
557
  const quorum_out = opts.quorum_out ?? null;
667
558
  const probe_store = opts.probe_store ?? null;
668
559
  const qualification_out = opts.qualification_out ?? null;
560
+ const fallback_out = opts.fallback_out ?? null;
669
561
  const qualifications: MemberQualification[] = [];
670
562
  // Read-only, per-process-memoised (`detectEnvironment` caches the
671
563
  // no-argument call) — cheap to call once per `build_members` invocation.
@@ -987,6 +879,19 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
987
879
  'prints the resolved path.',
988
880
  );
989
881
  }
882
+ if (fallback_out !== null) {
883
+ // Absent/malformed block means `false`, never a throw. Why quota
884
+ // fall-through is opt-in: `FallbackPolicy.apiOnQuota`.
885
+ const fallback_cfg = _isDict(ai['fallback']) ? (ai['fallback'] as Dict) : {};
886
+ fallback_out.options = buildFallbackOptions({
887
+ apiOnQuota: _pyBool(fallback_cfg['api_on_quota']),
888
+ hasApiRung: (p) => _API_PROVIDERS.has(p),
889
+ memberConfig: (p) => ((members_cfg[p] as Dict) || {}) as Dict,
890
+ modelOverride: (p) => (overrides[p] as string | undefined) ?? null,
891
+ constructApi: _construct_api_member,
892
+ emit: appendEvent,
893
+ });
894
+ }
990
895
  return members;
991
896
  }
992
897
 
@@ -1447,6 +1352,7 @@ function _maybe_run_chairman(
1447
1352
  table: PriceTable,
1448
1353
  project: unknown,
1449
1354
  args: Args,
1355
+ cli_fallback: CliFallbackOptions | null = null,
1450
1356
  ): ChairmanResult | null {
1451
1357
  const ch = (_isDict(ai_cfg) ? (ai_cfg['chairman'] as Dict) : null) || {};
1452
1358
  const mode = typeof ch['mode'] === 'string' ? (ch['mode'] as string) : 'host';
@@ -1515,10 +1421,18 @@ function _maybe_run_chairman(
1515
1421
  user_prompt: synth_prompt,
1516
1422
  max_tokens: question.max_tokens,
1517
1423
  });
1424
+ // The synthesis falls back like any other billable pass: an individual
1425
+ // seat has N−1 redundancy behind it, this has none, so a transport failure
1426
+ // loses the artefact the whole pass exists for. The counter — a larger
1427
+ // chairman model whose twin may fail differently — is bounded by
1428
+ // `model_unservable` being eligible and by the retry's spend gate, and the
1429
+ // annotation below still reports FAILED rather than degrading silently.
1430
+ // `consult` builds its own ledger: this is a separate invocation.
1518
1431
  const out = consult([client], synthQ, budget, {
1519
1432
  table,
1520
1433
  project: project as never,
1521
1434
  original_ask: args.original_ask,
1435
+ cli_fallback,
1522
1436
  });
1523
1437
  const r = out[0] ?? null;
1524
1438
  if (r === null || r.error !== null || r.text.trim() === '') {
@@ -1725,6 +1639,12 @@ function _resolve_max_tokens(args: Args, ai_cfg: Dict): number {
1725
1639
 
1726
1640
  // ── subcommands ─────────────────────────────────────────────────────
1727
1641
 
1642
+ /**
1643
+ * Cost preview only — it prices members and never calls one, so there is no
1644
+ * mid-flight failure to fall back from. `fallback_out` here is a DECIDED
1645
+ * non-goal: two of the three `build_members` sites pass it, and without this
1646
+ * note the third re-opens on every callsite census.
1647
+ */
1728
1648
  function cmd_estimate(
1729
1649
  args: Args,
1730
1650
  opts: { settings?: Dict | null; members?: ExternalAIClient[] | null; table?: PriceTable | null } = {},
@@ -2289,6 +2209,28 @@ function _resolvedTransportFor(
2289
2209
  });
2290
2210
  }
2291
2211
 
2212
+ /** Gather what `fallbackPostureFor` needs; the decision itself is pure. */
2213
+ function _posture(
2214
+ name: string,
2215
+ member: { readonly binary: string | null; readonly api_key_ref: string | null },
2216
+ ): FallbackPosture {
2217
+ const t = _resolvedTransportFor(name, member);
2218
+ const keyless: Readonly<Record<string, () => boolean>> = {
2219
+ anthropic: () => Boolean(load_anthropic_key()),
2220
+ openai: () => Boolean(load_openai_key()),
2221
+ };
2222
+ return fallbackPostureFor({
2223
+ transport: t.available ? t.transport : null,
2224
+ hasApiRung: _API_PROVIDERS.has(name),
2225
+ apiKeyRef: member.api_key_ref,
2226
+ refResolves:
2227
+ member.api_key_ref === null
2228
+ ? undefined
2229
+ : _member_api_key_present({ api_key_ref: member.api_key_ref }),
2230
+ keylessResolves: keyless[name],
2231
+ });
2232
+ }
2233
+
2292
2234
  export function cmd_status(args: Args, opts: { env?: Record<string, string | undefined> } = {}): number {
2293
2235
  const env = opts.env ?? process.env;
2294
2236
  const override = env[COUNCIL_CONFIG_ENV];
@@ -2346,6 +2288,12 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
2346
2288
  ];
2347
2289
  }),
2348
2290
  ),
2291
+ fallback: {
2292
+ api_on_quota: cfg?.fallback.api_on_quota ?? false,
2293
+ posture: Object.fromEntries(
2294
+ enabledMembers.map(([n, m]) => [n, _posture(n, m)]),
2295
+ ),
2296
+ },
2349
2297
  ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
2350
2298
  qualification: qualificationJson(qualifications),
2351
2299
  qualified_members: countableSeats(qualifications),
@@ -2384,6 +2332,12 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
2384
2332
  const detail = t.available ? t.transport : `unavailable — ${t.reason ?? 'no usable transport'}`;
2385
2333
  _stdout(` transport ${name}: ${detail}${billing}\n`);
2386
2334
  }
2335
+ for (const line of renderPostureLines(
2336
+ enabledMembers.map(([n, m]) => [n, _posture(n, m)] as const),
2337
+ cfg.fallback.api_on_quota,
2338
+ )) {
2339
+ _stdout(`${line}\n`);
2340
+ }
2387
2341
  // The four-value verdict per seat, then the advice — TWO warnings, so
2388
2342
  // an `unavailable` seat is not told to re-run (R2 finding 12).
2389
2343
  for (const line of qualificationStatusLines(qualifications)) {
@@ -2483,6 +2437,7 @@ function cmd_run(
2483
2437
  const explicit_overrides = _parse_model_overrides(_getattr<string[] | null>(args, 'model', null));
2484
2438
  const skipped: Dict[] = [];
2485
2439
  const quorum_out: { result: QuorumResult | null } = { result: null };
2440
+ const fallback_out: { options: CliFallbackOptions | null } = { options: null };
2486
2441
  if (members === null) {
2487
2442
  members = build_members(settings, {
2488
2443
  invocation_mode: args.mode_override,
@@ -2491,6 +2446,7 @@ function cmd_run(
2491
2446
  skipped,
2492
2447
  quorum_out,
2493
2448
  probe_store: readProbeStore(REPO_ROOT),
2449
+ fallback_out,
2494
2450
  });
2495
2451
  }
2496
2452
  // Measured, never assumed: `_apply_solo_dispatch` escalates back to the
@@ -2672,6 +2628,10 @@ function cmd_run(
2672
2628
  stance_tally: stance_tally_on,
2673
2629
  member_prompt_suffix,
2674
2630
  no_project_context_members,
2631
+ // Mid-flight cli→api fallback (ai-council-config.md § failure-class-
2632
+ // gated). `null` when the caller injected `members` directly — a
2633
+ // pre-built roster carries no config to derive the factory from.
2634
+ cli_fallback: fallback_out.options,
2675
2635
  // Interactive one-line confirm (cmd_run has no --auto-continue); the
2676
2636
  // repaired-call cost is collected so cost_usd_actual stays honest.
2677
2637
  on_stance_repair: stance_tally_on
@@ -2762,7 +2722,17 @@ function cmd_run(
2762
2722
  { persona_labels },
2763
2723
  );
2764
2724
  const consensus = _maybe_run_consensus(ai_cfg, question, members, responses, budget, table, project, args);
2765
- const chairman = _maybe_run_chairman(ai_cfg, question, members, responses, budget, table, project, args);
2725
+ const chairman = _maybe_run_chairman(
2726
+ ai_cfg,
2727
+ question,
2728
+ members,
2729
+ responses,
2730
+ budget,
2731
+ table,
2732
+ project,
2733
+ args,
2734
+ fallback_out.options,
2735
+ );
2766
2736
  // road-to-council-blind-review Phase 1 (Ü1), host-path only: when no
2767
2737
  // member chairman ran (mode === 'host'), the blind mapping is computed
2768
2738
  // here (seeded from the question text) and persisted for a later
@@ -3018,6 +2988,9 @@ function cmd_debate(
3018
2988
  // gap is declared in quorum-attendance-budget.json rather than left for a
3019
2989
  // reader to discover from a missing row.
3020
2990
  const quorum_out: { result: QuorumResult | null } = { result: null };
2991
+ // `null` when `build_members` is never called (injected members in
2992
+ // tests) — `run_debate` then behaves byte-identically to before.
2993
+ const fallback_out: { options: CliFallbackOptions | null } = { options: null };
3021
2994
  if (members === null) {
3022
2995
  members = build_members(settings, {
3023
2996
  invocation_mode: args.mode_override,
@@ -3027,6 +3000,7 @@ function cmd_debate(
3027
3000
  quorum_out,
3028
3001
  probe_store: readProbeStore(REPO_ROOT),
3029
3002
  command: 'debate',
3003
+ fallback_out,
3030
3004
  });
3031
3005
  }
3032
3006
  if (table === null) {
@@ -3225,6 +3199,7 @@ function cmd_debate(
3225
3199
  on_restate: restate_on ? on_restate : null,
3226
3200
  advisor_plans,
3227
3201
  seed_round_1: seed,
3202
+ cli_fallback: fallback_out.options,
3228
3203
  });
3229
3204
  } catch (exc) {
3230
3205
  if (exc instanceof DebateCapExceeded) {