@event4u/agent-config 8.11.0 → 8.13.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 +1 -0
- package/CHANGELOG.md +64 -266
- package/README.md +2 -2
- package/dist/agent-src/commands/pr/create.md +26 -0
- package/dist/agent-src/commands/team/adversarial.md +96 -0
- package/dist/agent-src/commands/team/delegate.md +109 -0
- package/dist/agent-src/commands/team/review.md +84 -0
- package/dist/agent-src/commands/team/status.md +92 -0
- package/dist/agent-src/commands/team.md +147 -0
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/code-comment-discipline.md +10 -94
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/improve-before-implement.md +8 -65
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +159 -21
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +10 -5
- package/dist/install/install.mjs +38 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +7 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +31 -3
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +39 -0
- package/docs/catalog.md +10 -4
- package/docs/command-flows.md +6 -1
- package/docs/contracts/ai-council-config.md +9 -4
- package/docs/contracts/ai-team-config.md +191 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/featured-skills.md +1 -1
- package/docs/getting-started-by-role.md +3 -1
- package/docs/getting-started.md +31 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +86 -1
- package/docs/maintainers/branch-protection.md +105 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +15 -4
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +49 -0
- package/src/scripts/_cli/cmd_doctor.ts +291 -1
- package/src/scripts/_lib/claude_plugin.ts +39 -4
- package/src/scripts/_lib/knowledge_global.ts +44 -0
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/ai_council/orchestrator.ts +188 -1
- package/src/scripts/ai_team/config.ts +162 -0
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_proof.ts +9 -0
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/consumer_matrix.ts +251 -12
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/install.ts +39 -0
- package/src/scripts/knowledge_global_cli.ts +234 -45
- package/src/scripts/rule_backlinks.ts +350 -0
- package/src/server/schemas/settings.ts +17 -0
|
@@ -65,7 +65,8 @@ import {
|
|
|
65
65
|
synthesis_template,
|
|
66
66
|
system_prompt_for,
|
|
67
67
|
} from './prompts.js';
|
|
68
|
-
import {
|
|
68
|
+
import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
|
|
69
|
+
import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
|
|
69
70
|
|
|
70
71
|
// ── Python-format / stdlib parity helpers ────────────────────────────────
|
|
71
72
|
//
|
|
@@ -419,6 +420,14 @@ export interface ConsultOptions {
|
|
|
419
420
|
* round's prompt is byte-identical to today.
|
|
420
421
|
*/
|
|
421
422
|
stance_tally?: boolean;
|
|
423
|
+
/**
|
|
424
|
+
* Phase 1 stance-repair transport (2026-07-12 council policy): confirm per
|
|
425
|
+
* member in interactive runs; `null`/absent → a missing/unparseable stance
|
|
426
|
+
* stays a repair-marker in the tally, never an unplanned billable call.
|
|
427
|
+
*/
|
|
428
|
+
on_stance_repair?: ((member: string) => boolean) | null;
|
|
429
|
+
/** Receives each stance-repair response so the caller can account its cost. */
|
|
430
|
+
on_stance_repair_result?: ((r: CouncilResponse) => void) | null;
|
|
422
431
|
}
|
|
423
432
|
|
|
424
433
|
/**
|
|
@@ -515,6 +524,46 @@ export function consult(
|
|
|
515
524
|
}
|
|
516
525
|
}
|
|
517
526
|
|
|
527
|
+
// Phase 1 stance repair: one bounded stance-line-only re-prompt per member
|
|
528
|
+
// whose final reply lacks a parseable STANCE line — dispatched only through
|
|
529
|
+
// the on_stance_repair transport, reusing _run_round (spend gate + ledger +
|
|
530
|
+
// stamping unchanged). The repaired stance line is APPENDED to the member's
|
|
531
|
+
// final text so the deterministic tally can read it.
|
|
532
|
+
const on_stance_repair = opts.on_stance_repair ?? null;
|
|
533
|
+
if (stance_tally && on_stance_repair !== null) {
|
|
534
|
+
for (let i = 0; i < last_results.length; i++) {
|
|
535
|
+
const r = last_results[i] as CouncilResponse;
|
|
536
|
+
if (r.error !== null || r.text.trim() === '' || parse_stance_line(r.text) !== null) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const member = members[i];
|
|
540
|
+
if (member === undefined || !on_stance_repair(member.name)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const repairQ = new CouncilQuestion({
|
|
544
|
+
mode: question.mode,
|
|
545
|
+
user_prompt:
|
|
546
|
+
`Your reply is missing the mandatory closing stance line. ` +
|
|
547
|
+
`Reply with ONLY that single line for your final position.\n\n${STANCE_LINE_CONTRACT}`,
|
|
548
|
+
max_tokens: question.max_tokens,
|
|
549
|
+
});
|
|
550
|
+
const rr = _run_round([member], repairQ, resolvedBudget, spent, {
|
|
551
|
+
table,
|
|
552
|
+
on_overrun,
|
|
553
|
+
project,
|
|
554
|
+
original_ask,
|
|
555
|
+
advisor_plans,
|
|
556
|
+
});
|
|
557
|
+
const rep = rr[0];
|
|
558
|
+
if (rep !== undefined) {
|
|
559
|
+
opts.on_stance_repair_result?.(rep);
|
|
560
|
+
if (rep.error === null && parse_stance_line(rep.text) !== null) {
|
|
561
|
+
r.text = `${r.text.trim()}\n\n${rep.text.trim()}`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
518
567
|
return last_results;
|
|
519
568
|
}
|
|
520
569
|
|
|
@@ -895,6 +944,90 @@ function _augment_for_debate_round(
|
|
|
895
944
|
);
|
|
896
945
|
}
|
|
897
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Phase 3 post-round enforcement (2026-07-12 council policy): deterministic
|
|
949
|
+
* detectors flag members whose round added nothing (novelty near-duplicate of
|
|
950
|
+
* their own prior round) or when the round collapsed toward agreement (dissent
|
|
951
|
+
* quota unmet → the most-recently-converged member). At most ONE bounded repair
|
|
952
|
+
* re-prompt per member per round, dispatched only through the `on_repair`
|
|
953
|
+
* transport (confirm-interactive / auto-fire under --auto-continue); the repair
|
|
954
|
+
* call reuses `_run_round`, so the projected-spend gate, ledger, and metadata
|
|
955
|
+
* stamping apply unchanged. A successful repair REPLACES the member's entry so
|
|
956
|
+
* downstream rounds/persistence see the repaired reply.
|
|
957
|
+
*/
|
|
958
|
+
function _run_debate_gate_repairs(
|
|
959
|
+
members: ExternalAIClient[],
|
|
960
|
+
results: CouncilResponse[],
|
|
961
|
+
prior: CouncilResponse[],
|
|
962
|
+
round_question: CouncilQuestion,
|
|
963
|
+
budget: CostBudget,
|
|
964
|
+
spent: Spent,
|
|
965
|
+
round_opts: {
|
|
966
|
+
table: PriceTable | null;
|
|
967
|
+
on_overrun: OnOverrunCallback | null;
|
|
968
|
+
project: ProjectContext | null;
|
|
969
|
+
original_ask: string;
|
|
970
|
+
advisor_plans: Map<string, AdvisorPlan> | null;
|
|
971
|
+
},
|
|
972
|
+
on_repair: ((member: string, reason: string) => boolean) | null,
|
|
973
|
+
): CouncilResponse[] {
|
|
974
|
+
const flagged: Array<{ idx: number; reason: string }> = [];
|
|
975
|
+
const okText = (r: CouncilResponse | undefined): string =>
|
|
976
|
+
r !== undefined && r.error === null ? r.text : '';
|
|
977
|
+
for (let i = 0; i < results.length; i++) {
|
|
978
|
+
const curr = okText(results[i]);
|
|
979
|
+
const prev = okText(prior[i]);
|
|
980
|
+
if (curr !== '' && prev !== '' && is_near_duplicate(prev, curr)) {
|
|
981
|
+
flagged.push({ idx: i, reason: 'novelty: near-duplicate of your own prior round' });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const texts = results.map((r) => okText(r));
|
|
985
|
+
if (!dissent_quota_met(texts.filter((t) => t !== ''))) {
|
|
986
|
+
// Most-recently-converged heuristic: the LAST member with a real reply
|
|
987
|
+
// that carries no objection marker.
|
|
988
|
+
for (let i = results.length - 1; i >= 0; i--) {
|
|
989
|
+
const t = okText(results[i]);
|
|
990
|
+
if (t !== '' && count_dissenters([t]) === 0 && !flagged.some((f) => f.idx === i)) {
|
|
991
|
+
flagged.push({ idx: i, reason: 'dissent quota unmet: state a genuine objection or confirm with a named reason' });
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (flagged.length === 0 || on_repair === null) {
|
|
997
|
+
return results;
|
|
998
|
+
}
|
|
999
|
+
const repaired = new Set<number>();
|
|
1000
|
+
const out = [...results];
|
|
1001
|
+
for (const f of flagged) {
|
|
1002
|
+
if (repaired.has(f.idx)) {
|
|
1003
|
+
continue; // hard cap: <= 1 repair per member per round
|
|
1004
|
+
}
|
|
1005
|
+
const member = members[f.idx];
|
|
1006
|
+
if (member === undefined) {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (!on_repair(member.name, f.reason)) {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
repaired.add(f.idx);
|
|
1013
|
+
const repairQ = new CouncilQuestion({
|
|
1014
|
+
mode: round_question.mode,
|
|
1015
|
+
user_prompt:
|
|
1016
|
+
`${round_question.user_prompt}\n\n---\n\n` +
|
|
1017
|
+
`REPAIR RE-PROMPT (${f.reason}). Your previous reply this round did not ` +
|
|
1018
|
+
`advance the debate. Respond again: either a genuinely updated position ` +
|
|
1019
|
+
`naming the specific flaw that moved you, or an explicit defense with NEW evidence.`,
|
|
1020
|
+
max_tokens: round_question.max_tokens,
|
|
1021
|
+
});
|
|
1022
|
+
const rr = _run_round([member], repairQ, budget, spent, round_opts);
|
|
1023
|
+
const r0 = rr[0];
|
|
1024
|
+
if (r0 !== undefined && r0.error === null && r0.text.trim() !== '') {
|
|
1025
|
+
out[f.idx] = r0;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
898
1031
|
export interface RunDebateOptions {
|
|
899
1032
|
budget?: CostBudget | null;
|
|
900
1033
|
table?: PriceTable | null;
|
|
@@ -914,6 +1047,22 @@ export interface RunDebateOptions {
|
|
|
914
1047
|
* prompt is byte-identical to today.
|
|
915
1048
|
*/
|
|
916
1049
|
debate_gates?: boolean;
|
|
1050
|
+
/**
|
|
1051
|
+
* Phase 3 repair transport, per the 2026-07-12 council policy
|
|
1052
|
+
* (`debate_gates.repair_action`): the CLI passes `() => true` under
|
|
1053
|
+
* `--auto-continue` (auto-fire under the cap) or an interactive one-line
|
|
1054
|
+
* confirm otherwise. `null`/absent → gates detect but never dispatch
|
|
1055
|
+
* (no unplanned spend without an explicit transport).
|
|
1056
|
+
*/
|
|
1057
|
+
on_repair?: ((member: string, reason: string) => boolean) | null;
|
|
1058
|
+
/**
|
|
1059
|
+
* Phase 3 restate pass (`ai_council.restate.enabled` / `--restate`): a
|
|
1060
|
+
* pre-round-1 billable call per member collecting a <=50-word restatement
|
|
1061
|
+
* + one alternative framing. Default off -> no extra calls, byte-identical.
|
|
1062
|
+
*/
|
|
1063
|
+
restate?: boolean;
|
|
1064
|
+
/** Receives the restate responses (rendering, divergence flags, cost). */
|
|
1065
|
+
on_restate?: ((responses: CouncilResponse[]) => void) | null;
|
|
917
1066
|
}
|
|
918
1067
|
|
|
919
1068
|
/**
|
|
@@ -975,6 +1124,27 @@ export function run_debate(
|
|
|
975
1124
|
const all_rounds: CouncilResponse[][] = [];
|
|
976
1125
|
let current_user_prompt = question.user_prompt;
|
|
977
1126
|
|
|
1127
|
+
// Phase 3 restate: one pre-round-1 call per member. Runs BEFORE any debate
|
|
1128
|
+
// spend so a diverging restatement can be flagged before round-2 cost.
|
|
1129
|
+
if (opts.restate === true) {
|
|
1130
|
+
const restateQ = new CouncilQuestion({
|
|
1131
|
+
mode: question.mode,
|
|
1132
|
+
user_prompt:
|
|
1133
|
+
`Before the debate begins: RESTATE the question below in at most 50 words, ` +
|
|
1134
|
+
`then offer ONE alternative framing of it. Do not argue a position yet.\n\n---\n\n` +
|
|
1135
|
+
`${question.user_prompt}`,
|
|
1136
|
+
max_tokens: question.max_tokens,
|
|
1137
|
+
});
|
|
1138
|
+
const restated = _run_round(members, restateQ, budget, spent, {
|
|
1139
|
+
table,
|
|
1140
|
+
on_overrun,
|
|
1141
|
+
project,
|
|
1142
|
+
original_ask,
|
|
1143
|
+
advisor_plans,
|
|
1144
|
+
});
|
|
1145
|
+
opts.on_restate?.(restated);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
978
1148
|
for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
|
|
979
1149
|
const round_number = round_idx + 1;
|
|
980
1150
|
let results: CouncilResponse[];
|
|
@@ -1000,6 +1170,23 @@ export function run_debate(
|
|
|
1000
1170
|
});
|
|
1001
1171
|
}
|
|
1002
1172
|
|
|
1173
|
+
if (opts.debate_gates === true && round_idx > 0 && all_rounds.length > 0) {
|
|
1174
|
+
results = _run_debate_gate_repairs(
|
|
1175
|
+
members,
|
|
1176
|
+
results,
|
|
1177
|
+
all_rounds[all_rounds.length - 1] as CouncilResponse[],
|
|
1178
|
+
// round_idx > 0 here, so current_user_prompt IS this round's prompt.
|
|
1179
|
+
new CouncilQuestion({
|
|
1180
|
+
mode: question.mode,
|
|
1181
|
+
user_prompt: current_user_prompt,
|
|
1182
|
+
max_tokens: question.max_tokens,
|
|
1183
|
+
}),
|
|
1184
|
+
budget,
|
|
1185
|
+
spent,
|
|
1186
|
+
{ table, on_overrun, project, original_ask, advisor_plans },
|
|
1187
|
+
opts.on_repair ?? null,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1003
1190
|
all_rounds.push(results);
|
|
1004
1191
|
if (on_round_complete !== null) {
|
|
1005
1192
|
on_round_complete(round_number, results);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai_team configuration loader — single source of truth for the `/team`
|
|
3
|
+
* cross-model review family.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the fail-closed validation posture of the council loader
|
|
6
|
+
* (`src/scripts/ai_council/config.ts`) at a fraction of its surface: the
|
|
7
|
+
* `ai_team` block has five flat keys and no nested sub-schemas, so this
|
|
8
|
+
* loader is deliberately lean — defaults, per-key type checks, and hard
|
|
9
|
+
* unknown-key rejection. Contract: `docs/contracts/ai-team-config.md`.
|
|
10
|
+
*
|
|
11
|
+
* Unlike the council (ALWAYS user-global `.ai-council.yml`, ADR-104),
|
|
12
|
+
* `ai_team` lives in the PROJECT settings cascade (`.agent-settings.yml`
|
|
13
|
+
* via `load_agent_settings`) — team mode is a per-project posture
|
|
14
|
+
* (default-off), not a per-developer credential store; the codex CLI
|
|
15
|
+
* carries the subscription auth itself.
|
|
16
|
+
*
|
|
17
|
+
* Validation contract (all enforced at load time, fail-closed):
|
|
18
|
+
*
|
|
19
|
+
* 1. `ai_team` absent → defaults (feature off). Non-mapping → error.
|
|
20
|
+
* 2. Unknown keys under `ai_team` are rejected — a typo must never
|
|
21
|
+
* silently disable a gate (`allow_delegate` misspelled = delegation
|
|
22
|
+
* stays off AND the load fails loudly).
|
|
23
|
+
* 3. `enabled`, `allow_delegate`, `suppress_setup_hint` are booleans.
|
|
24
|
+
* 4. `model` is a non-empty string. `'auto'` = pass no `--model` flag
|
|
25
|
+
* (the codex CLI default applies); any other value passes through
|
|
26
|
+
* verbatim.
|
|
27
|
+
* 5. `max_calls_per_day` is a non-negative integer (bools rejected).
|
|
28
|
+
*/
|
|
29
|
+
import { load_agent_settings, type SettingsDict } from '../_lib/agent_settings.js';
|
|
30
|
+
|
|
31
|
+
/** Raised when the `ai_team` block of `.agent-settings.yml` violates the schema. */
|
|
32
|
+
export class TeamConfigError extends Error {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'TeamConfigError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AiTeamConfig {
|
|
40
|
+
readonly enabled: boolean;
|
|
41
|
+
readonly model: string;
|
|
42
|
+
readonly allow_delegate: boolean;
|
|
43
|
+
readonly max_calls_per_day: number;
|
|
44
|
+
readonly suppress_setup_hint: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Shipped defaults — byte-parity with `src/config/agent-settings.template.yml`. */
|
|
48
|
+
export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
|
|
49
|
+
enabled: false,
|
|
50
|
+
model: 'auto',
|
|
51
|
+
allow_delegate: false,
|
|
52
|
+
max_calls_per_day: 50,
|
|
53
|
+
suppress_setup_hint: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sentinel: `model: 'auto'` means "pass NO --model flag" so the codex
|
|
58
|
+
* CLI's own default applies (tracks the subscription's strongest model
|
|
59
|
+
* instead of pinning a stale ID).
|
|
60
|
+
*/
|
|
61
|
+
export const AI_TEAM_MODEL_AUTO = 'auto';
|
|
62
|
+
|
|
63
|
+
const _KNOWN_KEYS: ReadonlySet<string> = new Set([
|
|
64
|
+
'enabled',
|
|
65
|
+
'model',
|
|
66
|
+
'allow_delegate',
|
|
67
|
+
'max_calls_per_day',
|
|
68
|
+
'suppress_setup_hint',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function _isDict(value: unknown): value is Record<string, unknown> {
|
|
72
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _isBool(value: unknown): value is boolean {
|
|
76
|
+
return typeof value === 'boolean';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _requireBool(raw: Record<string, unknown>, key: string, fallback: boolean): boolean {
|
|
80
|
+
if (!(key in raw)) {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
const value = raw[key];
|
|
84
|
+
if (!_isBool(value)) {
|
|
85
|
+
throw new TeamConfigError(
|
|
86
|
+
`\`ai_team.${key}\` must be a boolean (got ${JSON.stringify(value)}).`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a raw `ai_team` mapping and return the typed config.
|
|
94
|
+
*
|
|
95
|
+
* `raw = null | undefined` (block absent) returns the defaults — the
|
|
96
|
+
* feature is off, never an error. Every other shape is validated
|
|
97
|
+
* fail-closed per the module contract above.
|
|
98
|
+
*/
|
|
99
|
+
export function build_ai_team_config(raw: unknown): AiTeamConfig {
|
|
100
|
+
if (raw === null || raw === undefined) {
|
|
101
|
+
return AI_TEAM_DEFAULTS;
|
|
102
|
+
}
|
|
103
|
+
if (!_isDict(raw)) {
|
|
104
|
+
throw new TeamConfigError('`ai_team` must be a mapping.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const key of Object.keys(raw)) {
|
|
108
|
+
if (!_KNOWN_KEYS.has(key)) {
|
|
109
|
+
throw new TeamConfigError(
|
|
110
|
+
`ai_team.${key}: unknown key; valid: ` +
|
|
111
|
+
`[${[..._KNOWN_KEYS].map((k) => `'${k}'`).join(', ')}].`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const enabled = _requireBool(raw, 'enabled', AI_TEAM_DEFAULTS.enabled);
|
|
117
|
+
const allow_delegate = _requireBool(raw, 'allow_delegate', AI_TEAM_DEFAULTS.allow_delegate);
|
|
118
|
+
const suppress_setup_hint = _requireBool(
|
|
119
|
+
raw,
|
|
120
|
+
'suppress_setup_hint',
|
|
121
|
+
AI_TEAM_DEFAULTS.suppress_setup_hint,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let model = AI_TEAM_DEFAULTS.model;
|
|
125
|
+
if ('model' in raw) {
|
|
126
|
+
const value = raw['model'];
|
|
127
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
128
|
+
throw new TeamConfigError(
|
|
129
|
+
`\`ai_team.model\` must be a non-empty string (got ${JSON.stringify(value)}).`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
model = value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let max_calls_per_day = AI_TEAM_DEFAULTS.max_calls_per_day;
|
|
136
|
+
if ('max_calls_per_day' in raw) {
|
|
137
|
+
const value = raw['max_calls_per_day'];
|
|
138
|
+
if (_isBool(value) || typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
139
|
+
throw new TeamConfigError(
|
|
140
|
+
'`ai_team.max_calls_per_day` must be a non-negative integer ' +
|
|
141
|
+
`(got ${JSON.stringify(value)}).`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
max_calls_per_day = value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { enabled, model, allow_delegate, max_calls_per_day, suppress_setup_hint };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Load the `ai_team` config from the merged project settings cascade.
|
|
152
|
+
*
|
|
153
|
+
* `opts.settings` injects a pre-merged settings dict (test seam — no
|
|
154
|
+
* filesystem access); otherwise `load_agent_settings` walks the normal
|
|
155
|
+
* `.agent-settings.yml` cascade from `opts.cwd` (default: process cwd).
|
|
156
|
+
*/
|
|
157
|
+
export function load_ai_team_config(
|
|
158
|
+
opts: { settings?: SettingsDict | null; cwd?: string | null } = {},
|
|
159
|
+
): AiTeamConfig {
|
|
160
|
+
const settings = opts.settings ?? load_agent_settings({ cwd: opts.cwd ?? process.cwd() });
|
|
161
|
+
return build_ai_team_config((settings as Record<string, unknown>)['ai_team']);
|
|
162
|
+
}
|