@event4u/agent-config 9.27.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +38 -10
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -18,6 +18,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
18
18
|
|
|
19
19
|
import { project_settings_path, resolve_project_root } from './_lib/agent_settings.js';
|
|
20
20
|
import { load_agent_settings } from './_lib/agent_settings.js';
|
|
21
|
+
import { detectEnvironment, type EnvironmentReport } from './_lib/environment_detector.js';
|
|
21
22
|
|
|
22
23
|
import {
|
|
23
24
|
BundleTooLarge,
|
|
@@ -63,13 +64,19 @@ import { format_install_hints } from './ai_council/cli_hints.js';
|
|
|
63
64
|
import {
|
|
64
65
|
type AdvisorConfig,
|
|
65
66
|
type CouncilConfig,
|
|
67
|
+
type QuorumSetting,
|
|
68
|
+
COUNCIL_CONFIG_ENV,
|
|
69
|
+
COUNCIL_CONFIG_USER_GLOBAL_REL,
|
|
66
70
|
CouncilConfigError,
|
|
67
71
|
load_council_config,
|
|
68
72
|
resolve_api_key,
|
|
69
73
|
resolve_config_path,
|
|
70
74
|
} from './ai_council/config.js';
|
|
71
75
|
import { AuthCache, select_solo_member } from './ai_council/solo_dispatch.js';
|
|
72
|
-
import { InvalidModeError, resolve_global_mode
|
|
76
|
+
import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
|
|
77
|
+
import { resolveMemberTransport } from './ai_council/transport_resolver.js';
|
|
78
|
+
import { absentReasonFromCliFailure, classifyCliFailure, type AbsentReason } from './ai_council/transport_resolver.js';
|
|
79
|
+
import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
|
|
73
80
|
import { appendEvent } from './ai_council/events_log.js';
|
|
74
81
|
import {
|
|
75
82
|
type ClassificationResult,
|
|
@@ -80,7 +87,8 @@ import {
|
|
|
80
87
|
educate_message,
|
|
81
88
|
} from './ai_council/necessity.js';
|
|
82
89
|
import type {
|
|
83
|
-
DebateCheckpoint
|
|
90
|
+
DebateCheckpoint,
|
|
91
|
+
RenderAbsentMember} from './ai_council/orchestrator.js';
|
|
84
92
|
import {
|
|
85
93
|
ConsensusResult,
|
|
86
94
|
CostBudget,
|
|
@@ -126,6 +134,8 @@ import {
|
|
|
126
134
|
render_deanonymization_block,
|
|
127
135
|
with_chairman_fields,
|
|
128
136
|
} from './ai_council/blind_review.js';
|
|
137
|
+
import { tally_stances } from './ai_council/stance_tally.js';
|
|
138
|
+
import { buildHandoffFromStanceTally, type HandoffEnvelope } from './ai_council/handoff.js';
|
|
129
139
|
|
|
130
140
|
// ── argparse-style exit plumbing ────────────────────────────────────
|
|
131
141
|
// Mirror CPython argparse: a `prog: error: …` line on stderr + exit 2.
|
|
@@ -501,6 +511,13 @@ function _synthesize_ai_council_block(cfg: CouncilConfig): Dict {
|
|
|
501
511
|
max_calls_per_day: _mapToObject(cfg.cli_call_budget.max_calls_per_day),
|
|
502
512
|
warn_at: cfg.cli_call_budget.warn_at,
|
|
503
513
|
},
|
|
514
|
+
// Phase 3.3 (road-to-always-on-orchestration): was validated by
|
|
515
|
+
// `_build_config`/`_build_quorum` but never forwarded into the
|
|
516
|
+
// synthesized block, so `build_members` always saw `undefined` and
|
|
517
|
+
// silently fell back to `'majority'` regardless of the user's
|
|
518
|
+
// configured `quorum: <k>`. Additive key — callers that never read
|
|
519
|
+
// it observe no change.
|
|
520
|
+
quorum: cfg.quorum,
|
|
504
521
|
necessity_classifier: {
|
|
505
522
|
enabled: cfg.necessity_classifier.enabled,
|
|
506
523
|
mode: cfg.necessity_classifier.mode,
|
|
@@ -548,6 +565,118 @@ interface BuildMembersOptions {
|
|
|
548
565
|
model_overrides?: Record<string, string> | null;
|
|
549
566
|
siblings_overrides?: Record<string, string[]> | null;
|
|
550
567
|
skipped?: Dict[] | null;
|
|
568
|
+
/**
|
|
569
|
+
* Out-param (road-to-always-on-orchestration Phase 3.3): when supplied,
|
|
570
|
+
* `.result` is populated with this call's quorum verdict — enabled
|
|
571
|
+
* members vs. the count actually constructed (an "absent" push into
|
|
572
|
+
* `skipped` counts against it same as a hard construction failure).
|
|
573
|
+
* The object is mutated in place so a caller declares it once and reads
|
|
574
|
+
* it after `build_members` returns; `.result` stays `null` when the
|
|
575
|
+
* caller passes no ref, matching `SessionManifest.quorum`'s own
|
|
576
|
+
* "null = never evaluated" convention.
|
|
577
|
+
*/
|
|
578
|
+
quorum_out?: { result: QuorumResult | null } | null;
|
|
579
|
+
/**
|
|
580
|
+
* Test-only injection point, mirroring the `settings`/`members`/`table`
|
|
581
|
+
* DI pattern the CLI commands already use. Production callers never set
|
|
582
|
+
* this — `build_members` falls back to the real, process-memoised
|
|
583
|
+
* `detectEnvironment()` — but a fixed `EnvironmentReport` is what lets a
|
|
584
|
+
* unit test exercise the `auto` chain (present / absent / quorum)
|
|
585
|
+
* deterministically instead of poking real `$PATH` / `$HOME` / env-key
|
|
586
|
+
* state that differs per machine and per CI run.
|
|
587
|
+
*/
|
|
588
|
+
environment_report?: EnvironmentReport | null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The member's own `api_key_ref`, resolved directly — never the generic
|
|
593
|
+
* per-provider environment guess `resolveTransport`'s `auto` chain falls
|
|
594
|
+
* back to. A member with a non-default ref (`env:MY_CUSTOM_KEY`, a renamed
|
|
595
|
+
* key file) would otherwise read as keyless even when it is fully
|
|
596
|
+
* configured. Returns `undefined` when no ref is set at all, letting
|
|
597
|
+
* `resolveMemberTransport` fall through to its own generic detection.
|
|
598
|
+
*/
|
|
599
|
+
function _member_api_key_present(cfg: Dict): boolean | undefined {
|
|
600
|
+
const ref = cfg['api_key_ref'];
|
|
601
|
+
if (typeof ref !== 'string' || ref.trim() === '') {
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
resolve_api_key(ref);
|
|
606
|
+
return true;
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* `ai['quorum']` as forwarded by `_synthesize_ai_council_block` (already
|
|
614
|
+
* validated by `config.ts::_build_quorum` on that path) — or, for a caller
|
|
615
|
+
* that hands `build_members` a hand-built dict bypassing the loader
|
|
616
|
+
* entirely, a defensive re-check that fails soft to `'majority'` rather
|
|
617
|
+
* than throwing. `build_members` is not the config-validation layer;
|
|
618
|
+
* `evaluateQuorum` degrades an unrecognisable setting toward
|
|
619
|
+
* `inconclusive`, never toward a false `concluded`, so failing soft here
|
|
620
|
+
* costs nothing on the safety side.
|
|
621
|
+
*/
|
|
622
|
+
function _quorum_setting_from(ai: Dict): QuorumSetting {
|
|
623
|
+
const raw = ai['quorum'];
|
|
624
|
+
if (raw === 'majority') {
|
|
625
|
+
return 'majority';
|
|
626
|
+
}
|
|
627
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1) {
|
|
628
|
+
return raw;
|
|
629
|
+
}
|
|
630
|
+
return 'majority';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Re-derive quorum AFTER the provider calls, over what was actually USABLE —
|
|
635
|
+
* never what merely CONSTRUCTED (M3, independent-review finding). `members`
|
|
636
|
+
* and `responses` must be index-aligned (`consult()`'s own contract: one
|
|
637
|
+
* `CouncilResponse` per member, from the final round); `members.length` is
|
|
638
|
+
* the `n` — deliberately the roster that actually ran (post `--single` /
|
|
639
|
+
* `--siblings` filtering), never the full config roster a filtered run
|
|
640
|
+
* never attempted. A response carrying `.error` (or a missing response
|
|
641
|
+
* entry altogether) counts as absent, classified through the same
|
|
642
|
+
* `AbsentReason` vocabulary a static (pre-call) resolution failure uses, so
|
|
643
|
+
* one artefact buckets every absence — construction-time and mid-flight —
|
|
644
|
+
* under one taxonomy.
|
|
645
|
+
*/
|
|
646
|
+
function _postRunQuorum(
|
|
647
|
+
members: ExternalAIClient[],
|
|
648
|
+
responses: CouncilResponse[],
|
|
649
|
+
ai_cfg: Dict,
|
|
650
|
+
): { quorum: QuorumResult; absent: Dict[] } {
|
|
651
|
+
const absent: Dict[] = [];
|
|
652
|
+
let present = 0;
|
|
653
|
+
for (let i = 0; i < members.length; i++) {
|
|
654
|
+
const m = members[i] as ExternalAIClient;
|
|
655
|
+
const r = responses[i];
|
|
656
|
+
if (r !== undefined && !r.error) {
|
|
657
|
+
present += 1;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const raw = r?.error ?? 'no response';
|
|
661
|
+
const failure = classifyCliFailure(raw);
|
|
662
|
+
absent.push({
|
|
663
|
+
member: m.name,
|
|
664
|
+
reason: absentReasonFromCliFailure(failure) ?? 'unavailable',
|
|
665
|
+
detail: raw,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return { quorum: evaluateQuorum(members.length, present, _quorum_setting_from(ai_cfg)), absent };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* One-line k-of-n banner, mirrored from `orchestrator.ts::_render_quorum_line`
|
|
673
|
+
* but for the CLI's own stdout stream rather than the rendered report —
|
|
674
|
+
* "attendance is telemetry, never a silent drop" applies to the estimate
|
|
675
|
+
* preview too, not only to a completed pass.
|
|
676
|
+
*/
|
|
677
|
+
function _format_quorum_line(q: QuorumResult): string {
|
|
678
|
+
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
679
|
+
return `council:quorum · ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.`;
|
|
551
680
|
}
|
|
552
681
|
|
|
553
682
|
function build_members(settings: Dict, opts: BuildMembersOptions = {}): ExternalAIClient[] {
|
|
@@ -555,12 +684,31 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
555
684
|
const model_overrides = opts.model_overrides ?? null;
|
|
556
685
|
const siblings_overrides = opts.siblings_overrides ?? null;
|
|
557
686
|
const skipped = opts.skipped ?? null;
|
|
687
|
+
const quorum_out = opts.quorum_out ?? null;
|
|
688
|
+
// Read-only, per-process-memoised (`detectEnvironment` caches the
|
|
689
|
+
// no-argument call) — cheap to call once per `build_members` invocation.
|
|
690
|
+
// A caller may inject a fixed report (tests only); production leaves
|
|
691
|
+
// this unset.
|
|
692
|
+
const report = opts.environment_report ?? detectEnvironment();
|
|
558
693
|
|
|
559
694
|
const ai = _isDict(settings) ? ((settings['ai_council'] as Dict) || {}) : {};
|
|
560
695
|
if (!_pyBool(ai['enabled'])) {
|
|
696
|
+
// The file this message names is load-bearing, and it named the wrong one
|
|
697
|
+
// for months. Measured across 10 sessions in one consumer project: the
|
|
698
|
+
// agent reasoned from `.agent-settings.yml` six times, concluded no
|
|
699
|
+
// council existed, and substituted a weaker path — and one session
|
|
700
|
+
// "fixed" it by COPYING the user-global config into the project tree,
|
|
701
|
+
// because this message told it that was where the switch lived. An error
|
|
702
|
+
// from the authoritative tool is the strongest signal in the room; when
|
|
703
|
+
// it points at the wrong file it does not merely fail to help, it
|
|
704
|
+
// manufactures the wrong belief. Name the real path (ADR-104: always
|
|
705
|
+
// user-global, never project-local) and name the verb that reports it.
|
|
561
706
|
throw new CouncilDisabledError(
|
|
562
|
-
|
|
563
|
-
'flip it on before invoking
|
|
707
|
+
`enabled is false in the council config (${COUNCIL_CONFIG_USER_GLOBAL_REL} ` +
|
|
708
|
+
'under the user-global agent-config root) — flip it on before invoking ' +
|
|
709
|
+
'council:* commands. This config is NEVER project-local (ADR-104), so do ' +
|
|
710
|
+
'not look for it, or create it, in the project tree. ' +
|
|
711
|
+
'`agent-config council:status` prints the resolved path.',
|
|
564
712
|
);
|
|
565
713
|
}
|
|
566
714
|
const members_cfg = (ai['members'] as Dict) || {};
|
|
@@ -602,26 +750,58 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
602
750
|
);
|
|
603
751
|
}
|
|
604
752
|
const members: ExternalAIClient[] = [];
|
|
753
|
+
// Enabled member-config ENTRIES this pass considered — the `n` in
|
|
754
|
+
// quorum's k-of-n. One entry always resolves to exactly one of: a
|
|
755
|
+
// successful construction (>=1 pushed client; siblings fan-out still
|
|
756
|
+
// counts as one entry), or exactly one absent count — never both, never
|
|
757
|
+
// neither — so `total_enabled - absent_count` is `present` without a
|
|
758
|
+
// second pass over `members`. `absent_count` is tracked independently of
|
|
759
|
+
// the caller-optional `skipped` out-param (which may be `null` when a
|
|
760
|
+
// caller wants `quorum_out` without the human-readable skip list) —
|
|
761
|
+
// counting `skipped.length` instead silently overcounted `present` for
|
|
762
|
+
// that caller shape.
|
|
763
|
+
let total_enabled = 0;
|
|
764
|
+
let absent_count = 0;
|
|
605
765
|
for (const name of Object.keys(members_cfg)) {
|
|
606
766
|
const cfg = ((members_cfg[name] as Dict) || {}) as Dict;
|
|
607
767
|
if (!_pyBool(cfg['enabled'])) {
|
|
608
768
|
if (name in siblings) {
|
|
609
769
|
throw new CouncilDisabledError(
|
|
610
770
|
`--siblings targets member ${_pyReprStr(name)} but it is not ` +
|
|
611
|
-
`enabled in
|
|
771
|
+
`enabled in the council config (members.${name}.enabled in ` +
|
|
772
|
+
`${COUNCIL_CONFIG_USER_GLOBAL_REL} under the user-global root).`,
|
|
612
773
|
);
|
|
613
774
|
}
|
|
614
775
|
continue;
|
|
615
776
|
}
|
|
616
|
-
|
|
777
|
+
total_enabled += 1;
|
|
778
|
+
// road-to-always-on-orchestration Phase 3.1: the single reconciled
|
|
779
|
+
// entry point — composes `resolve_mode` (which layer decided the
|
|
780
|
+
// mode) with `resolveTransport` (what `auto` expands to on THIS
|
|
781
|
+
// machine). The hand-rolled `mode === 'api' | 'cli' | 'manual'`
|
|
782
|
+
// switch this replaces had no `'auto'` case, so every invocation
|
|
783
|
+
// broke the moment the loader default flipped `api` → `auto`
|
|
784
|
+
// (`config.ts::_build_defaults`) — every member fell through to the
|
|
785
|
+
// final `else` below and killed the whole pass.
|
|
786
|
+
const api_key_present = _member_api_key_present(cfg);
|
|
787
|
+
const resolved = resolveMemberTransport({
|
|
788
|
+
provider: name,
|
|
789
|
+
report,
|
|
617
790
|
invocationMode: invocation_mode,
|
|
618
791
|
memberSettings: cfg,
|
|
619
792
|
globalMode: global_mode ?? null,
|
|
793
|
+
binaryOverride: (cfg['binary'] as string | null) ?? null,
|
|
794
|
+
// `exactOptionalPropertyTypes` rejects an explicit `undefined` —
|
|
795
|
+
// see `resolveMemberTransport`'s own forwarding call for the
|
|
796
|
+
// same pattern.
|
|
797
|
+
...(api_key_present !== undefined ? { apiKeyPresent: api_key_present } : {}),
|
|
620
798
|
});
|
|
621
799
|
if (name in siblings) {
|
|
622
|
-
if (
|
|
800
|
+
if (resolved.transport !== 'api') {
|
|
623
801
|
throw new CouncilDisabledError(
|
|
624
|
-
`--siblings requires mode=api for member ${_pyReprStr(name)}
|
|
802
|
+
`--siblings requires mode=api for member ${_pyReprStr(name)} ` +
|
|
803
|
+
`(configured mode=${_pyReprStr(resolved.configuredMode)}, resolved transport=` +
|
|
804
|
+
`${_pyReprStr(resolved.transport ?? 'none')}).`,
|
|
625
805
|
);
|
|
626
806
|
}
|
|
627
807
|
const api_key_ref = (cfg['api_key_ref'] as string | null) ?? null;
|
|
@@ -642,19 +822,69 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
642
822
|
}
|
|
643
823
|
continue;
|
|
644
824
|
}
|
|
825
|
+
if (resolved.transport === null) {
|
|
826
|
+
// Phase 3.2 — graded degradation: a member whose transport
|
|
827
|
+
// resolves to ∅ (no CLI + no key under `auto`) is recorded
|
|
828
|
+
// `absent` with a machine-readable `AbsentReason` and the pass
|
|
829
|
+
// continues, instead of killing the whole invocation the way
|
|
830
|
+
// the old unconditional `else { throw }` below did.
|
|
831
|
+
const detail = resolved.reason ?? `member ${name} has no available transport`;
|
|
832
|
+
const entry: Dict = {
|
|
833
|
+
member: name,
|
|
834
|
+
reason: resolved.absentReason ?? 'unavailable',
|
|
835
|
+
detail,
|
|
836
|
+
};
|
|
837
|
+
absent_count += 1;
|
|
838
|
+
if (skipped !== null) {
|
|
839
|
+
skipped.push(entry);
|
|
840
|
+
}
|
|
841
|
+
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
645
844
|
const model = (overrides[name] as string | undefined) || (cfg['model'] as string | undefined) || null;
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
845
|
+
if (resolved.transport === 'api' && _API_PROVIDERS.has(name)) {
|
|
846
|
+
// m2 fix (independent-review finding) — `resolveMemberTransport`'s
|
|
847
|
+
// `auto` chain treats "a key-file or env-key auth record exists
|
|
848
|
+
// for this provider" as sufficient to resolve transport=`api`
|
|
849
|
+
// (its own default `apiKeyPresent` read off the environment
|
|
850
|
+
// report, used whenever the member config carries no explicit
|
|
851
|
+
// `api_key_ref` at all — see `_member_api_key_present` above,
|
|
852
|
+
// which returns `undefined`, not `false`, in exactly that case).
|
|
853
|
+
// But gemini/xai/perplexity's OWN construction contract is
|
|
854
|
+
// stricter: `_construct_api_member` requires an EXPLICIT
|
|
855
|
+
// `api_key_ref` for those three and refuses the legacy-fallback
|
|
856
|
+
// path other providers get, throwing `CouncilDisabledError`. A
|
|
857
|
+
// generic env var (e.g. `GEMINI_API_KEY` set for an unrelated
|
|
858
|
+
// tool) satisfying the permissive auto-chain read while
|
|
859
|
+
// `api_key_ref` was never wired into the council config is
|
|
860
|
+
// exactly this mismatch — previously an UNCAUGHT throw here
|
|
861
|
+
// killed the entire pass. Same catch-and-skip shape the CLI
|
|
862
|
+
// branch below already uses for its own construction failure.
|
|
863
|
+
try {
|
|
864
|
+
members.push(
|
|
865
|
+
_construct_api_member(name, model, {
|
|
866
|
+
api_key_ref: (cfg['api_key_ref'] as string | null) ?? null,
|
|
867
|
+
// Council opts in explicitly (client-default OFF); on unless
|
|
868
|
+
// the operator sets `prompt_cache: false`.
|
|
869
|
+
enable_prompt_cache: cfg['prompt_cache'] !== false,
|
|
870
|
+
// road-to-cache-economy Phase 4 — see the siblings branch above.
|
|
871
|
+
prompt_cache_ttl: (cfg['prompt_cache_ttl'] as '5m' | '1h' | undefined) ?? undefined,
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
} catch (exc) {
|
|
875
|
+
if (!(exc instanceof CouncilDisabledError)) {
|
|
876
|
+
throw exc;
|
|
877
|
+
}
|
|
878
|
+
const detail = exc.message;
|
|
879
|
+
const entry: Dict = { member: name, reason: 'no_auth', detail };
|
|
880
|
+
absent_count += 1;
|
|
881
|
+
if (skipped !== null) {
|
|
882
|
+
skipped.push(entry);
|
|
883
|
+
}
|
|
884
|
+
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
} else if (resolved.transport === 'cli' && _CLI_PROVIDERS.has(name)) {
|
|
658
888
|
try {
|
|
659
889
|
members.push(
|
|
660
890
|
_construct_cli_member(name, model, {
|
|
@@ -680,30 +910,32 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
680
910
|
reason: 'binary_missing',
|
|
681
911
|
detail,
|
|
682
912
|
};
|
|
913
|
+
absent_count += 1;
|
|
683
914
|
if (skipped !== null) {
|
|
684
915
|
skipped.push(entry);
|
|
685
916
|
}
|
|
686
917
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
687
918
|
continue;
|
|
688
919
|
}
|
|
689
|
-
} else if (
|
|
920
|
+
} else if (resolved.transport === 'cli') {
|
|
690
921
|
throw new CouncilDisabledError(
|
|
691
922
|
`member ${_pyReprStr(name)} resolves to mode=cli but no CLI client is ` +
|
|
692
923
|
`wired (known: ${_pyReprStrList(_pySortedStr(_CLI_PROVIDERS))}).`,
|
|
693
924
|
);
|
|
694
|
-
} else if (
|
|
925
|
+
} else if (resolved.transport === 'manual') {
|
|
695
926
|
members.push(new ManualClient({ name, model: model || 'manual' }));
|
|
696
|
-
} else if (mode === 'playwright') {
|
|
697
|
-
throw new CouncilDisabledError(
|
|
698
|
-
`member ${_pyReprStr(name)} resolves to mode=playwright (Phase 2c, not wired).`,
|
|
699
|
-
);
|
|
700
927
|
} else {
|
|
701
928
|
throw new CouncilDisabledError(
|
|
702
|
-
`member ${_pyReprStr(name)} has no transport —
|
|
703
|
-
`name not in
|
|
929
|
+
`member ${_pyReprStr(name)} has no transport — resolved=${_pyReprStr(resolved.transport)}, ` +
|
|
930
|
+
`configured mode=${_pyReprStr(resolved.configuredMode)}, name not in ` +
|
|
931
|
+
`${_pyReprStrList(_pySortedStr(_API_PROVIDERS))}.`,
|
|
704
932
|
);
|
|
705
933
|
}
|
|
706
934
|
}
|
|
935
|
+
if (quorum_out !== null) {
|
|
936
|
+
const present = total_enabled - absent_count;
|
|
937
|
+
quorum_out.result = evaluateQuorum(total_enabled, present, _quorum_setting_from(ai));
|
|
938
|
+
}
|
|
707
939
|
if (members.length === 0) {
|
|
708
940
|
if (skipped && skipped.length > 0) {
|
|
709
941
|
const names = skipped.map((s) => String(s['member'])).join(', ');
|
|
@@ -714,8 +946,10 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
714
946
|
);
|
|
715
947
|
}
|
|
716
948
|
throw new CouncilDisabledError(
|
|
717
|
-
'no council member has `enabled: true` — enable at least one
|
|
718
|
-
|
|
949
|
+
'no council member has `enabled: true` — enable at least one under ' +
|
|
950
|
+
`members.* in ${COUNCIL_CONFIG_USER_GLOBAL_REL} (user-global root; ` +
|
|
951
|
+
'never the project tree, ADR-104). `agent-config council:status` ' +
|
|
952
|
+
'prints the resolved path.',
|
|
719
953
|
);
|
|
720
954
|
}
|
|
721
955
|
return members;
|
|
@@ -1316,6 +1550,105 @@ function _objToMap(o: Dict): Map<string, string> {
|
|
|
1316
1550
|
return m;
|
|
1317
1551
|
}
|
|
1318
1552
|
|
|
1553
|
+
/**
|
|
1554
|
+
* Phase 4.1 — read a persisted `handoff` block back off a saved responses
|
|
1555
|
+
* JSON. Defensive against a payload written before this field existed
|
|
1556
|
+
* (`undefined`) and against a malformed one (a non-string `decision`, a
|
|
1557
|
+
* `rejected_alternatives` entry missing `option`/`reason`) — either
|
|
1558
|
+
* degrades that one field to `null` rather than throwing; a stale or
|
|
1559
|
+
* hand-edited artefact never blocks a re-render.
|
|
1560
|
+
*/
|
|
1561
|
+
function _deserialise_handoff(data: unknown): HandoffEnvelope | null {
|
|
1562
|
+
if (!_isDict(data)) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
const d = data as Dict;
|
|
1566
|
+
const decision = typeof d['decision'] === 'string' ? (d['decision'] as string) : null;
|
|
1567
|
+
const rawAlts = d['rejected_alternatives'];
|
|
1568
|
+
const alts = Array.isArray(rawAlts)
|
|
1569
|
+
? (rawAlts as unknown[])
|
|
1570
|
+
.filter(
|
|
1571
|
+
(a): a is Dict =>
|
|
1572
|
+
_isDict(a) && typeof (a as Dict)['option'] === 'string' && typeof (a as Dict)['reason'] === 'string',
|
|
1573
|
+
)
|
|
1574
|
+
.map((a) => ({ option: a['option'] as string, reason: a['reason'] as string }))
|
|
1575
|
+
: [];
|
|
1576
|
+
const rawConstraints = d['constraints'];
|
|
1577
|
+
const constraints = Array.isArray(rawConstraints)
|
|
1578
|
+
? (rawConstraints as unknown[]).filter((c): c is string => typeof c === 'string')
|
|
1579
|
+
: [];
|
|
1580
|
+
return {
|
|
1581
|
+
decision,
|
|
1582
|
+
rejected_alternatives: alts.length > 0 ? alts : null,
|
|
1583
|
+
constraints: constraints.length > 0 ? constraints : null,
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const _VALID_ABSENT_REASONS: ReadonlySet<AbsentReason> = new Set([
|
|
1588
|
+
'no_binary',
|
|
1589
|
+
'no_auth',
|
|
1590
|
+
'timeout',
|
|
1591
|
+
'quota',
|
|
1592
|
+
]);
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* M4 (independent-review finding) — `cmd_run` writes `payload['quorum']` /
|
|
1596
|
+
* `payload['absent_members']` (Phase 3.2/3.3), but `cmd_render` never read
|
|
1597
|
+
* either back, so a `council:render` off a saved payload silently dropped
|
|
1598
|
+
* the graded-degradation picture the JSON already carried — same shape as
|
|
1599
|
+
* the `handoff` round-trip gap `_deserialise_handoff` above closes. Same
|
|
1600
|
+
* defensive style: a payload written before these fields existed, or a
|
|
1601
|
+
* malformed entry, degrades to `null`/a shorter list rather than throwing —
|
|
1602
|
+
* a stale or hand-edited artefact never blocks a re-render. `reason` values
|
|
1603
|
+
* outside the `AbsentReason` enum (e.g. `build_members`'s own
|
|
1604
|
+
* `'unavailable'`/`'binary_missing'` skip reasons, which predate and are
|
|
1605
|
+
* broader than the enum `render()` types against) degrade to `null` — the
|
|
1606
|
+
* `member`/`detail` text still renders either way.
|
|
1607
|
+
*/
|
|
1608
|
+
function _deserialise_absent_members(data: unknown): readonly RenderAbsentMember[] | null {
|
|
1609
|
+
if (!Array.isArray(data)) {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
const out: RenderAbsentMember[] = [];
|
|
1613
|
+
for (const entry of data) {
|
|
1614
|
+
if (!_isDict(entry)) {
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const d = entry as Dict;
|
|
1618
|
+
const member = d['member'];
|
|
1619
|
+
const detail = d['detail'];
|
|
1620
|
+
if (typeof member !== 'string' || typeof detail !== 'string') {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const raw = d['reason'];
|
|
1624
|
+
const reason = typeof raw === 'string' && _VALID_ABSENT_REASONS.has(raw as AbsentReason)
|
|
1625
|
+
? (raw as AbsentReason)
|
|
1626
|
+
: null;
|
|
1627
|
+
out.push({ member, reason, detail });
|
|
1628
|
+
}
|
|
1629
|
+
return out.length > 0 ? out : null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function _deserialise_quorum(data: unknown): QuorumResult | null {
|
|
1633
|
+
if (!_isDict(data)) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
const d = data as Dict;
|
|
1637
|
+
const status = d['status'];
|
|
1638
|
+
const threshold = d['threshold'];
|
|
1639
|
+
const total = d['total'];
|
|
1640
|
+
const present = d['present'];
|
|
1641
|
+
if (
|
|
1642
|
+
(status !== 'concluded' && status !== 'inconclusive') ||
|
|
1643
|
+
typeof threshold !== 'number' ||
|
|
1644
|
+
typeof total !== 'number' ||
|
|
1645
|
+
typeof present !== 'number'
|
|
1646
|
+
) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
return { status, threshold, total, present };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1319
1652
|
// ── round / depth / token resolution ────────────────────────────────
|
|
1320
1653
|
|
|
1321
1654
|
function _resolve_rounds(args: Args, ai_cfg: Dict): number {
|
|
@@ -1362,12 +1695,14 @@ function cmd_estimate(
|
|
|
1362
1695
|
const advisor_plans = _build_advisor_plans(ai_cfg, REPO_ROOT);
|
|
1363
1696
|
const explicit_overrides = _parse_model_overrides(_getattr<string[] | null>(args, 'model', null));
|
|
1364
1697
|
const skipped: Dict[] = [];
|
|
1698
|
+
const quorum_out: { result: QuorumResult | null } = { result: null };
|
|
1365
1699
|
if (members === null) {
|
|
1366
1700
|
members = build_members(settings, {
|
|
1367
1701
|
invocation_mode: args.mode_override,
|
|
1368
1702
|
model_overrides: _advisor_model_overrides(advisor_plans, explicit_overrides),
|
|
1369
1703
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
1370
1704
|
skipped,
|
|
1705
|
+
quorum_out,
|
|
1371
1706
|
});
|
|
1372
1707
|
}
|
|
1373
1708
|
if (table === null) {
|
|
@@ -1406,6 +1741,9 @@ function cmd_estimate(
|
|
|
1406
1741
|
if (skipped.length > 0) {
|
|
1407
1742
|
_stdout(format_install_hints(skipped) + '\n');
|
|
1408
1743
|
}
|
|
1744
|
+
if (quorum_out.result !== null) {
|
|
1745
|
+
_stdout(_format_quorum_line(quorum_out.result) + '\n');
|
|
1746
|
+
}
|
|
1409
1747
|
_stdout(
|
|
1410
1748
|
format_estimate_table(billable, estimates, {
|
|
1411
1749
|
consensus_delta_usd: extra_usd,
|
|
@@ -1855,6 +2193,95 @@ function _emit_shadow_slo_banner(): void {
|
|
|
1855
2193
|
}
|
|
1856
2194
|
}
|
|
1857
2195
|
|
|
2196
|
+
/**
|
|
2197
|
+
* `council:status` — answer "is a council configured, and from where" without
|
|
2198
|
+
* spending anything, so no caller has to infer it from a filename.
|
|
2199
|
+
*
|
|
2200
|
+
* WHY THIS EXISTS. Measured 2026-08-08: an agent working in a consumer project
|
|
2201
|
+
* announced "Kein Council konfiguriert (keine `.agent-settings.yml`)" and
|
|
2202
|
+
* substituted a weaker subagent fan-out. The council was configured the whole
|
|
2203
|
+
* time, user-globally. The inference was wrong twice over — the council config
|
|
2204
|
+
* has not lived in `.agent-settings.yml` since the Phase-0 migration, and per
|
|
2205
|
+
* ADR-104 the project tree is **never** searched, so the presence or absence of
|
|
2206
|
+
* any project file says nothing at all about council availability.
|
|
2207
|
+
*
|
|
2208
|
+
* The guidance was not missing: `/council default` states this emphatically in
|
|
2209
|
+
* four places. But a command file only loads when the command is invoked, and
|
|
2210
|
+
* the agent was deciding *whether a capability existed* — a question it reached
|
|
2211
|
+
* before invoking anything. The always-loaded layer says "when the council is
|
|
2212
|
+
* enabled" and never says how to find out.
|
|
2213
|
+
*
|
|
2214
|
+
* So the fix is not more prose. It is a check that costs nothing to run and
|
|
2215
|
+
* leaves nothing to infer. Exit is always 0 — this reports a state, it does not
|
|
2216
|
+
* gate — and the verdict line is machine-greppable in both directions.
|
|
2217
|
+
*/
|
|
2218
|
+
export function cmd_status(args: Args, opts: { env?: Record<string, string | undefined> } = {}): number {
|
|
2219
|
+
const env = opts.env ?? process.env;
|
|
2220
|
+
const override = env[COUNCIL_CONFIG_ENV];
|
|
2221
|
+
const path_ = resolve_config_path(REPO_ROOT, { env: env as never });
|
|
2222
|
+
const exists = fs.existsSync(String(path_));
|
|
2223
|
+
const provenance = override ? `${COUNCIL_CONFIG_ENV} override` : 'user-global';
|
|
2224
|
+
|
|
2225
|
+
let cfg: CouncilConfig | null = null;
|
|
2226
|
+
let parse_error: string | null = null;
|
|
2227
|
+
if (exists) {
|
|
2228
|
+
try {
|
|
2229
|
+
cfg = load_council_config(path_);
|
|
2230
|
+
} catch (exc) {
|
|
2231
|
+
parse_error = exc instanceof Error ? exc.message : String(exc);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
const enabledMembers = cfg === null ? [] : [...cfg.members.entries()].filter(([, m]) => m.enabled);
|
|
2236
|
+
// "Configured" deliberately means: a readable file whose `enabled` is true and
|
|
2237
|
+
// which has at least one enabled member. A file that parses but enables
|
|
2238
|
+
// nothing is the case a caller most needs told apart from a missing file,
|
|
2239
|
+
// because only one of the two is fixed by writing a config.
|
|
2240
|
+
const configured = cfg !== null && cfg.enabled && enabledMembers.length > 0;
|
|
2241
|
+
|
|
2242
|
+
if (_getattr(args, 'json', false)) {
|
|
2243
|
+
_stdout(
|
|
2244
|
+
`${JSON.stringify({
|
|
2245
|
+
configured,
|
|
2246
|
+
path: String(path_),
|
|
2247
|
+
exists,
|
|
2248
|
+
provenance,
|
|
2249
|
+
enabled: cfg?.enabled ?? null,
|
|
2250
|
+
members_total: cfg === null ? null : cfg.members.size,
|
|
2251
|
+
members_enabled: cfg === null ? null : enabledMembers.length,
|
|
2252
|
+
member_names: enabledMembers.map(([n]) => n),
|
|
2253
|
+
parse_error,
|
|
2254
|
+
project_tree_consulted: false,
|
|
2255
|
+
})}\n`,
|
|
2256
|
+
);
|
|
2257
|
+
return 0;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
_stdout(`council:status · ${configured ? 'CONFIGURED' : 'NOT CONFIGURED'}\n`);
|
|
2261
|
+
_stdout(` config path ${String(path_)}\n`);
|
|
2262
|
+
_stdout(` resolved by ${provenance}\n`);
|
|
2263
|
+
_stdout(` file exists ${exists ? 'yes' : 'no'}\n`);
|
|
2264
|
+
if (parse_error !== null) {
|
|
2265
|
+
_stdout(` parse error ${parse_error}\n`);
|
|
2266
|
+
}
|
|
2267
|
+
if (cfg !== null) {
|
|
2268
|
+
_stdout(` enabled ${cfg.enabled ? 'yes' : 'no'}\n`);
|
|
2269
|
+
_stdout(` members ${enabledMembers.length} enabled of ${cfg.members.size}\n`);
|
|
2270
|
+
if (enabledMembers.length > 0) {
|
|
2271
|
+
_stdout(` ${enabledMembers.map(([n]) => n).join(', ')}\n`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
_stdout('\n');
|
|
2275
|
+
_stdout(' The project tree is NEVER consulted (ADR-104). No project file —\n');
|
|
2276
|
+
_stdout(' `.agent-settings.yml` included — indicates council availability in\n');
|
|
2277
|
+
_stdout(' either direction. Do not infer it from one; run this instead.\n');
|
|
2278
|
+
if (!configured) {
|
|
2279
|
+
_stdout(`\n To configure: write ${COUNCIL_CONFIG_USER_GLOBAL_REL} under the user-global\n`);
|
|
2280
|
+
_stdout(' agent-config root, with `enabled: true` and at least one enabled member.\n');
|
|
2281
|
+
}
|
|
2282
|
+
return 0;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1858
2285
|
function _apply_solo_dispatch(members: ExternalAIClient[]): [ExternalAIClient[], string | null] {
|
|
1859
2286
|
let cfg: CouncilConfig;
|
|
1860
2287
|
try {
|
|
@@ -1928,12 +2355,14 @@ function cmd_run(
|
|
|
1928
2355
|
const advisor_plans = _build_advisor_plans(ai_cfg, REPO_ROOT);
|
|
1929
2356
|
const explicit_overrides = _parse_model_overrides(_getattr<string[] | null>(args, 'model', null));
|
|
1930
2357
|
const skipped: Dict[] = [];
|
|
2358
|
+
const quorum_out: { result: QuorumResult | null } = { result: null };
|
|
1931
2359
|
if (members === null) {
|
|
1932
2360
|
members = build_members(settings, {
|
|
1933
2361
|
invocation_mode: args.mode_override,
|
|
1934
2362
|
model_overrides: _advisor_model_overrides(advisor_plans, explicit_overrides),
|
|
1935
2363
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
1936
2364
|
skipped,
|
|
2365
|
+
quorum_out,
|
|
1937
2366
|
});
|
|
1938
2367
|
}
|
|
1939
2368
|
if (_getattr(args, 'single', false)) {
|
|
@@ -1996,6 +2425,9 @@ function cmd_run(
|
|
|
1996
2425
|
if (skipped.length > 0) {
|
|
1997
2426
|
_stdout(format_install_hints(skipped) + '\n');
|
|
1998
2427
|
}
|
|
2428
|
+
if (quorum_out.result !== null) {
|
|
2429
|
+
_stdout(_format_quorum_line(quorum_out.result) + '\n');
|
|
2430
|
+
}
|
|
1999
2431
|
_stdout(
|
|
2000
2432
|
format_estimate_table(billable, estimates, {
|
|
2001
2433
|
consensus_delta_usd: extra_usd,
|
|
@@ -2091,6 +2523,38 @@ function cmd_run(
|
|
|
2091
2523
|
stance_repairs.push(r);
|
|
2092
2524
|
},
|
|
2093
2525
|
});
|
|
2526
|
+
// M3 fix (independent-review finding) — quorum measures MEMBERS THAT
|
|
2527
|
+
// ACTUALLY PRODUCED A USABLE RESPONSE, per `quorum.ts`'s own docstring
|
|
2528
|
+
// ("at least k of n enabled members produced a usable response"). Before
|
|
2529
|
+
// this, `quorum_out.result` stayed the PRE-RUN value `build_members`
|
|
2530
|
+
// set — constructibility, not usability: a member that constructed fine
|
|
2531
|
+
// and then failed mid-flight (auth expired, timeout, quota exhausted)
|
|
2532
|
+
// still counted as `present`, so a pass with every provider erroring
|
|
2533
|
+
// could still report `concluded`. `_postRunQuorum` re-derives presence
|
|
2534
|
+
// over `responses` (index-aligned with `members` — `consult()`'s own
|
|
2535
|
+
// contract, one `CouncilResponse` per member from the final round) and
|
|
2536
|
+
// this overwrites BOTH the shared `quorum_out` out-param (the
|
|
2537
|
+
// payload-construction code below already reads it) and `skipped`
|
|
2538
|
+
// (mid-flight failures appended to the same array construction-time
|
|
2539
|
+
// skips already populate, so `payload['absent_members']` needs no
|
|
2540
|
+
// separate wiring either).
|
|
2541
|
+
const post_run = _postRunQuorum(members, responses, ai_cfg);
|
|
2542
|
+
quorum_out.result = post_run.quorum;
|
|
2543
|
+
skipped.push(...post_run.absent);
|
|
2544
|
+
// Phase 4.1 — verdict → handoff envelope. Same tally `render()` computes
|
|
2545
|
+
// internally for the Vote Tally block (mirrored here, not imported from
|
|
2546
|
+
// there, because `render()` re-derives it from the SAVED payload on a
|
|
2547
|
+
// later `council:render` — this pass needs its own copy to WRITE into
|
|
2548
|
+
// the payload in the first place). `buildHandoffFromStanceTally` returns
|
|
2549
|
+
// the honest all-null envelope when stance tally never ran or split, so
|
|
2550
|
+
// this is always additive, never a fabricated decision.
|
|
2551
|
+
const handoff: HandoffEnvelope = buildHandoffFromStanceTally(
|
|
2552
|
+
stance_tally_on
|
|
2553
|
+
? tally_stances(
|
|
2554
|
+
responses.filter((r) => !r.error).map((r) => ({ member: `${r.provider}:${r.model}`, text: r.text })),
|
|
2555
|
+
)
|
|
2556
|
+
: null,
|
|
2557
|
+
);
|
|
2094
2558
|
const persona_labels = build_persona_labels(advisor_plans, billable);
|
|
2095
2559
|
const peer_review = _maybe_run_peer_review(
|
|
2096
2560
|
ai_cfg,
|
|
@@ -2172,6 +2636,21 @@ function cmd_run(
|
|
|
2172
2636
|
if (blind_review_map !== null) {
|
|
2173
2637
|
payload['blind_review_map'] = _mapToObject(blind_review_map);
|
|
2174
2638
|
}
|
|
2639
|
+
// Phase 3.3 — the response artefact carries the same k-of-n verdict the
|
|
2640
|
+
// stdout banner already showed; a `null` (no ref supplied) writes
|
|
2641
|
+
// nothing, so a caller that never asks for quorum sees no schema change.
|
|
2642
|
+
if (quorum_out.result !== null) {
|
|
2643
|
+
payload['quorum'] = quorum_out.result;
|
|
2644
|
+
}
|
|
2645
|
+
// Phase 3.2 — machine-readable graded degradation alongside the
|
|
2646
|
+
// stdout `format_install_hints` banner; empty when nothing was absent.
|
|
2647
|
+
if (skipped.length > 0) {
|
|
2648
|
+
payload['absent_members'] = skipped;
|
|
2649
|
+
}
|
|
2650
|
+
// Phase 4.1 — always written (a stable key beats a conditionally-present
|
|
2651
|
+
// one for a machine consumer), even when every field is `null`: the
|
|
2652
|
+
// work-order envelope for whatever executes on this verdict next.
|
|
2653
|
+
payload['handoff'] = handoff;
|
|
2175
2654
|
const out_path = _validate_council_output_path(args.output as string, {
|
|
2176
2655
|
kind: 'responses',
|
|
2177
2656
|
subcommand: 'run',
|
|
@@ -2601,6 +3080,9 @@ function cmd_render(args: Args): number {
|
|
|
2601
3080
|
chairman: _isDict(payload['chairman'])
|
|
2602
3081
|
? (payload['chairman'] as { member: string | null; annotation: string; text: string | null })
|
|
2603
3082
|
: null,
|
|
3083
|
+
handoff: _deserialise_handoff(payload['handoff']),
|
|
3084
|
+
absent_members: _deserialise_absent_members(payload['absent_members']),
|
|
3085
|
+
quorum: _deserialise_quorum(payload['quorum']),
|
|
2604
3086
|
});
|
|
2605
3087
|
if (_getattr<string | null>(args, 'output', null)) {
|
|
2606
3088
|
const out_path = _validate_council_output_path(args.output as string, {
|
|
@@ -2826,13 +3308,15 @@ interface Args {
|
|
|
2826
3308
|
// shadow-report
|
|
2827
3309
|
log: string | null;
|
|
2828
3310
|
window_days: number;
|
|
3311
|
+
// status
|
|
3312
|
+
json: boolean;
|
|
2829
3313
|
}
|
|
2830
3314
|
|
|
2831
|
-
const _SUBCOMMANDS = ['estimate', 'run', 'debate', 'render', 'replay', 'quota', 'shadow-report'];
|
|
3315
|
+
const _SUBCOMMANDS = ['estimate', 'run', 'debate', 'render', 'replay', 'quota', 'shadow-report', 'status'];
|
|
2832
3316
|
|
|
2833
3317
|
const _TOP_USAGE =
|
|
2834
3318
|
`usage: ${_PROG} [-h]\n` +
|
|
2835
|
-
` {estimate,run,debate,render,replay,quota,shadow-report}\n` +
|
|
3319
|
+
` {estimate,run,debate,render,replay,quota,shadow-report,status}\n` +
|
|
2836
3320
|
` ...\n`;
|
|
2837
3321
|
|
|
2838
3322
|
function _topError(message: string): never {
|
|
@@ -2886,6 +3370,7 @@ function _defaultArgs(): Args {
|
|
|
2886
3370
|
reset: null,
|
|
2887
3371
|
log: null,
|
|
2888
3372
|
window_days: 7,
|
|
3373
|
+
json: false,
|
|
2889
3374
|
};
|
|
2890
3375
|
}
|
|
2891
3376
|
|
|
@@ -3009,6 +3494,12 @@ function _specsFor(cmd: string): { positionals: string[]; opts: OptSpec[]; requi
|
|
|
3009
3494
|
],
|
|
3010
3495
|
requiredOpts: [],
|
|
3011
3496
|
};
|
|
3497
|
+
case 'status':
|
|
3498
|
+
return {
|
|
3499
|
+
positionals: [],
|
|
3500
|
+
opts: [{ flag: '--json', takesValue: false, apply: (o) => (o.json = true) }],
|
|
3501
|
+
requiredOpts: [],
|
|
3502
|
+
};
|
|
3012
3503
|
default:
|
|
3013
3504
|
return { positionals: [], opts: [], requiredOpts: [] };
|
|
3014
3505
|
}
|
|
@@ -3213,6 +3704,9 @@ function main(argv: string[] | null = null): number {
|
|
|
3213
3704
|
if (args.cmd === 'shadow-report') {
|
|
3214
3705
|
return cmd_shadow_report(args);
|
|
3215
3706
|
}
|
|
3707
|
+
if (args.cmd === 'status') {
|
|
3708
|
+
return cmd_status(args);
|
|
3709
|
+
}
|
|
3216
3710
|
} catch (exc) {
|
|
3217
3711
|
if (exc instanceof CouncilDisabledError) {
|
|
3218
3712
|
_stderr(`❌ council:${args.cmd}: ${exc.message}\n`);
|
|
@@ -3284,5 +3778,6 @@ export {
|
|
|
3284
3778
|
_parse_model_overrides,
|
|
3285
3779
|
_parse_siblings_overrides,
|
|
3286
3780
|
_synthesize_ai_council_block,
|
|
3781
|
+
_postRunQuorum,
|
|
3287
3782
|
CouncilDisabledError,
|
|
3288
3783
|
};
|