@event4u/agent-config 9.28.0 → 9.29.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 (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/cost/report.md +2 -2
  7. package/dist/agent-src/commands/team/adversarial.md +8 -7
  8. package/dist/agent-src/commands/team/delegate.md +13 -12
  9. package/dist/agent-src/commands/team/review.md +9 -8
  10. package/dist/agent-src/commands/team/status.md +9 -8
  11. package/dist/agent-src/commands/team.md +29 -19
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  13. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  15. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
  16. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  18. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  19. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  20. package/dist/agent-src/rules/delegation-policy.md +11 -7
  21. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  22. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  23. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  24. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  25. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  26. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  27. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  28. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  29. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  30. package/dist/agent-src/templates/AGENTS.md +4 -4
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/roadmaps.md +23 -0
  33. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  34. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  35. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  36. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  37. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  38. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
  39. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
  40. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
  41. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  42. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  43. package/dist/cli-delegate/cmd_conformance.js +6 -43
  44. package/dist/cli-delegate/cmd_doctor.js +9 -5
  45. package/dist/cli-delegate/cmd_explain.js +2 -2
  46. package/dist/cli-delegate/cmd_export.js +4 -4
  47. package/dist/cli-delegate/cmd_handoff.js +3 -3
  48. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  49. package/dist/cli-delegate/cmd_migrate.js +2 -2
  50. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  51. package/dist/cli-delegate/cmd_prune.js +4 -4
  52. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  53. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_get.js +1 -7
  55. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  56. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  57. package/dist/cli-delegate/cmd_sync.js +4 -4
  58. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  59. package/dist/cli-delegate/cmd_update.js +1 -1
  60. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  61. package/dist/cli-delegate/cmd_validate.js +4 -4
  62. package/dist/cli-delegate/cmd_versions.js +1 -1
  63. package/dist/discovery/deprecation-report.md +1 -1
  64. package/dist/discovery/discovery-manifest.json +69 -20
  65. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  66. package/dist/discovery/discovery-manifest.summary.md +3 -2
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +39 -2
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +12 -4
  71. package/dist/hooks/dispatch.js +373 -35
  72. package/dist/install/install.mjs +42 -17
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +651 -262
  76. package/dist/router.json +1 -1
  77. package/dist/scripts/_lib/agent_settings.js +54 -0
  78. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  79. package/dist/scripts/condense.js +147 -2
  80. package/dist/scripts/condense.js.map +1 -1
  81. package/dist/server/schemas/settings.js +6 -7
  82. package/dist/server/schemas/settings.js.map +1 -1
  83. package/dist/shared/settingsCarveOut.js +0 -9
  84. package/dist/shared/settingsCarveOut.js.map +1 -1
  85. package/dist/shared/settingsClasses.js +11 -9
  86. package/dist/shared/settingsClasses.js.map +1 -1
  87. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  88. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  89. package/dist/ui/index.html +1 -1
  90. package/docs/contracts/settings-classes.md +39 -9
  91. package/package.json +1 -1
  92. package/src/agent-src/templates/AGENTS.md +4 -4
  93. package/src/config/agent-settings.template.yml +52 -66
  94. package/src/config/discovery/packs.yml +11 -0
  95. package/src/config/discovery/workspaces.yml +1 -1
  96. package/src/config/gate-violation-baselines.json +3 -3
  97. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  98. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  99. package/src/scripts/_lib/agent_settings.ts +58 -0
  100. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  101. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  102. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  103. package/src/scripts/_lib/host_capability.ts +63 -6
  104. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  105. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  106. package/src/scripts/_lib/orchestration_record.ts +37 -0
  107. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  108. package/src/scripts/_lib/self_repair.ts +160 -13
  109. package/src/scripts/_lib/self_repair_store.ts +28 -0
  110. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  111. package/src/scripts/_lib/subagent_response.ts +19 -0
  112. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  113. package/src/scripts/_lib/subagent_steering.ts +9 -4
  114. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  115. package/src/scripts/_lib/worker_budget.ts +37 -2
  116. package/src/scripts/ai_council/clients.ts +76 -54
  117. package/src/scripts/ai_council/config.ts +94 -6
  118. package/src/scripts/ai_council/handoff.ts +113 -0
  119. package/src/scripts/ai_council/modes.ts +19 -4
  120. package/src/scripts/ai_council/orchestrator.ts +105 -0
  121. package/src/scripts/ai_council/quorum.ts +84 -0
  122. package/src/scripts/ai_council/session.ts +44 -0
  123. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  124. package/src/scripts/ai_team/availability.ts +110 -0
  125. package/src/scripts/ai_team/config.ts +34 -14
  126. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  127. package/src/scripts/bench_critic_protocol.ts +396 -0
  128. package/src/scripts/check_detector_corpus.ts +202 -0
  129. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  130. package/src/scripts/condense.ts +162 -2
  131. package/src/scripts/council_cli.ts +401 -25
  132. package/src/scripts/forensics_report.ts +333 -0
  133. package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
  134. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  135. package/src/scripts/lint_never_silent.ts +276 -0
  136. package/src/scripts/lint_no_activation_gates.ts +271 -0
  137. package/src/scripts/orchestration_record.ts +8 -0
  138. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  139. package/src/scripts/release.ts +74 -2
  140. package/src/scripts/routing_doctor.ts +21 -36
  141. package/src/scripts/score_skill_selection.ts +19 -5
  142. package/src/scripts/self_repair_cli.ts +287 -57
  143. package/src/scripts/team_events_spike.ts +271 -0
  144. package/src/scripts/team_review_gate_hook.ts +31 -9
  145. package/src/server/schemas/settings.ts +10 -17
  146. package/src/shared/settingsCarveOut.ts +0 -10
  147. package/src/shared/settingsClasses.ts +11 -9
@@ -1052,9 +1052,12 @@ export class SubprocessError extends Error {
1052
1052
  * `~/.event4u/agent-config/cli-calls.json` and resets on UTC date rollover.
1053
1053
  *
1054
1054
  * Subclass contract:
1055
- * - `name`: provider key (`anthropic`, `openai`, `gemini`, …).
1056
- * - `default_binary`: executable name resolved via PATH when the member-level
1057
- * `binary:` field is not set.
1055
+ * - `super(opts, { name, default_binary })`: `name` is the provider key
1056
+ * (`anthropic`, `openai`, `gemini`, …); `default_binary` is the executable
1057
+ * resolved via PATH when the member-level `binary:` field is not set. Both
1058
+ * are passed explicitly to `super()` — not read off subclass instance
1059
+ * fields — because a subclass's own field initializers run only AFTER this
1060
+ * `super()` call returns.
1058
1061
  * - `_build_command(system_prompt, user_prompt, max_tokens)`: return the argv.
1059
1062
  * - `_parse_output(stdout, stderr)`: return a partial `CouncilResponse`.
1060
1063
  *
@@ -1097,8 +1100,22 @@ export abstract class CliClient extends ExternalAIClient {
1097
1100
  'usage limit',
1098
1101
  ];
1099
1102
 
1100
- constructor(opts: CliClientOptions & { model: string }) {
1103
+ /**
1104
+ * `identity` carries the subclass constants (`name`, `default_binary`)
1105
+ * that construction validates against. They are passed explicitly rather
1106
+ * than read off `this.name` / `this.default_binary` because subclass
1107
+ * instance-field initializers (`override default_binary = 'claude'`,
1108
+ * declared in the subclass body) run only AFTER this `super()` call
1109
+ * returns — reading them here, before that happens, would always see the
1110
+ * base class's own field defaults (`''`), not the subclass override.
1111
+ */
1112
+ constructor(
1113
+ opts: CliClientOptions & { model: string },
1114
+ identity: { name: string; default_binary: string },
1115
+ ) {
1101
1116
  super();
1117
+ this.name = identity.name;
1118
+ this.default_binary = identity.default_binary;
1102
1119
  this.model = opts.model;
1103
1120
  this.timeout_seconds = opts.timeout_seconds ?? DEFAULT_CLI_TIMEOUT_SECONDS;
1104
1121
  this.max_calls_per_day = opts.max_calls_per_day ?? null;
@@ -1353,19 +1370,20 @@ export abstract class CliClient extends ExternalAIClient {
1353
1370
  * collides with argv length limits.
1354
1371
  */
1355
1372
  export class AnthropicCliClient extends CliClient {
1356
- override name = 'anthropic';
1357
- override default_binary = 'claude';
1358
1373
  override subscription_label = 'claude-pro';
1359
1374
 
1360
1375
  constructor(opts: CliClientOptions = {}) {
1361
- super({
1362
- model: opts.model ?? DEFAULT_ANTHROPIC_CLI_MODEL,
1363
- binary: opts.binary,
1364
- timeout_seconds: opts.timeout_seconds,
1365
- max_calls_per_day: opts.max_calls_per_day,
1366
- warn_at: opts.warn_at,
1367
- cli_calls_path: opts.cli_calls_path,
1368
- });
1376
+ super(
1377
+ {
1378
+ model: opts.model ?? DEFAULT_ANTHROPIC_CLI_MODEL,
1379
+ binary: opts.binary,
1380
+ timeout_seconds: opts.timeout_seconds,
1381
+ max_calls_per_day: opts.max_calls_per_day,
1382
+ warn_at: opts.warn_at,
1383
+ cli_calls_path: opts.cli_calls_path,
1384
+ },
1385
+ { name: 'anthropic', default_binary: 'claude' },
1386
+ );
1369
1387
  }
1370
1388
 
1371
1389
  protected override _build_command(
@@ -1446,8 +1464,6 @@ export class AnthropicCliClient extends CliClient {
1446
1464
  * token usage in `usage.input_tokens` / `usage.output_tokens`.
1447
1465
  */
1448
1466
  export class OpenAICliClient extends CliClient {
1449
- override name = 'openai';
1450
- override default_binary = 'codex';
1451
1467
  override subscription_label = 'chatgpt-plus';
1452
1468
 
1453
1469
  static override _AUTH_FAILURE_PATTERNS: readonly string[] = [
@@ -1458,14 +1474,17 @@ export class OpenAICliClient extends CliClient {
1458
1474
  ];
1459
1475
 
1460
1476
  constructor(opts: CliClientOptions = {}) {
1461
- super({
1462
- model: opts.model ?? DEFAULT_OPENAI_CLI_MODEL,
1463
- binary: opts.binary,
1464
- timeout_seconds: opts.timeout_seconds,
1465
- max_calls_per_day: opts.max_calls_per_day,
1466
- warn_at: opts.warn_at,
1467
- cli_calls_path: opts.cli_calls_path,
1468
- });
1477
+ super(
1478
+ {
1479
+ model: opts.model ?? DEFAULT_OPENAI_CLI_MODEL,
1480
+ binary: opts.binary,
1481
+ timeout_seconds: opts.timeout_seconds,
1482
+ max_calls_per_day: opts.max_calls_per_day,
1483
+ warn_at: opts.warn_at,
1484
+ cli_calls_path: opts.cli_calls_path,
1485
+ },
1486
+ { name: 'openai', default_binary: 'codex' },
1487
+ );
1469
1488
  }
1470
1489
 
1471
1490
  protected override _build_command(
@@ -1569,8 +1588,6 @@ export class OpenAICliClient extends CliClient {
1569
1588
  * stdin to dodge argv limits.
1570
1589
  */
1571
1590
  export class GeminiCliClient extends CliClient {
1572
- override name = 'gemini';
1573
- override default_binary = 'gemini';
1574
1591
  override subscription_label = 'gemini-pro';
1575
1592
 
1576
1593
  static override _AUTH_FAILURE_PATTERNS: readonly string[] = [
@@ -1581,14 +1598,17 @@ export class GeminiCliClient extends CliClient {
1581
1598
  ];
1582
1599
 
1583
1600
  constructor(opts: CliClientOptions = {}) {
1584
- super({
1585
- model: opts.model ?? DEFAULT_GEMINI_CLI_MODEL,
1586
- binary: opts.binary,
1587
- timeout_seconds: opts.timeout_seconds,
1588
- max_calls_per_day: opts.max_calls_per_day,
1589
- warn_at: opts.warn_at,
1590
- cli_calls_path: opts.cli_calls_path,
1591
- });
1601
+ super(
1602
+ {
1603
+ model: opts.model ?? DEFAULT_GEMINI_CLI_MODEL,
1604
+ binary: opts.binary,
1605
+ timeout_seconds: opts.timeout_seconds,
1606
+ max_calls_per_day: opts.max_calls_per_day,
1607
+ warn_at: opts.warn_at,
1608
+ cli_calls_path: opts.cli_calls_path,
1609
+ },
1610
+ { name: 'gemini', default_binary: 'gemini' },
1611
+ );
1592
1612
  }
1593
1613
 
1594
1614
  protected override _build_command(
@@ -1686,8 +1706,6 @@ export class GeminiCliClient extends CliClient {
1686
1706
  * heuristically (chars / 4) for the audit-trail.
1687
1707
  */
1688
1708
  export class XAICliClient extends CliClient {
1689
- override name = 'xai';
1690
- override default_binary = 'grok';
1691
1709
  override billable = true; // community CLI consumes an API key — billable applies
1692
1710
 
1693
1711
  static override _AUTH_FAILURE_PATTERNS: readonly string[] = [
@@ -1698,14 +1716,17 @@ export class XAICliClient extends CliClient {
1698
1716
  ];
1699
1717
 
1700
1718
  constructor(opts: CliClientOptions = {}) {
1701
- super({
1702
- model: opts.model ?? DEFAULT_XAI_MODEL,
1703
- binary: opts.binary,
1704
- timeout_seconds: opts.timeout_seconds,
1705
- max_calls_per_day: opts.max_calls_per_day,
1706
- warn_at: opts.warn_at,
1707
- cli_calls_path: opts.cli_calls_path,
1708
- });
1719
+ super(
1720
+ {
1721
+ model: opts.model ?? DEFAULT_XAI_MODEL,
1722
+ binary: opts.binary,
1723
+ timeout_seconds: opts.timeout_seconds,
1724
+ max_calls_per_day: opts.max_calls_per_day,
1725
+ warn_at: opts.warn_at,
1726
+ cli_calls_path: opts.cli_calls_path,
1727
+ },
1728
+ { name: 'xai', default_binary: 'grok' },
1729
+ );
1709
1730
  }
1710
1731
 
1711
1732
  protected override _build_command(
@@ -1752,8 +1773,6 @@ export class XAICliClient extends CliClient {
1752
1773
  * Invokes `perplexity -p <prompt>`. Output is plain text — no JSON envelope.
1753
1774
  */
1754
1775
  export class PerplexityCliClient extends CliClient {
1755
- override name = 'perplexity';
1756
- override default_binary = 'perplexity';
1757
1776
  override billable = true; // community CLI consumes an API key — billable applies
1758
1777
 
1759
1778
  static override _AUTH_FAILURE_PATTERNS: readonly string[] = [
@@ -1764,14 +1783,17 @@ export class PerplexityCliClient extends CliClient {
1764
1783
  ];
1765
1784
 
1766
1785
  constructor(opts: CliClientOptions = {}) {
1767
- super({
1768
- model: opts.model ?? DEFAULT_PERPLEXITY_MODEL,
1769
- binary: opts.binary,
1770
- timeout_seconds: opts.timeout_seconds,
1771
- max_calls_per_day: opts.max_calls_per_day,
1772
- warn_at: opts.warn_at,
1773
- cli_calls_path: opts.cli_calls_path,
1774
- });
1786
+ super(
1787
+ {
1788
+ model: opts.model ?? DEFAULT_PERPLEXITY_MODEL,
1789
+ binary: opts.binary,
1790
+ timeout_seconds: opts.timeout_seconds,
1791
+ max_calls_per_day: opts.max_calls_per_day,
1792
+ warn_at: opts.warn_at,
1793
+ cli_calls_path: opts.cli_calls_path,
1794
+ },
1795
+ { name: 'perplexity', default_binary: 'perplexity' },
1796
+ );
1775
1797
  }
1776
1798
 
1777
1799
  protected override _build_command(
@@ -21,9 +21,12 @@
21
21
  * a locally-installed CLI under subscription auth (free for
22
22
  * first-party CLIs, billable for community wrappers); `auto` = pick
23
23
  * per provider per invocation (cli → api → unavailable), resolved by
24
- * `transport_resolver.ts`. `auto` is opt-in — it is never the shipped
25
- * default, because flipping it would move an existing user's spend
26
- * from per-token dollars onto subscription quota with no config edit.
24
+ * `transport_resolver.ts`. `auto` is the SHIPPED DEFAULT
25
+ * (road-to-always-on-orchestration Phase 3.1 — CLI-first is the
26
+ * owner-set transport doctrine: ride the vendor CLI under subscription
27
+ * auth first, fall to the metered API only where no CLI resolves).
28
+ * `manual` and a pinned `api`/`cli` remain valid explicit per-member or
29
+ * per-invocation overrides — `auto` never displaces one.
27
30
  * 3. `members.<name>` keys are restricted to the known provider set.
28
31
  * 4. `cost_budget.*` numeric fields are >= 0.
29
32
  * 5. Enabled members carry a non-empty `model` and `api_key_ref` when
@@ -37,7 +40,18 @@
37
40
  * 8. `binary:` is only valid when the member's effective mode is `cli` or
38
41
  * `auto` (auto may resolve to the cli rung);
39
42
  * `cli_call_budget.max_calls_per_day.<provider>` keys must be valid
40
- * providers.
43
+ * providers. `max_calls_per_day` SHIPS POPULATED with a generous
44
+ * per-provider default (50/day, road-to-always-on-orchestration
45
+ * Phase 3.4) for every known provider — a guard against a silent
46
+ * always-on pass exhausting a subscription's plan quota, not a brake;
47
+ * an explicit per-provider entry overrides just that provider's default.
48
+ * 9. `quorum` ∈ {`"majority"`, a positive integer} (default `"majority"`,
49
+ * road-to-always-on-orchestration Phase 3.3). `"majority"` at n members
50
+ * resolves to `ceil(n / 2)` — a SIMPLE majority, deliberately NOT
51
+ * "more than half": at n=2 that is 1-of-2, because 2-of-2 turns any
52
+ * single absent member into a deadlocked release gate (council-verified
53
+ * 2026-08-09). See `quorum.ts` for the resolver a caller applies to its
54
+ * own present/total member counts.
41
55
  *
42
56
  * Parity notes (intentional, documented):
43
57
  * - YAML is parsed with `yaml` (npm) at `version: '1.1'`, matching
@@ -562,6 +576,15 @@ export interface CliCallBudgetConfig {
562
576
  readonly warn_at: number;
563
577
  }
564
578
 
579
+ /**
580
+ * `"majority"` (default) or a positive integer k — the number of members a
581
+ * pass needs to conclude (road-to-always-on-orchestration Phase 3.3). See
582
+ * `quorum.ts::resolveQuorumThreshold` for how a caller turns this into a
583
+ * concrete threshold against its own total-member count, and the module
584
+ * docstring rule 9 for why `"majority"` is `ceil(n / 2)`, not `> n / 2`.
585
+ */
586
+ export type QuorumSetting = 'majority' | number;
587
+
565
588
  export interface CouncilConfig {
566
589
  readonly enabled: boolean;
567
590
  readonly defaults: DefaultsConfig;
@@ -570,6 +593,7 @@ export interface CouncilConfig {
570
593
  readonly advisors: ReadonlyMap<string, AdvisorConfig>;
571
594
  readonly consensus_scoring: ConsensusScoringConfig;
572
595
  readonly cli_call_budget: CliCallBudgetConfig;
596
+ readonly quorum: QuorumSetting;
573
597
  readonly necessity_classifier: NecessityClassifierConfig;
574
598
  readonly model_downgrade: ModelDowngradeConfig;
575
599
  readonly debate: DebateConfig;
@@ -578,6 +602,7 @@ export interface CouncilConfig {
578
602
  readonly chairman: ChairmanConfig;
579
603
  readonly debate_gates: DebateGatesConfig;
580
604
  readonly restate: RestateConfig;
605
+ readonly critic_protocol: CriticProtocol;
581
606
  readonly decision_resolution: DecisionResolutionConfig;
582
607
  readonly routing: RoutingConfig;
583
608
  readonly low_impact: LowImpactConfig;
@@ -796,6 +821,7 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
796
821
  const cli_call_budget = _build_cli_call_budget(
797
822
  _asDict(_getOr(raw, 'cli_call_budget', {})),
798
823
  );
824
+ const quorum = _build_quorum(_get(raw, 'quorum', 'majority'));
799
825
  const necessity_classifier = _build_necessity_classifier(
800
826
  _asDict(_getOr(raw, 'necessity_classifier', {})),
801
827
  );
@@ -821,6 +847,7 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
821
847
  'debate_gates',
822
848
  );
823
849
  const restate = _build_restate(_asDict(_getOr(raw, 'restate', {})), 'restate');
850
+ const critic_protocol = _build_critic_protocol(_getOr(raw, 'critic_protocol', 'legacy'));
824
851
  const decision_resolution = _build_decision_resolution(
825
852
  _asDict(_getOr(raw, 'decision_resolution', {})),
826
853
  );
@@ -843,6 +870,7 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
843
870
  advisors,
844
871
  consensus_scoring: consensus,
845
872
  cli_call_budget,
873
+ quorum,
846
874
  necessity_classifier,
847
875
  model_downgrade,
848
876
  debate,
@@ -851,6 +879,7 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
851
879
  chairman,
852
880
  debate_gates,
853
881
  restate,
882
+ critic_protocol,
854
883
  decision_resolution,
855
884
  routing,
856
885
  low_impact,
@@ -1115,6 +1144,27 @@ function _build_restate(d: Dict, scope: string): RestateConfig {
1115
1144
  return { enabled: Boolean(enabled) };
1116
1145
  }
1117
1146
 
1147
+ /**
1148
+ * `critic_protocol` (road-to-judgment-and-forensic-evidence Phase 2) — which
1149
+ * critic posture adversarial/skeptic review passes use. `legacy` is the
1150
+ * shipped free-hunt skeptic; `load_bearing` is the fixed protocol that must
1151
+ * name the single load-bearing assumption and may return "holds". Default
1152
+ * `legacy` regardless of the A/B outcome (promotion is a separate decision).
1153
+ */
1154
+ export type CriticProtocol = 'legacy' | 'load_bearing';
1155
+
1156
+ const _VALID_CRITIC_PROTOCOLS: ReadonlySet<string> = new Set(['legacy', 'load_bearing']);
1157
+
1158
+ function _build_critic_protocol(v: unknown): CriticProtocol {
1159
+ if (v === undefined || v === null) return 'legacy';
1160
+ if (!(_isStr(v) && _VALID_CRITIC_PROTOCOLS.has(v))) {
1161
+ throw new CouncilConfigError(
1162
+ `\`critic_protocol\`=${_pyRepr(v)} not in ${_sortedListRepr(_VALID_CRITIC_PROTOCOLS)}.`,
1163
+ );
1164
+ }
1165
+ return v as CriticProtocol;
1166
+ }
1167
+
1118
1168
  const _VALID_RESOLUTION_MODES: ReadonlySet<string> = new Set([
1119
1169
  'agent',
1120
1170
  'council',
@@ -1427,7 +1477,11 @@ function _build_defaults(d: Dict): DefaultsConfig {
1427
1477
  if (!_isDict(d)) {
1428
1478
  throw new CouncilConfigError('`defaults` must be a mapping.');
1429
1479
  }
1430
- const mode = _get(d, 'mode', 'api');
1480
+ // road-to-always-on-orchestration Phase 3.1: CLI-first is the shipped
1481
+ // default (was `api`) — see the module docstring rule 2. `manual` and a
1482
+ // pinned `api`/`cli` remain valid overrides at every layer resolve_mode
1483
+ // checks; this only changes what a config that never sets `mode` gets.
1484
+ const mode = _get(d, 'mode', 'auto');
1431
1485
  if (!(_isStr(mode) && _VALID_MODES.has(mode))) {
1432
1486
  throw new CouncilConfigError(
1433
1487
  `defaults.mode=${_pyRepr(mode)} not in ${_sortedListRepr(_VALID_MODES)}.`,
@@ -1630,7 +1684,13 @@ function _build_cost_budget(d: Dict): CostBudgetConfig {
1630
1684
  function _build_member(
1631
1685
  name: string,
1632
1686
  cfg: Dict,
1633
- default_mode = 'api',
1687
+ // road-to-always-on-orchestration Phase 3.1: kept in sync with
1688
+ // `_build_defaults`'s `auto` default for consistency. The sole call site
1689
+ // (`_build_config`) always passes `defaults.mode` explicitly, so this
1690
+ // parameter default is unreachable on the live path today; it exists so
1691
+ // a future caller that omits the third argument observes the same
1692
+ // doctrine rather than silently reverting to the retired `api` default.
1693
+ default_mode = 'auto',
1634
1694
  ): MemberConfig {
1635
1695
  if (!_VALID_PROVIDERS.has(name)) {
1636
1696
  throw new CouncilConfigError(
@@ -1775,6 +1835,18 @@ function _build_member(
1775
1835
  };
1776
1836
  }
1777
1837
 
1838
+ /**
1839
+ * Generous per-provider guard for `mode: cli` / `mode: auto` members
1840
+ * (road-to-always-on-orchestration Phase 3.4). Before this default the map
1841
+ * shipped empty and an unlisted provider ran uncapped — a plan-quota guard
1842
+ * that only existed if the user remembered to opt in. Every known provider
1843
+ * now carries this floor unless a config entry overrides it; sized as a
1844
+ * GUARD against silent quota exhaustion on an always-on pass, not a brake on
1845
+ * normal use (see the template's own worked-example sizing for the
1846
+ * reasoning behind a TIGHTER per-provider number).
1847
+ */
1848
+ const _DEFAULT_CLI_CALLS_PER_DAY = 50;
1849
+
1778
1850
  function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
1779
1851
  if (!_isDict(d)) {
1780
1852
  throw new CouncilConfigError('`cli_call_budget` must be a mapping.');
@@ -1786,6 +1858,9 @@ function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
1786
1858
  );
1787
1859
  }
1788
1860
  const caps = new Map<string, number>();
1861
+ for (const provider of _VALID_PROVIDERS) {
1862
+ caps.set(provider, _DEFAULT_CLI_CALLS_PER_DAY);
1863
+ }
1789
1864
  for (const [provider, value] of Object.entries(raw_caps)) {
1790
1865
  if (!_VALID_PROVIDERS.has(provider)) {
1791
1866
  throw new CouncilConfigError(
@@ -1817,6 +1892,19 @@ function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
1817
1892
  return { max_calls_per_day: caps, warn_at };
1818
1893
  }
1819
1894
 
1895
+ function _build_quorum(raw: Json): QuorumSetting {
1896
+ if (raw === 'majority') {
1897
+ return 'majority';
1898
+ }
1899
+ if (_isInt(raw) && !_isBool(raw) && (raw as number) >= 1) {
1900
+ return raw as number;
1901
+ }
1902
+ throw new CouncilConfigError(
1903
+ `\`quorum\`=${_pyRepr(raw)} must be 'majority' or an integer >= 1 ` +
1904
+ `(got ${_pyTypeName(raw)}).`,
1905
+ );
1906
+ }
1907
+
1820
1908
  function _build_advisor(name: string, cfg: Dict): AdvisorConfig {
1821
1909
  if (!_isDict(cfg)) {
1822
1910
  throw new CouncilConfigError(`advisors.${name}: must be a mapping.`);
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Verdict → handoff envelope (Phase 4.1 of road-to-always-on-orchestration).
3
+ *
4
+ * A council pass produces a verdict a human currently has to read and
5
+ * re-type into whatever executes next. This module is the machine-readable
6
+ * bridge: an additive `HandoffEnvelope` — decision, rejected alternatives
7
+ * with reasons, binding constraints — injectable as the dispatched work
8
+ * order of an implementing subagent, so deliberation output becomes
9
+ * execution input without a human transcription step.
10
+ *
11
+ * ## The honesty constraint
12
+ *
13
+ * A council pass has exactly ONE place in this codebase where "the
14
+ * decision" is a discrete, machine-parsed value rather than free-text prose
15
+ * a human (or an NLP guess) would have to interpret: the option-level
16
+ * stance tally (`stance_tally.ts`, opt-in via `ai_council.stance_tally`).
17
+ * Every other synthesis path — the default template, a chairman's prose
18
+ * verdict, consensus scoring's qualitative Strong/Findings/Minority
19
+ * buckets — has no comparable discrete "decision" field to extract from
20
+ * without parsing prose, which is exactly the kind of confident-looking
21
+ * fabrication this module refuses to do.
22
+ *
23
+ * So: `buildHandoffFromStanceTally` is the ONLY builder this module ships.
24
+ * No tally (the common case — the flag defaults off) or a tally that split
25
+ * (no option cleared the consensus threshold) → every field is `null`,
26
+ * never a guessed decision. A future builder over a different structured
27
+ * source (a chairman synthesis that itself emits structured fields, a
28
+ * consensus-scoring extension) is additive — it does not change this one.
29
+ *
30
+ * @see docs/contracts/ai-council-config.md § Handoff envelope
31
+ */
32
+
33
+ import type { StanceTallyResult } from './stance_tally.js';
34
+
35
+ /** One option the tally counted but did not conclude on. */
36
+ export interface RejectedAlternative {
37
+ readonly option: string;
38
+ /**
39
+ * A factual, derived-from-the-tally sentence — never invented prose.
40
+ * States the option's own backer count and weight against the
41
+ * threshold the winner had to clear, nothing else.
42
+ */
43
+ readonly reason: string;
44
+ }
45
+
46
+ /**
47
+ * The machine-readable work order a council verdict hands to whatever
48
+ * executes next. Every field is independently nullable: `null` means "no
49
+ * structured source existed to populate this field this pass", never a
50
+ * fabricated placeholder. A consumer checks for `null`, not for an empty
51
+ * array — an empty array would claim "checked, found none", which this
52
+ * module never asserts when it never actually checked.
53
+ */
54
+ export interface HandoffEnvelope {
55
+ readonly decision: string | null;
56
+ readonly rejected_alternatives: readonly RejectedAlternative[] | null;
57
+ /**
58
+ * Binding constraints the decision carries. No structured source for
59
+ * this exists anywhere in the current synthesis pipeline (a
60
+ * dealbreaker count describes objection PRESSURE on an option, not a
61
+ * named constraint on the winner) — always `null` until one does.
62
+ */
63
+ readonly constraints: readonly string[] | null;
64
+ }
65
+
66
+ /** An envelope with every field `null` — the honest "nothing to hand off" shape. */
67
+ export const EMPTY_HANDOFF: HandoffEnvelope = {
68
+ decision: null,
69
+ rejected_alternatives: null,
70
+ constraints: null,
71
+ };
72
+
73
+ /** True when every field is `null` — nothing worth rendering or dispatching. */
74
+ export function isEmptyHandoff(h: HandoffEnvelope): boolean {
75
+ return h.decision === null && h.rejected_alternatives === null && h.constraints === null;
76
+ }
77
+
78
+ /**
79
+ * Build a `HandoffEnvelope` from a stance tally, or the honest empty
80
+ * envelope when there is nothing structured to extract:
81
+ *
82
+ * - `tally` is `null` — stance tally never ran this pass (the default,
83
+ * opt-in-off case).
84
+ * - `tally.consensus` is `null` — the tally ran but split; no option
85
+ * cleared the two-thirds threshold, so there is no decision to report,
86
+ * only a split (which is quorum's/the render's job to surface, not this
87
+ * module's).
88
+ *
89
+ * Otherwise: `decision` is the winning option's display label;
90
+ * `rejected_alternatives` lists every OTHER non-abstain option the tally
91
+ * counted, each with a reason sentence built strictly from that option's
92
+ * own tally numbers (backer count, weight, the threshold the winner
93
+ * cleared) — never a narrative guess at WHY members backed it.
94
+ */
95
+ export function buildHandoffFromStanceTally(tally: StanceTallyResult | null): HandoffEnvelope {
96
+ if (tally === null || tally.consensus === null) {
97
+ return EMPTY_HANDOFF;
98
+ }
99
+ const winner = tally.consensus;
100
+ const rejected: RejectedAlternative[] = tally.options
101
+ .filter((o) => o !== winner)
102
+ .map((o) => ({
103
+ option: o.label,
104
+ reason:
105
+ `backed by ${o.backers.length} member(s), weight ${o.weight.toFixed(2)} of ` +
106
+ `${tally.threshold.toFixed(2)} needed to conclude`,
107
+ }));
108
+ return {
109
+ decision: winner.label,
110
+ rejected_alternatives: rejected.length > 0 ? rejected : null,
111
+ constraints: null,
112
+ };
113
+ }
@@ -33,21 +33,36 @@
33
33
  * conflated them. They are now named separately:
34
34
  *
35
35
  * - **Loader default** — when a config file omits `defaults.mode` entirely,
36
- * `config.ts::_build_defaults` fills `api`. Unchanged; this is what every
37
- * real config observes, because the loader always populates the key.
36
+ * `config.ts::_build_defaults` fills `auto` (road-to-always-on-orchestration
37
+ * Phase 3.1 — CLI-first is now the owner-set transport doctrine; before this
38
+ * flip the loader default was `api`). This is what every real config
39
+ * observes, because the loader always populates the key.
38
40
  * - **Built-in fallback** — when NO layer supplies a mode at all (a settings
39
41
  * dict handed straight to the resolver, no config file involved), the answer
40
42
  * is `manual`, below. That is the fail-closed direction the no-silent-spend
41
43
  * Iron Law requires: a caller who supplied no transport preference has not
42
44
  * asked to spend money, so the free transport wins.
45
+ *
46
+ * ### Why `DEFAULT_MODE` did NOT flip to `auto` alongside the loader default
47
+ *
48
+ * `auto` still resolves to a paying rung (the api fallback) when no CLI is
49
+ * usable — it is a routing rule, not a free transport. `DEFAULT_MODE` is only
50
+ * reached by a caller that bypasses the config loader entirely (no
51
+ * `.ai-council.yml`, a hand-built settings dict handed straight to
52
+ * `resolve_mode`/`resolve_modes`). Flipping it to `auto` would let such a
53
+ * caller spend money on a config that never asked for `auto` in the first
54
+ * place — the same no-silent-spend violation the built-in fallback exists to
55
+ * prevent. Every real, file-backed config always has `defaults.mode`
56
+ * populated by the loader (now `auto`), so `DEFAULT_MODE` is never consulted
57
+ * on that path — verified in modes.test.ts and config.test.ts.
43
58
  */
44
59
 
45
60
  export const VALID_MODES: ReadonlySet<string> = new Set(['api', 'manual', 'cli', 'auto']);
46
61
 
47
62
  /**
48
63
  * Built-in fallback when no layer supplies a mode. `manual` — free, no key,
49
- * no spend. NOT the same as the loader's `defaults.mode` default (`api`); see
50
- * the module header.
64
+ * no spend. NOT the same as the loader's `defaults.mode` default (`auto`
65
+ * since road-to-always-on-orchestration Phase 3.1); see the module header.
51
66
  */
52
67
  export const DEFAULT_MODE = 'manual';
53
68