@event4u/agent-config 7.0.1 → 7.1.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 +8 -1
- package/CHANGELOG.md +99 -0
- package/README.md +4 -2
- package/dist/agent-src/commands/council/default.md +28 -20
- package/dist/agent-src/commands/evals/fix.json +5 -1
- package/dist/agent-src/commands/fix/comments.md +101 -0
- package/dist/agent-src/commands/fix.md +5 -2
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +65 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +68 -0
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +68 -0
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +62 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +59 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +8 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +56 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +57 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +71 -0
- package/dist/agent-src/contexts/execution/verify-budget.md +50 -0
- package/dist/agent-src/rules/delegation-policy.md +84 -0
- package/dist/agent-src/rules/design-fidelity.md +98 -0
- package/dist/agent-src/rules/domain-safety-pii.md +22 -0
- package/dist/agent-src/rules/legal-safety-floor.md +144 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +25 -22
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +102 -15
- package/dist/agent-src/scripts/update_roadmap_progress.ts +52 -7
- package/dist/agent-src/skills/ai-council/SKILL.md +28 -1
- package/dist/agent-src/skills/contract-review/SKILL.md +124 -0
- package/dist/agent-src/skills/contract-review/evals/triggers.json +17 -0
- package/dist/agent-src/skills/dpa-review/SKILL.md +129 -0
- package/dist/agent-src/skills/dpa-review/evals/triggers.json +17 -0
- package/dist/agent-src/skills/laravel/SKILL.md +1 -0
- package/dist/agent-src/skills/legal-intake-triage/SKILL.md +127 -0
- package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +17 -0
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +100 -0
- package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +17 -0
- package/dist/agent-src/skills/nda-triage/SKILL.md +113 -0
- package/dist/agent-src/skills/nda-triage/evals/triggers.json +17 -0
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +36 -9
- package/dist/agent-src/skills/recursive-verification/SKILL.md +186 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +17 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -0
- package/dist/agent-src/templates/agent-settings.md +64 -19
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +278 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +12 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +50 -8
- package/dist/discovery/trust-report.md +7 -5
- package/dist/discovery/workspaces.json +34 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +14 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/architecture.md +4 -4
- package/docs/benchmark.md +46 -0
- package/docs/catalog.md +18 -6
- package/docs/command-flows.md +7 -1
- package/docs/contracts/ai-council-config.md +17 -10
- package/docs/contracts/audit-log-v1.md +7 -0
- package/docs/contracts/auto-orchestration-v1.md +70 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/discovery-manifest.schema.json +2 -2
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +19 -0
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +13 -6
- package/docs/decisions/ADR-049-configuration-trust-boundary.md +11 -1
- package/docs/decisions/ADR-093-ai-council-config-user-global.md +10 -4
- package/docs/decisions/ADR-104-ai-council-config-global-only.md +119 -0
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +102 -0
- package/docs/decisions/ADR-106-recursive-verification-benchmark-gate.md +126 -0
- package/docs/decisions/ADR-107-legal-domain-pack-adoption.md +51 -0
- package/docs/decisions/ADR-108-open-source-forever-no-commercial-tier.md +46 -0
- package/docs/decisions/INDEX.md +5 -0
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
- package/docs/guidelines/agent-infra/domain-pack-architecture.md +55 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +72 -3
- package/src/config/discovery/packs.yml +13 -0
- package/src/config/discovery/workspaces.yml +7 -0
- package/src/scripts/_lib/auto_dispatch.ts +95 -0
- package/src/scripts/_lib/host_capability.ts +59 -0
- package/src/scripts/_lib/orchestration_gate.ts +98 -0
- package/src/scripts/_lib/subagent_bundle.ts +154 -0
- package/src/scripts/_lib/subagent_response.ts +82 -0
- package/src/scripts/_lib/subagent_routing.ts +81 -0
- package/src/scripts/_lib/subagent_spawn.ts +97 -0
- package/src/scripts/_lib/subagent_steering.ts +64 -0
- package/src/scripts/_lib/verify_budget.ts +54 -0
- package/src/scripts/ai_council/advisors.ts +4 -2
- package/src/scripts/ai_council/clients.ts +7 -1
- package/src/scripts/ai_council/config.ts +22 -17
- package/src/scripts/bench_ab_v2_run.ts +127 -0
- package/src/scripts/bench_ab_v2_stats.ts +39 -0
- package/src/scripts/check_council_config_location.ts +6 -5
- package/src/scripts/lint_discovery_vocabulary.ts +6 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +21 -0
- package/src/scripts/lint_legal_pack.ts +139 -0
- package/src/scripts/probe_projection_fidelity.ts +7 -1
- package/src/scripts/rdp_gate_classify.ts +131 -0
- package/src/scripts/rdp_quality_eval.ts +437 -0
- package/src/scripts/schemas/agent-settings.schema.json +11 -0
- package/src/scripts/skill_linter.ts +3 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent bundle resolver (Phase 2 / A2).
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. Maps a task slice to a concrete subagent bundle by selecting
|
|
5
|
+
* from the EXISTING surfaces — role-modes, the `judge-*` review-lens skills,
|
|
6
|
+
* personas, knowledge-card refs, and an ADR-035 tier — instead of a new parallel
|
|
7
|
+
* registry. Feeds `composeSpawnBrief` (subagent_spawn.ts) real values.
|
|
8
|
+
*
|
|
9
|
+
* Contract: `src/agent-src/contexts/execution/subagent-spawn-contract.md`.
|
|
10
|
+
*
|
|
11
|
+
* ADR-100 guard: proprietary knowledge-card refs never enter a cross-project
|
|
12
|
+
* subagent bundle. The resolver drops them and records the drop.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { RoleMode, SpawnSelection } from './subagent_spawn.js';
|
|
16
|
+
|
|
17
|
+
export type Tier = 'lite' | 'medium' | 'high' | 'inherit';
|
|
18
|
+
|
|
19
|
+
/** The well-known delegable slice kinds and their lens/role/tier mapping. */
|
|
20
|
+
export type SliceKind =
|
|
21
|
+
| 'review'
|
|
22
|
+
| 'security'
|
|
23
|
+
| 'tests'
|
|
24
|
+
| 'bug-hunt'
|
|
25
|
+
| 'research'
|
|
26
|
+
| 'docs'
|
|
27
|
+
| 'refactor'
|
|
28
|
+
| 'plan';
|
|
29
|
+
|
|
30
|
+
interface KindProfile {
|
|
31
|
+
/** Reuse a `judge-*` skill as a ready-made subagent role-profile, or null. */
|
|
32
|
+
judge_lens: string | null;
|
|
33
|
+
role_mode: RoleMode | null;
|
|
34
|
+
tier: Tier;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Slice-kind → bundle profile. The `judge-*` skills are reused as subagent
|
|
39
|
+
* role-profiles (the package already ships them as review lenses).
|
|
40
|
+
*/
|
|
41
|
+
const KIND_PROFILE: Record<SliceKind, KindProfile> = {
|
|
42
|
+
review: { judge_lens: 'judge-code-quality', role_mode: 'reviewer', tier: 'medium' },
|
|
43
|
+
security: { judge_lens: 'judge-security-auditor', role_mode: 'reviewer', tier: 'high' },
|
|
44
|
+
tests: { judge_lens: 'judge-test-coverage', role_mode: 'tester', tier: 'medium' },
|
|
45
|
+
'bug-hunt': { judge_lens: 'judge-bug-hunter', role_mode: 'developer', tier: 'medium' },
|
|
46
|
+
research: { judge_lens: null, role_mode: null, tier: 'lite' },
|
|
47
|
+
docs: { judge_lens: null, role_mode: null, tier: 'lite' },
|
|
48
|
+
refactor: { judge_lens: null, role_mode: 'developer', tier: 'high' },
|
|
49
|
+
plan: { judge_lens: null, role_mode: 'planner', tier: 'high' },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** A knowledge-card ref tagged with its sharing tier (ADR-100). */
|
|
53
|
+
export interface KnowledgeRef {
|
|
54
|
+
ref: string;
|
|
55
|
+
tier: 'public' | 'vendor' | 'proprietary';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SliceInput {
|
|
59
|
+
kind: SliceKind;
|
|
60
|
+
task: string;
|
|
61
|
+
/** Active profile id (e.g. 'developer'), inherited from the session unless overridden. */
|
|
62
|
+
profile?: string | null;
|
|
63
|
+
/** Persona ids cited by the task's skill. */
|
|
64
|
+
personas?: string[];
|
|
65
|
+
/** Candidate knowledge-card refs (tier-tagged) for this slice. */
|
|
66
|
+
knowledge?: KnowledgeRef[];
|
|
67
|
+
/** Will this bundle be visible across project boundaries? */
|
|
68
|
+
cross_project?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ResolvedBundle {
|
|
72
|
+
selection: SpawnSelection;
|
|
73
|
+
judge_lens: string | null;
|
|
74
|
+
tier: Tier;
|
|
75
|
+
dropped_proprietary: number;
|
|
76
|
+
reason: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ADR-100 guard: keep refs whose tier is shareable in the bundle's visibility
|
|
81
|
+
* scope. In a cross-project bundle, `proprietary` refs are dropped.
|
|
82
|
+
*/
|
|
83
|
+
export function filterKnowledgeByPolicy(
|
|
84
|
+
knowledge: KnowledgeRef[],
|
|
85
|
+
cross_project: boolean,
|
|
86
|
+
): { kept: string[]; dropped: number } {
|
|
87
|
+
const kept: string[] = [];
|
|
88
|
+
let dropped = 0;
|
|
89
|
+
for (const k of knowledge) {
|
|
90
|
+
if (cross_project && k.tier === 'proprietary') {
|
|
91
|
+
dropped += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
kept.push(k.ref);
|
|
95
|
+
}
|
|
96
|
+
return { kept, dropped };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a task slice into a subagent bundle: role-mode + reused judge lens +
|
|
101
|
+
* personas + ADR-100-filtered knowledge refs + tier. The result's `selection`
|
|
102
|
+
* is ready for `composeSpawnBrief`.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveBundle(slice: SliceInput): ResolvedBundle {
|
|
105
|
+
const profile = KIND_PROFILE[slice.kind];
|
|
106
|
+
const cross = slice.cross_project === true;
|
|
107
|
+
const { kept, dropped } = filterKnowledgeByPolicy(slice.knowledge ?? [], cross);
|
|
108
|
+
|
|
109
|
+
// The reused judge lens rides as a persona-equivalent so the subagent loads it.
|
|
110
|
+
const personas = [...(slice.personas ?? [])];
|
|
111
|
+
if (profile.judge_lens && !personas.includes(profile.judge_lens)) {
|
|
112
|
+
personas.unshift(profile.judge_lens);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const selection: SpawnSelection = {
|
|
116
|
+
task: slice.task,
|
|
117
|
+
role_mode: profile.role_mode,
|
|
118
|
+
profile: slice.profile ?? null,
|
|
119
|
+
personas,
|
|
120
|
+
knowledge_refs: kept,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const reasonParts = [`kind=${slice.kind}`, `tier=${profile.tier}`];
|
|
124
|
+
if (profile.judge_lens) reasonParts.push(`lens=${profile.judge_lens}`);
|
|
125
|
+
if (dropped > 0) reasonParts.push(`dropped ${dropped} proprietary (cross-project)`);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
selection,
|
|
129
|
+
judge_lens: profile.judge_lens,
|
|
130
|
+
tier: profile.tier,
|
|
131
|
+
dropped_proprietary: dropped,
|
|
132
|
+
reason: reasonParts.join(' · '),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Auditable bundle signature for the orchestration-telemetry object — counts +
|
|
138
|
+
* ids only, never bodies (preserves the audit-log privacy floor).
|
|
139
|
+
*/
|
|
140
|
+
export function bundleAuditLine(b: ResolvedBundle): {
|
|
141
|
+
role_mode: RoleMode | null;
|
|
142
|
+
judge_lens: string | null;
|
|
143
|
+
tier: Tier;
|
|
144
|
+
knowledge_ref_count: number;
|
|
145
|
+
dropped_proprietary: number;
|
|
146
|
+
} {
|
|
147
|
+
return {
|
|
148
|
+
role_mode: b.selection.role_mode ?? null,
|
|
149
|
+
judge_lens: b.judge_lens,
|
|
150
|
+
tier: b.tier,
|
|
151
|
+
knowledge_ref_count: b.selection.knowledge_refs?.length ?? 0,
|
|
152
|
+
dropped_proprietary: b.dropped_proprietary,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured subagent-response envelope + synthesis helpers (Phase 3 / A3).
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. Pins the body a subagent returns inside the 4-status envelope
|
|
5
|
+
* (the status stays the envelope; this is the body) and gives the orchestrator
|
|
6
|
+
* the synthesis duties: surface evidence gaps, and force the full cross-model
|
|
7
|
+
* judge when a mutating finding is low-confidence.
|
|
8
|
+
*
|
|
9
|
+
* Contract: `src/agent-src/contexts/execution/subagent-response-contract.md`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type Confidence = 'low' | 'medium' | 'high';
|
|
13
|
+
|
|
14
|
+
export interface Finding {
|
|
15
|
+
title: string;
|
|
16
|
+
/** Refs (file:line / id / path) backing this finding — never inline bodies. */
|
|
17
|
+
evidence_refs?: string[];
|
|
18
|
+
/** Does acting on this finding mutate files / state? */
|
|
19
|
+
mutating?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubagentResponse {
|
|
23
|
+
summary: string;
|
|
24
|
+
findings: Finding[];
|
|
25
|
+
risks: string[];
|
|
26
|
+
confidence: Confidence;
|
|
27
|
+
handoff: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CONFIDENCE: ReadonlySet<string> = new Set<Confidence>(['low', 'medium', 'high']);
|
|
31
|
+
|
|
32
|
+
export interface ValidationResult {
|
|
33
|
+
valid: boolean;
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Validate the envelope shape. Counts/refs only — reject inline-body evidence. */
|
|
38
|
+
export function validateResponse(input: unknown): ValidationResult {
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
if (input === null || typeof input !== 'object') {
|
|
41
|
+
return { valid: false, errors: ['response is not an object'] };
|
|
42
|
+
}
|
|
43
|
+
const r = input as Record<string, unknown>;
|
|
44
|
+
if (typeof r.summary !== 'string') errors.push('summary missing or not a string');
|
|
45
|
+
if (typeof r.handoff !== 'string') errors.push('handoff missing or not a string');
|
|
46
|
+
if (!CONFIDENCE.has(r.confidence as string)) errors.push(`confidence must be low|medium|high (got ${String(r.confidence)})`);
|
|
47
|
+
if (!Array.isArray(r.findings)) errors.push('findings missing or not an array');
|
|
48
|
+
if (!Array.isArray(r.risks)) errors.push('risks missing or not an array');
|
|
49
|
+
if (Array.isArray(r.findings)) {
|
|
50
|
+
for (const [i, f] of (r.findings as Finding[]).entries()) {
|
|
51
|
+
if (!f || typeof f.title !== 'string') errors.push(`finding[${i}] missing title`);
|
|
52
|
+
const refs = f?.evidence_refs;
|
|
53
|
+
if (refs && refs.some((x) => typeof x !== 'string' || x.includes('\n'))) {
|
|
54
|
+
errors.push(`finding[${i}] evidence_refs must be ref tokens, not bodies`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { valid: errors.length === 0, errors };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Synthesis gaps the orchestrator must resolve before adopting the response:
|
|
63
|
+
* a finding with no evidence ref is an unbacked claim to re-check.
|
|
64
|
+
*/
|
|
65
|
+
export function synthesisGaps(response: SubagentResponse): string[] {
|
|
66
|
+
const gaps: string[] = [];
|
|
67
|
+
for (const f of response.findings) {
|
|
68
|
+
if (!f.evidence_refs || f.evidence_refs.length === 0) {
|
|
69
|
+
gaps.push(`finding "${f.title}" has no evidence — re-check before adopting`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return gaps;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Confidence → verify-budget link. A mutating finding returned at `low`
|
|
77
|
+
* confidence forces the full cross-model judge (no deterministic-only pass).
|
|
78
|
+
* Returns true when the full judge is mandatory for this finding.
|
|
79
|
+
*/
|
|
80
|
+
export function forcesJudge(f: Finding, confidence: Confidence): boolean {
|
|
81
|
+
return f.mutating === true && confidence === 'low';
|
|
82
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent routing (Phase 2) — cost/speed downshift + optional quota arbitrage.
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O, VENDOR-NEUTRAL. Resolves which model a delegated sub-task runs
|
|
5
|
+
* on, from the sub-task's declared capability tier, the `subagents.*` settings,
|
|
6
|
+
* and the host-capability manifest. Contract:
|
|
7
|
+
* `src/agent-src/contexts/execution/subagent-routing.md`.
|
|
8
|
+
*
|
|
9
|
+
* Invariants:
|
|
10
|
+
* - The orchestrator stays on the session/high tier (this resolves SUB-tasks).
|
|
11
|
+
* - Downshift off → every sub-task runs on the session tier.
|
|
12
|
+
* - Downshift on → a sub-task runs on the lowest-capable tier it declares.
|
|
13
|
+
* - Quota arbitrage is an OPTIONAL bonus: it only prefers the separate pool
|
|
14
|
+
* when BOTH the setting and the host manifest allow it. Where unsupported,
|
|
15
|
+
* routing is identical minus the quota win — never a hard-coded vendor model.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type Tier = 'lite' | 'medium' | 'high' | 'inherit';
|
|
19
|
+
export type QuotaPool = 'separate' | 'shared';
|
|
20
|
+
|
|
21
|
+
/** Per-tier model alias overrides (empty string = use the tier's runtime default). */
|
|
22
|
+
export interface ModelMap {
|
|
23
|
+
lite?: string;
|
|
24
|
+
medium?: string;
|
|
25
|
+
high?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RoutingInputs {
|
|
29
|
+
/** The sub-task's declared capability tier. */
|
|
30
|
+
task_tier: Tier;
|
|
31
|
+
/** The orchestrator's session tier (used when downshift is off / tier is inherit). */
|
|
32
|
+
session_tier: Tier;
|
|
33
|
+
downshift: boolean;
|
|
34
|
+
quota_arbitrage: boolean;
|
|
35
|
+
model_map: ModelMap;
|
|
36
|
+
/** From the host-capability manifest. */
|
|
37
|
+
separate_quota_pool: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RoutingDecision {
|
|
41
|
+
/** Resolved tier the sub-task runs on. */
|
|
42
|
+
tier: Tier;
|
|
43
|
+
/** Model alias, or '' meaning "use the tier's runtime default" (no vendor name baked in). */
|
|
44
|
+
model: string;
|
|
45
|
+
/** Whether a separate quota pool is used (bonus) or the shared one. */
|
|
46
|
+
quota_pool: QuotaPool;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Resolve a model alias for a tier from the map; '' = tier default sentinel. */
|
|
51
|
+
function modelForTier(tier: Tier, map: ModelMap): string {
|
|
52
|
+
if (tier === 'lite') return map.lite ?? '';
|
|
53
|
+
if (tier === 'medium') return map.medium ?? '';
|
|
54
|
+
if (tier === 'high') return map.high ?? '';
|
|
55
|
+
return ''; // inherit → session default
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve routing for one delegated sub-task. Vendor-neutral: returns a tier +
|
|
60
|
+
* an optional model alias (never a hard-coded provider model) + the quota pool.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveSubagentRouting(inp: RoutingInputs): RoutingDecision {
|
|
63
|
+
// Downshift off, or an `inherit` task → run on the session tier.
|
|
64
|
+
const tier: Tier = !inp.downshift || inp.task_tier === 'inherit' ? inp.session_tier : inp.task_tier;
|
|
65
|
+
const downshifted = inp.downshift && inp.task_tier !== 'inherit' && tier !== inp.session_tier;
|
|
66
|
+
|
|
67
|
+
// Quota arbitrage: a bonus, only when BOTH the setting and the host allow it.
|
|
68
|
+
const useSeparate = inp.quota_arbitrage && inp.separate_quota_pool;
|
|
69
|
+
const quota_pool: QuotaPool = useSeparate ? 'separate' : 'shared';
|
|
70
|
+
|
|
71
|
+
const reasonParts: string[] = [];
|
|
72
|
+
reasonParts.push(downshifted ? `downshift to ${tier}` : `session tier ${tier}`);
|
|
73
|
+
reasonParts.push(useSeparate ? 'separate quota pool (bonus)' : 'shared quota pool');
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
tier,
|
|
77
|
+
model: modelForTier(tier, inp.model_map),
|
|
78
|
+
quota_pool,
|
|
79
|
+
reason: reasonParts.join(' · '),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent spawn-brief composition (Phase 3) — task-optimal configuration.
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. Composes a subagent's brief from the existing config seams —
|
|
5
|
+
* role-mode + profile + persona(s) + a MINIMAL knowledge slice — so each
|
|
6
|
+
* subagent is configured for its task. Contract:
|
|
7
|
+
* `src/agent-src/contexts/execution/subagent-spawn-contract.md`.
|
|
8
|
+
*
|
|
9
|
+
* Hard invariant (lethal-trifecta-guard): knowledge is passed as a small set of
|
|
10
|
+
* REFERENCES, never as inline bodies, and is capped. Never bulk-dump context
|
|
11
|
+
* into a subagent — keep the private-data leg narrow.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type RoleMode = 'developer' | 'reviewer' | 'tester' | 'po' | 'incident' | 'planner';
|
|
15
|
+
|
|
16
|
+
const ROLE_MODES: ReadonlySet<string> = new Set<RoleMode>([
|
|
17
|
+
'developer',
|
|
18
|
+
'reviewer',
|
|
19
|
+
'tester',
|
|
20
|
+
'po',
|
|
21
|
+
'incident',
|
|
22
|
+
'planner',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** Max knowledge slice refs handed to one subagent (minimal-slice cap). */
|
|
26
|
+
export const MAX_KNOWLEDGE_REFS = 5;
|
|
27
|
+
/** Max personas overlaid on one subagent. */
|
|
28
|
+
export const MAX_PERSONAS = 2;
|
|
29
|
+
|
|
30
|
+
export interface SpawnSelection {
|
|
31
|
+
task: string;
|
|
32
|
+
/** Role-mode contract the subagent runs under, or null for none. */
|
|
33
|
+
role_mode?: RoleMode | null;
|
|
34
|
+
/** Active profile id (e.g. 'developer', 'content_creator'), or null. */
|
|
35
|
+
profile?: string | null;
|
|
36
|
+
/** Persona ids (review lenses) cited by the task's skill, capped. */
|
|
37
|
+
personas?: string[];
|
|
38
|
+
/** Knowledge slice REFERENCES (ids / paths) — never inline bodies. */
|
|
39
|
+
knowledge_refs?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SpawnBrief {
|
|
43
|
+
task: string;
|
|
44
|
+
role_mode: RoleMode | null;
|
|
45
|
+
profile: string | null;
|
|
46
|
+
personas: string[];
|
|
47
|
+
knowledge_refs: string[];
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A ref is acceptable only if it looks like an id/path token, not a body. */
|
|
52
|
+
function isRefLike(s: unknown): s is string {
|
|
53
|
+
return typeof s === 'string' && s.length > 0 && s.length <= 200 && !s.includes('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compose a validated spawn brief. Invalid role-mode → dropped to null;
|
|
58
|
+
* personas capped at {@link MAX_PERSONAS}; knowledge refs filtered to
|
|
59
|
+
* ref-like tokens and capped at {@link MAX_KNOWLEDGE_REFS}. Anything dropped is
|
|
60
|
+
* recorded in `warnings` (surfaced, never silent).
|
|
61
|
+
*/
|
|
62
|
+
export function composeSpawnBrief(sel: SpawnSelection): SpawnBrief {
|
|
63
|
+
const warnings: string[] = [];
|
|
64
|
+
|
|
65
|
+
let role_mode: RoleMode | null = null;
|
|
66
|
+
if (sel.role_mode != null) {
|
|
67
|
+
if (ROLE_MODES.has(sel.role_mode)) role_mode = sel.role_mode;
|
|
68
|
+
else warnings.push(`unknown role_mode '${sel.role_mode}' dropped`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const personasIn = sel.personas ?? [];
|
|
72
|
+
let personas = personasIn.filter((p) => typeof p === 'string' && p.length > 0);
|
|
73
|
+
if (personas.length > MAX_PERSONAS) {
|
|
74
|
+
warnings.push(`personas capped ${personas.length} → ${MAX_PERSONAS}`);
|
|
75
|
+
personas = personas.slice(0, MAX_PERSONAS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const refsIn = sel.knowledge_refs ?? [];
|
|
79
|
+
const bodyLike = refsIn.filter((r) => !isRefLike(r));
|
|
80
|
+
if (bodyLike.length > 0) {
|
|
81
|
+
warnings.push(`${bodyLike.length} non-ref knowledge entr${bodyLike.length === 1 ? 'y' : 'ies'} rejected (refs only, no bodies)`);
|
|
82
|
+
}
|
|
83
|
+
let knowledge_refs = refsIn.filter(isRefLike);
|
|
84
|
+
if (knowledge_refs.length > MAX_KNOWLEDGE_REFS) {
|
|
85
|
+
warnings.push(`knowledge refs capped ${knowledge_refs.length} → ${MAX_KNOWLEDGE_REFS}`);
|
|
86
|
+
knowledge_refs = knowledge_refs.slice(0, MAX_KNOWLEDGE_REFS);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
task: sel.task,
|
|
91
|
+
role_mode,
|
|
92
|
+
profile: sel.profile ?? null,
|
|
93
|
+
personas,
|
|
94
|
+
knowledge_refs,
|
|
95
|
+
warnings,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent steering & guardrails (Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. The guardrails that keep auto-dispatch from becoming a token
|
|
5
|
+
* sink: the N=3 per-target budget, the kill-switch, and the rollback
|
|
6
|
+
* guardrail-threshold checks. Contract:
|
|
7
|
+
* `src/agent-src/contexts/execution/subagent-steering.md`.
|
|
8
|
+
*
|
|
9
|
+
* Automatic cohort-level disable is OUT OF SCOPE — a config package runs no
|
|
10
|
+
* daemon. Breaches are surfaced for the maintainer/user to act on; the only
|
|
11
|
+
* automatic stop is the per-target N=3 budget.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Max consecutive failed attempts on one validation target before halting. */
|
|
15
|
+
export const MAX_ATTEMPTS_PER_TARGET = 3;
|
|
16
|
+
|
|
17
|
+
/** Rollback guardrail thresholds (council-defined). */
|
|
18
|
+
export const GUARDRAILS = {
|
|
19
|
+
/** Token spend vs single-agent baseline. */
|
|
20
|
+
token_blowup_ratio: 2,
|
|
21
|
+
/** Subagent spawn-failure rate. */
|
|
22
|
+
spawn_failure_rate: 0.1,
|
|
23
|
+
/** Fraction of dispatches that completed without a required verification. */
|
|
24
|
+
verify_skip_rate: 0.01,
|
|
25
|
+
/** Fraction of users manually disabling the layer. */
|
|
26
|
+
user_override_rate: 0.3,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export interface LayerState {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
auto: 'off' | 'ask' | 'on';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The kill-switch: layer is disabled when the master switch is off or auto is off. */
|
|
35
|
+
export function isLayerDisabled(state: LayerState): boolean {
|
|
36
|
+
return !state.enabled || state.auto === 'off';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** N=3 per-target budget: halt once consecutive failed attempts reach the cap. */
|
|
40
|
+
export function budgetHalt(consecutiveFailures: number): boolean {
|
|
41
|
+
return consecutiveFailures >= MAX_ATTEMPTS_PER_TARGET;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GuardrailMetrics {
|
|
45
|
+
/** Observed token spend / single-agent baseline (e.g. 2.4 = 2.4x). */
|
|
46
|
+
token_ratio: number;
|
|
47
|
+
spawn_failure_rate: number;
|
|
48
|
+
verify_skip_rate: number;
|
|
49
|
+
user_override_rate: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Evaluate the rollback guardrails. Returns the list of breached threshold
|
|
54
|
+
* names (empty = all clear). The orchestrator surfaces a non-empty result —
|
|
55
|
+
* it does NOT auto-disable (no daemon); the maintainer/user decides.
|
|
56
|
+
*/
|
|
57
|
+
export function breachedGuardrails(m: GuardrailMetrics): string[] {
|
|
58
|
+
const breached: string[] = [];
|
|
59
|
+
if (m.token_ratio > GUARDRAILS.token_blowup_ratio) breached.push('token_blowup');
|
|
60
|
+
if (m.spawn_failure_rate > GUARDRAILS.spawn_failure_rate) breached.push('spawn_failure');
|
|
61
|
+
if (m.verify_skip_rate > GUARDRAILS.verify_skip_rate) breached.push('verify_skip');
|
|
62
|
+
if (m.user_override_rate > GUARDRAILS.user_override_rate) breached.push('user_override');
|
|
63
|
+
return breached;
|
|
64
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification-budget model (Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. Decides HOW a delegated sub-task's output is verified so the
|
|
5
|
+
* cross-model judge Iron Law and verify-before-complete are preserved WITHOUT
|
|
6
|
+
* double-costing every trivial delegation. Contract:
|
|
7
|
+
* `src/agent-src/contexts/execution/verify-budget.md`.
|
|
8
|
+
*
|
|
9
|
+
* - Trivial (below the change-size floor, read-only / no file writes) →
|
|
10
|
+
* `deterministic` verification (diff + dry-run + structural checks, no LLM).
|
|
11
|
+
* - Otherwise → `judge` (full cross-model judge per subagent-orchestration).
|
|
12
|
+
* - A required verification that did not run is a SURFACED safety gap, never a
|
|
13
|
+
* silent pass.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type VerifyMode = 'deterministic' | 'judge' | 'none';
|
|
17
|
+
|
|
18
|
+
/** Max changed-line count (inclusive) that still counts as trivial. */
|
|
19
|
+
export const TRIVIAL_CHANGE_FLOOR = 20;
|
|
20
|
+
|
|
21
|
+
export interface VerifyInputs {
|
|
22
|
+
/** Changed lines the sub-task produced. */
|
|
23
|
+
change_size: number;
|
|
24
|
+
/** Did the sub-task write/modify files? */
|
|
25
|
+
file_writes: boolean;
|
|
26
|
+
/** Was the sub-task read-only (query/analysis, no mutation)? */
|
|
27
|
+
read_only: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Select the verification mode for a completed sub-task.
|
|
32
|
+
* Trivial + non-mutating → deterministic; anything else → judge.
|
|
33
|
+
*/
|
|
34
|
+
export function selectVerifyMode(inp: VerifyInputs): VerifyMode {
|
|
35
|
+
if (inp.read_only) return 'deterministic';
|
|
36
|
+
const trivial = inp.change_size <= TRIVIAL_CHANGE_FLOOR && !inp.file_writes;
|
|
37
|
+
return trivial ? 'deterministic' : 'judge';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect a verification safety gap: a mode was required but the recorded mode
|
|
42
|
+
* was `none`. Returns a human-readable gap string, or null when clean.
|
|
43
|
+
* The orchestrator surfaces a non-null result — it is never a silent pass.
|
|
44
|
+
*/
|
|
45
|
+
export function verificationGap(required: VerifyMode, recorded: VerifyMode): string | null {
|
|
46
|
+
if (required === 'none') return null;
|
|
47
|
+
if (recorded === 'none') {
|
|
48
|
+
return `verification gap: '${required}' required but none recorded`;
|
|
49
|
+
}
|
|
50
|
+
if (required === 'judge' && recorded === 'deterministic') {
|
|
51
|
+
return `verification gap: non-trivial change verified deterministically (judge required)`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Thinking-style advisors — replace-mode call planning (Phase 6).
|
|
3
3
|
*
|
|
4
4
|
* TypeScript twin of `src/scripts/ai_council/advisors.py` (ADR-200 —
|
|
5
|
-
* Python→TS migration, Phase 1). When
|
|
6
|
-
*
|
|
5
|
+
* Python→TS migration, Phase 1). When the user-global
|
|
6
|
+
* `~/.event4u/agent-config/settings/.ai-council.yml` (always global —
|
|
7
|
+
* ADR-104) enables an advisor (e.g. `contrarian` bound to
|
|
8
|
+
* `member: anthropic`), the
|
|
7
9
|
* orchestrator REPLACES the matching plain-member call with an
|
|
8
10
|
* advisor-persona call on the same provider. Same total call count as a
|
|
9
11
|
* plain run; bounded extra cost beyond the persona-prompt token delta.
|
|
@@ -287,12 +287,18 @@ function _curlJsonPost(url: string, extraHeaders: string[], body: unknown): unkn
|
|
|
287
287
|
for (const h of extraHeaders) {
|
|
288
288
|
args.push('-H', h);
|
|
289
289
|
}
|
|
290
|
+
// `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
|
|
291
|
+
// itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
|
|
292
|
+
// before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
|
|
293
|
+
// A full 16k-token generation legitimately runs several minutes, so the
|
|
294
|
+
// ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
|
|
295
|
+
args.push('--connect-timeout', '30', '--max-time', '290');
|
|
290
296
|
args.push('-w', '\n%{http_code}', '--data-binary', '@-');
|
|
291
297
|
const r = spawnSync('curl', args, {
|
|
292
298
|
input: JSON.stringify(body),
|
|
293
299
|
encoding: 'utf8',
|
|
294
300
|
maxBuffer: 64 * 1024 * 1024,
|
|
295
|
-
timeout:
|
|
301
|
+
timeout: 300_000,
|
|
296
302
|
});
|
|
297
303
|
if (r.error) {
|
|
298
304
|
throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* surface and validation behaviour byte-for-byte: same exported
|
|
7
7
|
* snake_case names, same defaults, same error messages, same precedence.
|
|
8
8
|
*
|
|
9
|
-
* Reads
|
|
10
|
-
* `docs/contracts/ai-council-config.md
|
|
9
|
+
* Reads the user-global `~/.event4u/agent-config/settings/.ai-council.yml`
|
|
10
|
+
* per the contract in `docs/contracts/ai-council-config.md` (council
|
|
11
|
+
* config is ALWAYS user-global — ADR-104). Replaces the fragmented
|
|
11
12
|
* `.agent-settings.yml` `ai_council` block (Phase 0 migration).
|
|
12
13
|
*
|
|
13
14
|
* Validation contract (8 rules, all enforced at load time):
|
|
@@ -93,7 +94,7 @@ export type PathLike = string;
|
|
|
93
94
|
|
|
94
95
|
// ── error type ─────────────────────────────────────────────────────
|
|
95
96
|
|
|
96
|
-
/** Raised when
|
|
97
|
+
/** Raised when the user-global `.ai-council.yml` violates the schema. */
|
|
97
98
|
export class CouncilConfigError extends Error {
|
|
98
99
|
constructor(message: string) {
|
|
99
100
|
super(message);
|
|
@@ -562,8 +563,10 @@ export const COUNCIL_CONFIG_USER_GLOBAL_REL = 'settings/.ai-council.yml';
|
|
|
562
563
|
|
|
563
564
|
/**
|
|
564
565
|
* Env var pinning the council config to an explicit absolute path, ahead
|
|
565
|
-
* of the
|
|
566
|
-
*
|
|
566
|
+
* of the user-global default. Mirrors `EVENT4U_CONFIG_HOME` but targets
|
|
567
|
+
* the config file itself (tests / power users). This is the ONLY escape
|
|
568
|
+
* from the user-global location — it is an explicit absolute path, never
|
|
569
|
+
* a "search the project" path.
|
|
567
570
|
*/
|
|
568
571
|
export const COUNCIL_CONFIG_ENV = 'AI_COUNCIL_CONFIG';
|
|
569
572
|
|
|
@@ -598,14 +601,24 @@ function _isAbsoluteLikePython(p: string): boolean {
|
|
|
598
601
|
/**
|
|
599
602
|
* Resolve which `.ai-council.yml` the council reads.
|
|
600
603
|
*
|
|
604
|
+
* **The council config is ALWAYS user-global.** It is a per-developer
|
|
605
|
+
* facility configured once and works in every project, worktree, and
|
|
606
|
+
* CWD — including consumer repos that carry no council file of their
|
|
607
|
+
* own. The project tree is NEVER searched for council config (ADR-104,
|
|
608
|
+
* superseding the project-local override kept by ADR-093).
|
|
609
|
+
*
|
|
601
610
|
* Precedence (first match wins):
|
|
602
611
|
*
|
|
603
612
|
* 1. `$AI_COUNCIL_CONFIG` — explicit absolute override (tests / power
|
|
604
|
-
* users). Honoured even when the target is absent.
|
|
605
|
-
*
|
|
606
|
-
*
|
|
613
|
+
* users). Honoured even when the target is absent. This is an
|
|
614
|
+
* explicit path, not a project search.
|
|
615
|
+
* 2. User-global `~/.event4u/agent-config/settings/.ai-council.yml`
|
|
607
616
|
* (with the legacy `~/.config/agent-config/` read-fallback).
|
|
608
617
|
*
|
|
618
|
+
* `project_root` is accepted for signature stability with callers but is
|
|
619
|
+
* NOT consulted for config resolution — the council never reads
|
|
620
|
+
* `<project_root>/agents/settings/.ai-council.yml`.
|
|
621
|
+
*
|
|
609
622
|
* Always returns a path (never `null`): when nothing exists yet it
|
|
610
623
|
* returns the user-global write target.
|
|
611
624
|
*/
|
|
@@ -613,20 +626,12 @@ export function resolve_config_path(
|
|
|
613
626
|
project_root: PathLike,
|
|
614
627
|
options: { env?: user_global_paths.EnvMap | null } = {},
|
|
615
628
|
): PathLike {
|
|
629
|
+
void project_root; // intentionally unused — council config is always user-global (ADR-104).
|
|
616
630
|
const env_map = options.env != null ? options.env : process.env;
|
|
617
631
|
const override = env_map[COUNCIL_CONFIG_ENV];
|
|
618
632
|
if (override) {
|
|
619
633
|
return _expanduser(override);
|
|
620
634
|
}
|
|
621
|
-
const project_path = path.join(
|
|
622
|
-
project_root,
|
|
623
|
-
'agents',
|
|
624
|
-
'settings',
|
|
625
|
-
COUNCIL_CONFIG_RELNAME,
|
|
626
|
-
);
|
|
627
|
-
if (fs.existsSync(project_path)) {
|
|
628
|
-
return project_path;
|
|
629
|
-
}
|
|
630
635
|
const found = user_global_paths.resolve_with_fallback(
|
|
631
636
|
COUNCIL_CONFIG_USER_GLOBAL_REL,
|
|
632
637
|
{ env: options.env ?? null },
|