@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- 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 +18 -0
- 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/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.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/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -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/templates/AGENTS.md +4 -4
- 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-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- 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-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -7
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- 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 +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- 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/host_capability.ts +63 -6
- 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/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -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/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_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- 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/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- 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
|
@@ -69,6 +69,9 @@ import {
|
|
|
69
69
|
import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
|
|
70
70
|
import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
|
|
71
71
|
import { CHAIRMAN_FIELDS_ADDENDUM, render_deanonymization_block } from './blind_review.js';
|
|
72
|
+
import type { AbsentReason } from './transport_resolver.js';
|
|
73
|
+
import type { QuorumResult } from './quorum.js';
|
|
74
|
+
import { isEmptyHandoff, type HandoffEnvelope } from './handoff.js';
|
|
72
75
|
|
|
73
76
|
// ── Python-format / stdlib parity helpers ────────────────────────────────
|
|
74
77
|
//
|
|
@@ -1811,6 +1814,38 @@ export interface RenderOptions {
|
|
|
1811
1814
|
* directly in `chairman.text`. Default `null` → byte-identical.
|
|
1812
1815
|
*/
|
|
1813
1816
|
blind?: { label_to_source: ReadonlyMap<string, string> } | null;
|
|
1817
|
+
/**
|
|
1818
|
+
* Phase 3.2 (road-to-always-on-orchestration): members that produced no
|
|
1819
|
+
* usable response this pass, rendered as a trailing section so the
|
|
1820
|
+
* human-readable report shows the same graded-degradation picture the
|
|
1821
|
+
* session manifest carries structurally. Default `null`/empty →
|
|
1822
|
+
* byte-identical — no existing caller passes this, so no existing
|
|
1823
|
+
* output changes.
|
|
1824
|
+
*/
|
|
1825
|
+
absent_members?: readonly RenderAbsentMember[] | null;
|
|
1826
|
+
/**
|
|
1827
|
+
* Phase 3.3: this pass's quorum verdict, rendered as one line right
|
|
1828
|
+
* before the absent-members section (or standalone when there is
|
|
1829
|
+
* nothing absent to report). Default `null` → byte-identical.
|
|
1830
|
+
*/
|
|
1831
|
+
quorum?: QuorumResult | null;
|
|
1832
|
+
/**
|
|
1833
|
+
* Phase 4.1: the verdict → handoff envelope, rendered as a `### Handoff`
|
|
1834
|
+
* section right after the Convergence/Divergence slot — closest to the
|
|
1835
|
+
* synthesis it was extracted from, ahead of the trailing quorum/absent
|
|
1836
|
+
* bookkeeping. Rendered only when at least one field is non-`null`
|
|
1837
|
+
* (`isEmptyHandoff` — an all-null envelope has nothing worth a section
|
|
1838
|
+
* for, same "only show when there's substance" call `absent_members`
|
|
1839
|
+
* makes). Default `null` → byte-identical.
|
|
1840
|
+
*/
|
|
1841
|
+
handoff?: HandoffEnvelope | null;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/** The minimal shape `render()` needs for one absent-member row. */
|
|
1845
|
+
export interface RenderAbsentMember {
|
|
1846
|
+
readonly member: string;
|
|
1847
|
+
readonly reason: AbsentReason | null;
|
|
1848
|
+
readonly detail: string;
|
|
1814
1849
|
}
|
|
1815
1850
|
|
|
1816
1851
|
/**
|
|
@@ -1918,14 +1953,84 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
|
|
|
1918
1953
|
blocks.push(render_vote_tally(tally));
|
|
1919
1954
|
}
|
|
1920
1955
|
blocks.push(`## Convergence / Divergence\n\n${body}`);
|
|
1956
|
+
// Phase 4.1: closest to the synthesis it was extracted from — ahead of
|
|
1957
|
+
// the de-anonymization map and the trailing quorum/absent bookkeeping.
|
|
1958
|
+
// An all-null envelope renders nothing (`isEmptyHandoff`), so a caller
|
|
1959
|
+
// that always attaches one (the common shape once a producer wires
|
|
1960
|
+
// this in) does not spam every response with "nothing to hand off".
|
|
1961
|
+
if (opts.handoff && !isEmptyHandoff(opts.handoff)) {
|
|
1962
|
+
blocks.push(_render_handoff(opts.handoff));
|
|
1963
|
+
}
|
|
1921
1964
|
if (blind !== null) {
|
|
1922
1965
|
// Ü1: de-anonymization AFTER the synthesis slot — blind is only at
|
|
1923
1966
|
// decision time, never the archive.
|
|
1924
1967
|
blocks.push(render_deanonymization_block('### De-anonymization map', blind.label_to_source));
|
|
1925
1968
|
}
|
|
1969
|
+
// Phase 3.2/3.3: trailing, opt-in sections — absent no caller passes
|
|
1970
|
+
// either option, so the join below adds nothing and every existing
|
|
1971
|
+
// render() output stays byte-identical.
|
|
1972
|
+
if (opts.quorum) {
|
|
1973
|
+
blocks.push(_render_quorum_line(opts.quorum));
|
|
1974
|
+
}
|
|
1975
|
+
const absent = opts.absent_members ?? [];
|
|
1976
|
+
if (absent.length > 0) {
|
|
1977
|
+
blocks.push(_render_absent_members(absent));
|
|
1978
|
+
}
|
|
1926
1979
|
return blocks.join('\n\n---\n\n');
|
|
1927
1980
|
}
|
|
1928
1981
|
|
|
1982
|
+
/** One line naming the k-of-n outcome — machine-parseable, human-readable. */
|
|
1983
|
+
function _render_quorum_line(q: QuorumResult): string {
|
|
1984
|
+
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
1985
|
+
return `**Quorum:** ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.`;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* `### Absent Members` — one row per member that never produced a usable
|
|
1990
|
+
* response this pass, naming the machine-readable reason when classified.
|
|
1991
|
+
*/
|
|
1992
|
+
function _render_absent_members(absent: readonly RenderAbsentMember[]): string {
|
|
1993
|
+
const lines = ['### Absent Members', ''];
|
|
1994
|
+
for (const a of absent) {
|
|
1995
|
+
const tag = a.reason !== null ? ` (${a.reason})` : '';
|
|
1996
|
+
lines.push(`- **${a.member}**${tag} — ${a.detail}`);
|
|
1997
|
+
}
|
|
1998
|
+
return lines.join('\n');
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* `### Handoff` — the verdict → work-order envelope (Phase 4.1). Only
|
|
2003
|
+
* called once the caller already confirmed `!isEmptyHandoff(h)`, so at
|
|
2004
|
+
* least one field is populated; the other two still render their own
|
|
2005
|
+
* honest "none recorded" line rather than being silently dropped, so a
|
|
2006
|
+
* reader sees exactly what this pass did and did not extract.
|
|
2007
|
+
*/
|
|
2008
|
+
function _render_handoff(h: HandoffEnvelope): string {
|
|
2009
|
+
const lines = ['### Handoff', ''];
|
|
2010
|
+
lines.push(`**Decision:** ${h.decision ?? 'none recorded — no structured verdict this pass.'}`);
|
|
2011
|
+
lines.push('');
|
|
2012
|
+
if (h.rejected_alternatives !== null && h.rejected_alternatives.length > 0) {
|
|
2013
|
+
lines.push('**Rejected alternatives:**');
|
|
2014
|
+
lines.push('');
|
|
2015
|
+
for (const alt of h.rejected_alternatives) {
|
|
2016
|
+
lines.push(`- **${alt.option}** — ${alt.reason}`);
|
|
2017
|
+
}
|
|
2018
|
+
} else {
|
|
2019
|
+
lines.push('**Rejected alternatives:** none recorded.');
|
|
2020
|
+
}
|
|
2021
|
+
lines.push('');
|
|
2022
|
+
if (h.constraints !== null && h.constraints.length > 0) {
|
|
2023
|
+
lines.push('**Constraints:**');
|
|
2024
|
+
lines.push('');
|
|
2025
|
+
for (const c of h.constraints) {
|
|
2026
|
+
lines.push(`- ${c}`);
|
|
2027
|
+
}
|
|
2028
|
+
} else {
|
|
2029
|
+
lines.push('**Constraints:** none recorded.');
|
|
2030
|
+
}
|
|
2031
|
+
return lines.join('\n');
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1929
2034
|
/**
|
|
1930
2035
|
* Render the peer-review block under deterministic Reviewer labels.
|
|
1931
2036
|
*
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quorum resolution for a council pass (Phase 3.3 of
|
|
3
|
+
* road-to-always-on-orchestration).
|
|
4
|
+
*
|
|
5
|
+
* A pass concludes when at least `k` of `n` enabled members produced a
|
|
6
|
+
* usable response ("present"); below `k` the pass is `inconclusive`. At a
|
|
7
|
+
* release gate an inconclusive pass HOLDS the gate for a human — it is
|
|
8
|
+
* NEVER silently downgraded to advisory (council-verified, 2026-08-09).
|
|
9
|
+
* Deciding what to DO with an `inconclusive` status (block a merge, retry,
|
|
10
|
+
* surface to the user) is the caller's job; this module only answers
|
|
11
|
+
* whether the threshold was met.
|
|
12
|
+
*
|
|
13
|
+
* `majority` is a SIMPLE majority — `ceil(n / 2)` — deliberately NOT the
|
|
14
|
+
* traditional "more than half" (`floor(n / 2) + 1`). At n=2 those two
|
|
15
|
+
* definitions diverge: `ceil(2/2)=1` vs `floor(2/2)+1=2`. The stricter
|
|
16
|
+
* 2-of-2 reading turns any single absent member into a deadlocked release
|
|
17
|
+
* gate, which is exactly the failure mode council members flagged for the
|
|
18
|
+
* n=2 case this module is built around — 1-of-2 is the deliberate choice,
|
|
19
|
+
* not an off-by-one.
|
|
20
|
+
*
|
|
21
|
+
* This module is pure — no filesystem, no environment, no config parsing.
|
|
22
|
+
* `config.ts::QuorumSetting` is the validated shape a loaded
|
|
23
|
+
* `.ai-council.yml` supplies; a caller reads `cfg.quorum` and passes it
|
|
24
|
+
* straight through to `evaluateQuorum`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { QuorumSetting } from './config.js';
|
|
28
|
+
|
|
29
|
+
export type QuorumStatus = 'concluded' | 'inconclusive';
|
|
30
|
+
|
|
31
|
+
export interface QuorumResult {
|
|
32
|
+
/** `concluded` when `present >= threshold`; `inconclusive` otherwise. */
|
|
33
|
+
readonly status: QuorumStatus;
|
|
34
|
+
/** The concrete k this pass needed, after resolving `setting` against `total`. */
|
|
35
|
+
readonly threshold: number;
|
|
36
|
+
/** Total enabled members configured for this pass — the `n`. */
|
|
37
|
+
readonly total: number;
|
|
38
|
+
/** Members that actually produced a usable response. */
|
|
39
|
+
readonly present: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve `setting` into a concrete integer threshold for `total` members.
|
|
44
|
+
*
|
|
45
|
+
* `'majority'` → `ceil(total / 2)` (0 when `total` is 0 — there is nothing
|
|
46
|
+
* to require attendance from). A fixed numeric `setting` is clamped to
|
|
47
|
+
* `[1, total]` so neither a misconfigured cap above `total` (structurally
|
|
48
|
+
* unwinnable) nor one below 1 (would resolve trivially) can escape the
|
|
49
|
+
* config validation `config.ts::_build_quorum` already runs at load time —
|
|
50
|
+
* this is a second, cheap floor for any caller that builds a `QuorumSetting`
|
|
51
|
+
* by hand instead of through the loader.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveQuorumThreshold(total: number, setting: QuorumSetting = 'majority'): number {
|
|
54
|
+
if (!Number.isInteger(total) || total < 0) {
|
|
55
|
+
throw new RangeError(
|
|
56
|
+
`resolveQuorumThreshold: total must be a non-negative integer (got ${total}).`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (total === 0) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
if (setting === 'majority') {
|
|
63
|
+
return Math.ceil(total / 2);
|
|
64
|
+
}
|
|
65
|
+
return Math.min(Math.max(setting, 1), total);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Evaluate whether a pass met quorum.
|
|
70
|
+
*
|
|
71
|
+
* `present` is clamped into `[0, total]` before comparison — a caller that
|
|
72
|
+
* miscounts (more "present" than were ever configured) cannot manufacture a
|
|
73
|
+
* `concluded` verdict past what `total` actually allows.
|
|
74
|
+
*/
|
|
75
|
+
export function evaluateQuorum(
|
|
76
|
+
total: number,
|
|
77
|
+
present: number,
|
|
78
|
+
setting: QuorumSetting = 'majority',
|
|
79
|
+
): QuorumResult {
|
|
80
|
+
const threshold = resolveQuorumThreshold(total, setting);
|
|
81
|
+
const clampedPresent = Math.min(Math.max(present, 0), total);
|
|
82
|
+
const status: QuorumStatus = clampedPresent >= threshold ? 'concluded' : 'inconclusive';
|
|
83
|
+
return { status, threshold, total, present: clampedPresent };
|
|
84
|
+
}
|
|
@@ -46,6 +46,24 @@ import { PyFloat, py_json_dumps_indent2 } from '../_lib/security_lint.js';
|
|
|
46
46
|
import { pyRound } from '../_lib/value_ladder.js';
|
|
47
47
|
import { CouncilResponse } from './clients.js';
|
|
48
48
|
import { render } from './orchestrator.js';
|
|
49
|
+
import type { AbsentReason } from './transport_resolver.js';
|
|
50
|
+
import type { QuorumResult } from './quorum.js';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A member that produced no usable response this pass
|
|
54
|
+
* (road-to-always-on-orchestration Phase 3.2) — never constructed (no CLI +
|
|
55
|
+
* no key), or constructed but lost mid-run (timeout, quota exhaustion). The
|
|
56
|
+
* caller assembling the manifest is the one that knows which; this record
|
|
57
|
+
* only carries what it decided.
|
|
58
|
+
*/
|
|
59
|
+
export interface AbsentMemberRecord {
|
|
60
|
+
/** Provider name (or "provider/model" — whatever the caller's roster used). */
|
|
61
|
+
readonly member: string;
|
|
62
|
+
/** Machine-readable bucket, or `null` when the failure did not classify. */
|
|
63
|
+
readonly reason: AbsentReason | null;
|
|
64
|
+
/** Human-readable detail — the CLI-hints / install-instructions text. */
|
|
65
|
+
readonly detail: string;
|
|
66
|
+
}
|
|
49
67
|
|
|
50
68
|
// src/scripts/ai_council/session.py → parents[3] == repo root.
|
|
51
69
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -76,6 +94,19 @@ export class SessionManifest {
|
|
|
76
94
|
rounds: number;
|
|
77
95
|
cost_usd_estimated: number;
|
|
78
96
|
cost_usd_actual: number;
|
|
97
|
+
/**
|
|
98
|
+
* Members that never produced a usable response this pass (Phase 3.2).
|
|
99
|
+
* Empty by default — a caller that never populates it (every caller
|
|
100
|
+
* before this field existed) writes the same manifest shape it always
|
|
101
|
+
* did, plus one empty array.
|
|
102
|
+
*/
|
|
103
|
+
absent_members: AbsentMemberRecord[];
|
|
104
|
+
/**
|
|
105
|
+
* Quorum verdict for this pass (Phase 3.3), or `null` when the caller
|
|
106
|
+
* did not evaluate one (e.g. a single-member solo dispatch, where
|
|
107
|
+
* quorum has nothing to say).
|
|
108
|
+
*/
|
|
109
|
+
quorum: QuorumResult | null;
|
|
79
110
|
extra: Record<string, unknown>;
|
|
80
111
|
|
|
81
112
|
constructor(opts: {
|
|
@@ -86,6 +117,8 @@ export class SessionManifest {
|
|
|
86
117
|
rounds?: number;
|
|
87
118
|
cost_usd_estimated?: number;
|
|
88
119
|
cost_usd_actual?: number;
|
|
120
|
+
absent_members?: AbsentMemberRecord[];
|
|
121
|
+
quorum?: QuorumResult | null;
|
|
89
122
|
extra?: Record<string, unknown>;
|
|
90
123
|
}) {
|
|
91
124
|
this.mode = opts.mode;
|
|
@@ -95,6 +128,8 @@ export class SessionManifest {
|
|
|
95
128
|
this.rounds = opts.rounds ?? 1;
|
|
96
129
|
this.cost_usd_estimated = opts.cost_usd_estimated ?? 0.0;
|
|
97
130
|
this.cost_usd_actual = opts.cost_usd_actual ?? 0.0;
|
|
131
|
+
this.absent_members = opts.absent_members ?? [];
|
|
132
|
+
this.quorum = opts.quorum ?? null;
|
|
98
133
|
// Python dataclass uses `field(default_factory=dict)` — each instance
|
|
99
134
|
// gets its own fresh dict.
|
|
100
135
|
this.extra = opts.extra ?? {};
|
|
@@ -504,6 +539,15 @@ export function save(opts: {
|
|
|
504
539
|
responses_per_round: rounds_data.map((round_responses) =>
|
|
505
540
|
round_responses.map((r) => _serialise_response(r)),
|
|
506
541
|
),
|
|
542
|
+
// Phase 3.2/3.3 — graded degradation + quorum, both machine-readable.
|
|
543
|
+
// Empty array / `null` when the caller never populated them, so an
|
|
544
|
+
// old caller's manifest keeps its existing keys plus these two.
|
|
545
|
+
absent_members: manifest.absent_members.map((a) => ({
|
|
546
|
+
member: a.member,
|
|
547
|
+
reason: a.reason,
|
|
548
|
+
detail: a.detail,
|
|
549
|
+
})),
|
|
550
|
+
quorum: manifest.quorum,
|
|
507
551
|
...manifest.extra,
|
|
508
552
|
};
|
|
509
553
|
|
|
@@ -48,10 +48,22 @@ import {
|
|
|
48
48
|
type BillingClass,
|
|
49
49
|
type EnvironmentReport,
|
|
50
50
|
} from '../_lib/environment_detector.js';
|
|
51
|
+
import { resolve_mode } from './modes.js';
|
|
51
52
|
|
|
52
53
|
/** A concrete transport a member can actually run on. `auto` is not one. */
|
|
53
54
|
export type Transport = 'api' | 'manual' | 'cli';
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Machine-readable reason a member is `absent` from a pass
|
|
58
|
+
* (road-to-always-on-orchestration Phase 3.2). The static half — `no_binary`
|
|
59
|
+
* / `no_auth` — is decided here, at resolution time, before any provider
|
|
60
|
+
* call is attempted. The runtime half — `timeout` / `quota` — only exists
|
|
61
|
+
* once a call was actually attempted and failed; see
|
|
62
|
+
* `absentReasonFromCliFailure` below for the mapping FROM the mid-flight
|
|
63
|
+
* `CliFailureClass` a caller already classifies its errors into.
|
|
64
|
+
*/
|
|
65
|
+
export type AbsentReason = 'no_binary' | 'no_auth' | 'timeout' | 'quota';
|
|
66
|
+
|
|
55
67
|
/** The accepted values of the `mode` key, including the resolver-only `auto`. */
|
|
56
68
|
export const VALID_TRANSPORT_MODES: ReadonlySet<string> = new Set([
|
|
57
69
|
'api',
|
|
@@ -73,6 +85,16 @@ export interface ResolvedTransport {
|
|
|
73
85
|
readonly makesProviderCall: boolean;
|
|
74
86
|
/** One-line reason. Non-empty exactly when `available` is false. */
|
|
75
87
|
readonly reason: string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Machine-readable classification of `reason`, for a pass artifact that
|
|
90
|
+
* needs to bucket absent members without parsing prose (Phase 3.2).
|
|
91
|
+
* Non-null for the two STATIC auto-chain failures this function can
|
|
92
|
+
* classify (`no_binary` / `no_auth`); `null` when `available` is true,
|
|
93
|
+
* and also `null` for the "unknown mode" validation-error path below —
|
|
94
|
+
* that is a config bug, not a missing-capability finding, so it does not
|
|
95
|
+
* belong in this enum.
|
|
96
|
+
*/
|
|
97
|
+
readonly absentReason: AbsentReason | null;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
export interface ResolveTransportOptions {
|
|
@@ -149,6 +171,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
|
|
|
149
171
|
authSource,
|
|
150
172
|
makesProviderCall: false,
|
|
151
173
|
reason: null,
|
|
174
|
+
absentReason: null,
|
|
152
175
|
};
|
|
153
176
|
}
|
|
154
177
|
|
|
@@ -160,6 +183,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
|
|
|
160
183
|
authSource,
|
|
161
184
|
makesProviderCall: true,
|
|
162
185
|
reason: null,
|
|
186
|
+
absentReason: null,
|
|
163
187
|
};
|
|
164
188
|
}
|
|
165
189
|
|
|
@@ -185,6 +209,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
|
|
|
185
209
|
authSource,
|
|
186
210
|
makesProviderCall: true,
|
|
187
211
|
reason: null,
|
|
212
|
+
absentReason: null,
|
|
188
213
|
};
|
|
189
214
|
}
|
|
190
215
|
|
|
@@ -197,16 +222,80 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
|
|
|
197
222
|
authSource,
|
|
198
223
|
makesProviderCall: true,
|
|
199
224
|
reason: null,
|
|
225
|
+
absentReason: null,
|
|
200
226
|
};
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
return unavailable(
|
|
229
|
+
return unavailable(
|
|
230
|
+
provider,
|
|
231
|
+
billing,
|
|
232
|
+
autoUnavailableReason(provider, binary, cliAuthed),
|
|
233
|
+
classifyStaticAbsentReason(binary),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ResolveMemberTransportOptions {
|
|
238
|
+
readonly provider: string;
|
|
239
|
+
/** The single read-only environment report `resolveTransport` needs. */
|
|
240
|
+
readonly report: EnvironmentReport;
|
|
241
|
+
/** e.g. `/council mode:manual`. Highest precedence layer. */
|
|
242
|
+
readonly invocationMode?: string | null;
|
|
243
|
+
/** The member's own config sub-dict (`ai_council.members.<name>`). */
|
|
244
|
+
readonly memberSettings?: ReadonlyMap<string, unknown> | Record<string, unknown> | null;
|
|
245
|
+
/** `ai_council.mode` / `defaults.mode`, already extracted by the caller. */
|
|
246
|
+
readonly globalMode?: string | null;
|
|
247
|
+
readonly binaryOverride?: string | null;
|
|
248
|
+
readonly apiKeyPresent?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface ResolvedMemberTransport extends ResolvedTransport {
|
|
252
|
+
/** The mode `resolve_mode` chose BEFORE any `auto` expansion. */
|
|
253
|
+
readonly configuredMode: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The single reconciled entry point for "given this member's config, what
|
|
258
|
+
* transport does it actually run on" (road-to-always-on-orchestration
|
|
259
|
+
* Phase 3.1). Composes `modes.ts::resolve_mode` (which layer decided the
|
|
260
|
+
* mode) with `resolveTransport` (what `auto` expands to on THIS machine) so
|
|
261
|
+
* a caller never has to replicate either step by hand.
|
|
262
|
+
*
|
|
263
|
+
* This exists because the two functions previously lived at arm's length:
|
|
264
|
+
* `resolve_mode` can legitimately return the literal string `'auto'`, and
|
|
265
|
+
* nothing forced a caller to then route that value through
|
|
266
|
+
* `resolveTransport` — a hand-rolled `mode === 'api' | 'cli' | 'manual'`
|
|
267
|
+
* switch (the shape `council_cli.ts::build_members` still uses today) has
|
|
268
|
+
* no `'auto'` case and treats it as an unrecognised transport instead of
|
|
269
|
+
* expanding it. `resolveMemberTransport` is the fix: there is no longer a
|
|
270
|
+
* gap between "which mode was configured" and "which transport that becomes"
|
|
271
|
+
* for any caller that uses it instead of the two functions separately.
|
|
272
|
+
*/
|
|
273
|
+
export function resolveMemberTransport(
|
|
274
|
+
opts: ResolveMemberTransportOptions,
|
|
275
|
+
): ResolvedMemberTransport {
|
|
276
|
+
const configuredMode = resolve_mode(opts.provider, {
|
|
277
|
+
invocationMode: opts.invocationMode ?? null,
|
|
278
|
+
memberSettings: opts.memberSettings ?? null,
|
|
279
|
+
globalMode: opts.globalMode ?? null,
|
|
280
|
+
});
|
|
281
|
+
const resolved = resolveTransport({
|
|
282
|
+
provider: opts.provider,
|
|
283
|
+
mode: configuredMode,
|
|
284
|
+
report: opts.report,
|
|
285
|
+
binaryOverride: opts.binaryOverride ?? null,
|
|
286
|
+
// `exactOptionalPropertyTypes` rejects an explicit `undefined` for an
|
|
287
|
+
// optional-boolean field, so the key is omitted entirely rather than
|
|
288
|
+
// forwarded as `apiKeyPresent: undefined`.
|
|
289
|
+
...(opts.apiKeyPresent !== undefined ? { apiKeyPresent: opts.apiKeyPresent } : {}),
|
|
290
|
+
});
|
|
291
|
+
return { ...resolved, configuredMode };
|
|
204
292
|
}
|
|
205
293
|
|
|
206
294
|
function unavailable(
|
|
207
295
|
_provider: string,
|
|
208
296
|
billing: BillingClass,
|
|
209
297
|
reason: string,
|
|
298
|
+
absentReason: AbsentReason | null = null,
|
|
210
299
|
): ResolvedTransport {
|
|
211
300
|
return {
|
|
212
301
|
available: false,
|
|
@@ -217,9 +306,21 @@ function unavailable(
|
|
|
217
306
|
authSource: null,
|
|
218
307
|
makesProviderCall: false,
|
|
219
308
|
reason,
|
|
309
|
+
absentReason,
|
|
220
310
|
};
|
|
221
311
|
}
|
|
222
312
|
|
|
313
|
+
/**
|
|
314
|
+
* `no_binary` when the CLI binary itself never resolved (whether or not a
|
|
315
|
+
* credential was found — a credential with nothing to run it against is
|
|
316
|
+
* still "the binary is missing" from the caller's point of view); `no_auth`
|
|
317
|
+
* when the binary IS present but neither the cli-auth nor the key rung
|
|
318
|
+
* resolved. Mirrors `autoUnavailableReason`'s three-way branch below.
|
|
319
|
+
*/
|
|
320
|
+
function classifyStaticAbsentReason(binary: string | null): AbsentReason {
|
|
321
|
+
return binary === null ? 'no_binary' : 'no_auth';
|
|
322
|
+
}
|
|
323
|
+
|
|
223
324
|
/** The one-line reason `auto` prints when no rung resolved. */
|
|
224
325
|
function autoUnavailableReason(
|
|
225
326
|
provider: string,
|
|
@@ -298,6 +399,31 @@ export function isFallbackEligible(failure: CliFailureClass): boolean {
|
|
|
298
399
|
return FALLBACK_ELIGIBLE.has(failure);
|
|
299
400
|
}
|
|
300
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Maps a mid-flight `CliFailureClass` (a call was actually attempted and
|
|
404
|
+
* failed) onto the same `AbsentReason` enum a static resolution failure
|
|
405
|
+
* uses (road-to-always-on-orchestration Phase 3.2/3.4), so a pass artifact
|
|
406
|
+
* can bucket every absent member — whether it never got a transport at all
|
|
407
|
+
* or lost one partway through — under one vocabulary. Returns `null` for a
|
|
408
|
+
* failure class outside the four-value enum (`cli_unsupported`,
|
|
409
|
+
* `server_error`, `other`); a caller falls back to the raw failure detail
|
|
410
|
+
* for those rather than mis-classifying them.
|
|
411
|
+
*/
|
|
412
|
+
export function absentReasonFromCliFailure(failure: CliFailureClass): AbsentReason | null {
|
|
413
|
+
switch (failure) {
|
|
414
|
+
case 'binary_missing':
|
|
415
|
+
return 'no_binary';
|
|
416
|
+
case 'auth_rejected':
|
|
417
|
+
return 'no_auth';
|
|
418
|
+
case 'timeout':
|
|
419
|
+
return 'timeout';
|
|
420
|
+
case 'quota_exhausted':
|
|
421
|
+
return 'quota';
|
|
422
|
+
default:
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
301
427
|
/**
|
|
302
428
|
* One-shot, per-provider fallback ledger for a single invocation.
|
|
303
429
|
*
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/team` availability — CLI-first, not settings-gated
|
|
3
|
+
* (road-to-always-on-orchestration Phase 1, Step 1.3).
|
|
4
|
+
*
|
|
5
|
+
* `ai_team.enabled` — the former master switch — was DELETED. `/team`'s
|
|
6
|
+
* on/off state is now a FACT resolved from the machine, the same doctrine
|
|
7
|
+
* already applied to the subagent and council layers (Phase 1): the codex
|
|
8
|
+
* CLI binary must resolve on `$PATH`, AND some auth for the `openai`
|
|
9
|
+
* provider must be detectable (subscription login, API key, key file, or
|
|
10
|
+
* env key — the same transport-preference order `auto` walks via
|
|
11
|
+
* `strongestAuth`). Reuses the existing, read-only, zero-spend,
|
|
12
|
+
* per-process-cached detector (`_lib/environment_detector.ts`); this module
|
|
13
|
+
* adds NO new probing and NO new cache of its own.
|
|
14
|
+
*
|
|
15
|
+
* `ai_team.allow_delegate` is UNCHANGED by this module: a distinct,
|
|
16
|
+
* write-access authorization gate that stacks ON TOP of availability, never
|
|
17
|
+
* a replacement for it (`assert_delegate_allowed` in `team_dispatch.ts`
|
|
18
|
+
* still checks it separately, after availability).
|
|
19
|
+
*/
|
|
20
|
+
import { detectEnvironment, strongestAuth } from '../_lib/environment_detector.js';
|
|
21
|
+
import { load_agent_settings, type SettingsDict } from '../_lib/agent_settings.js';
|
|
22
|
+
import { _team_codex_binary_name, shutilWhich } from '../_cli/cmd_doctor.js';
|
|
23
|
+
|
|
24
|
+
/** Tool id `environment_detector` uses for the codex CLI (`install/toolDetection.ts`). */
|
|
25
|
+
const CODEX_HOST_ID = 'codex';
|
|
26
|
+
|
|
27
|
+
/** Provider id `environment_detector` / `PROVIDER_CLI_META` uses for the codex CLI. */
|
|
28
|
+
const CODEX_PROVIDER = 'openai';
|
|
29
|
+
|
|
30
|
+
/** The provider default `_team_codex_binary_name` falls back to with no override configured. */
|
|
31
|
+
const DEFAULT_CODEX_BINARY = 'codex';
|
|
32
|
+
|
|
33
|
+
export interface TeamAvailability {
|
|
34
|
+
readonly available: boolean;
|
|
35
|
+
/** One clear line for the user — populated only when `available` is false. */
|
|
36
|
+
readonly reason: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* codex CLI binary + auth presence — the availability half of the `/team`
|
|
41
|
+
* gate. Cached per process via `detectEnvironment()`'s own no-argument memo
|
|
42
|
+
* (`resetEnvironmentCache()` in tests); this function adds no caching of
|
|
43
|
+
* its own.
|
|
44
|
+
*
|
|
45
|
+
* m5 fix (independent-review finding) — `detectEnvironment()`'s host probe
|
|
46
|
+
* is hardcoded to the literal binary name `codex`; it has no way to be told
|
|
47
|
+
* "check this other name/path instead". So an operator who set
|
|
48
|
+
* `members.openai.binary:` in `.ai-council.yml` (honoured under an
|
|
49
|
+
* effective mode of `cli` OR `auto` — `cmd_doctor.ts::_mayRunOverCli`) was
|
|
50
|
+
* invisible to this check: `/team` could report "codex CLI not available"
|
|
51
|
+
* even with a working, custom-named/pathed binary configured and
|
|
52
|
+
* functional. `_team_codex_binary_name` (shared with `cmd_doctor.ts`'s own
|
|
53
|
+
* `council-cli` / `team` checks, so the override-resolution logic has one
|
|
54
|
+
* source) resolves the SAME name `cmd_doctor` would use; when it differs
|
|
55
|
+
* from the provider default, this probes that name directly instead of
|
|
56
|
+
* trusting the fixed-name host detector.
|
|
57
|
+
*/
|
|
58
|
+
export function checkCodexAvailability(project_root: string = process.cwd()): TeamAvailability {
|
|
59
|
+
const report = detectEnvironment();
|
|
60
|
+
const configuredBinary = _team_codex_binary_name(project_root);
|
|
61
|
+
const installed =
|
|
62
|
+
configuredBinary !== DEFAULT_CODEX_BINARY
|
|
63
|
+
? shutilWhich(configuredBinary) !== null
|
|
64
|
+
: (report.hosts.find((h) => h.id === CODEX_HOST_ID)?.installed ?? false);
|
|
65
|
+
if (!installed) {
|
|
66
|
+
return {
|
|
67
|
+
available: false,
|
|
68
|
+
reason:
|
|
69
|
+
'codex CLI not available — /team needs the codex CLI installed and ' +
|
|
70
|
+
'authenticated. Install: `npm install -g @openai/codex`, then run ' +
|
|
71
|
+
'`codex login`. Verify with `agent-config doctor --check team`.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (strongestAuth(report, CODEX_PROVIDER) === null) {
|
|
75
|
+
return {
|
|
76
|
+
available: false,
|
|
77
|
+
reason:
|
|
78
|
+
'codex CLI not available — no auth detected for it. Run `codex login` ' +
|
|
79
|
+
'(or set `OPENAI_API_KEY`). Verify with `agent-config doctor --check team`.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { available: true, reason: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `emergency.orchestration_halt` — the one audited incident switch over the
|
|
87
|
+
* always-on orchestration stack (subagents, council, TEAM). Reads the
|
|
88
|
+
* project settings cascade fresh (or an injected dict for tests) on every
|
|
89
|
+
* call — no cache of its own, so an armed halt takes effect on the very
|
|
90
|
+
* next read, same as every other `orchestration_halt` reader in the tree
|
|
91
|
+
* (`routing_doctor.ts`, `delegation_nudge_hook.ts`).
|
|
92
|
+
*/
|
|
93
|
+
export function isOrchestrationHalted(
|
|
94
|
+
settings?: SettingsDict | null,
|
|
95
|
+
cwd?: string | null,
|
|
96
|
+
): boolean {
|
|
97
|
+
const merged = settings ?? load_agent_settings({ cwd: cwd ?? process.cwd() });
|
|
98
|
+
const emergency = (merged as Record<string, unknown>)['emergency'];
|
|
99
|
+
if (typeof emergency !== 'object' || emergency === null || Array.isArray(emergency)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return (emergency as Record<string, unknown>)['orchestration_halt'] === true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The one-line halt notice — `/team` prints this verbatim when halted. */
|
|
106
|
+
export const ORCHESTRATION_HALT_MESSAGE =
|
|
107
|
+
'/team is halted (`emergency.orchestration_halt: true`) — the always-on ' +
|
|
108
|
+
'orchestration stack (subagents, council, team) is paused for incident ' +
|
|
109
|
+
'response. Resume: set `orchestration_halt: false` with a non-empty ' +
|
|
110
|
+
'`orchestration_halt_justification` in `.agent-settings.yml`.';
|