@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.
Files changed (103) hide show
  1. package/.claude-plugin/marketplace.json +8 -1
  2. package/CHANGELOG.md +99 -0
  3. package/README.md +4 -2
  4. package/dist/agent-src/commands/council/default.md +28 -20
  5. package/dist/agent-src/commands/evals/fix.json +5 -1
  6. package/dist/agent-src/commands/fix/comments.md +101 -0
  7. package/dist/agent-src/commands/fix.md +5 -2
  8. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +65 -0
  9. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +68 -0
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +68 -0
  11. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +62 -0
  12. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +59 -0
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +8 -3
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +56 -0
  15. package/dist/agent-src/contexts/execution/subagent-routing.md +57 -0
  16. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-steering.md +71 -0
  18. package/dist/agent-src/contexts/execution/verify-budget.md +50 -0
  19. package/dist/agent-src/rules/delegation-policy.md +84 -0
  20. package/dist/agent-src/rules/design-fidelity.md +98 -0
  21. package/dist/agent-src/rules/domain-safety-pii.md +22 -0
  22. package/dist/agent-src/rules/legal-safety-floor.md +144 -0
  23. package/dist/agent-src/rules/lethal-trifecta-guard.md +25 -22
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +102 -15
  25. package/dist/agent-src/scripts/update_roadmap_progress.ts +52 -7
  26. package/dist/agent-src/skills/ai-council/SKILL.md +28 -1
  27. package/dist/agent-src/skills/contract-review/SKILL.md +124 -0
  28. package/dist/agent-src/skills/contract-review/evals/triggers.json +17 -0
  29. package/dist/agent-src/skills/dpa-review/SKILL.md +129 -0
  30. package/dist/agent-src/skills/dpa-review/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/laravel/SKILL.md +1 -0
  32. package/dist/agent-src/skills/legal-intake-triage/SKILL.md +127 -0
  33. package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +17 -0
  34. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +100 -0
  35. package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +17 -0
  36. package/dist/agent-src/skills/nda-triage/SKILL.md +113 -0
  37. package/dist/agent-src/skills/nda-triage/evals/triggers.json +17 -0
  38. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +36 -9
  39. package/dist/agent-src/skills/recursive-verification/SKILL.md +186 -0
  40. package/dist/agent-src/skills/roadmap-management/SKILL.md +17 -0
  41. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -0
  42. package/dist/agent-src/templates/agent-settings.md +64 -19
  43. package/dist/discovery/deprecation-report.md +1 -1
  44. package/dist/discovery/discovery-manifest.json +278 -23
  45. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  46. package/dist/discovery/discovery-manifest.summary.md +12 -4
  47. package/dist/discovery/orphan-report.md +1 -1
  48. package/dist/discovery/packs.json +50 -8
  49. package/dist/discovery/trust-report.md +7 -5
  50. package/dist/discovery/workspaces.json +34 -6
  51. package/dist/mcp/registry-manifest.json +3 -3
  52. package/dist/router.json +1 -1
  53. package/dist/server/schemas/settings.js +14 -0
  54. package/dist/server/schemas/settings.js.map +1 -1
  55. package/docs/architecture.md +4 -4
  56. package/docs/benchmark.md +46 -0
  57. package/docs/catalog.md +18 -6
  58. package/docs/command-flows.md +7 -1
  59. package/docs/contracts/ai-council-config.md +17 -10
  60. package/docs/contracts/audit-log-v1.md +7 -0
  61. package/docs/contracts/auto-orchestration-v1.md +70 -0
  62. package/docs/contracts/command-clusters.md +1 -1
  63. package/docs/contracts/discovery-manifest.schema.json +2 -2
  64. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +19 -0
  65. package/docs/decisions/ADR-020-global-only-consumer-scope.md +13 -6
  66. package/docs/decisions/ADR-049-configuration-trust-boundary.md +11 -1
  67. package/docs/decisions/ADR-093-ai-council-config-user-global.md +10 -4
  68. package/docs/decisions/ADR-104-ai-council-config-global-only.md +119 -0
  69. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +102 -0
  70. package/docs/decisions/ADR-106-recursive-verification-benchmark-gate.md +126 -0
  71. package/docs/decisions/ADR-107-legal-domain-pack-adoption.md +51 -0
  72. package/docs/decisions/ADR-108-open-source-forever-no-commercial-tier.md +46 -0
  73. package/docs/decisions/INDEX.md +5 -0
  74. package/docs/getting-started.md +2 -2
  75. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
  76. package/docs/guidelines/agent-infra/domain-pack-architecture.md +55 -0
  77. package/package.json +1 -1
  78. package/src/config/agent-settings.template.yml +72 -3
  79. package/src/config/discovery/packs.yml +13 -0
  80. package/src/config/discovery/workspaces.yml +7 -0
  81. package/src/scripts/_lib/auto_dispatch.ts +95 -0
  82. package/src/scripts/_lib/host_capability.ts +59 -0
  83. package/src/scripts/_lib/orchestration_gate.ts +98 -0
  84. package/src/scripts/_lib/subagent_bundle.ts +154 -0
  85. package/src/scripts/_lib/subagent_response.ts +82 -0
  86. package/src/scripts/_lib/subagent_routing.ts +81 -0
  87. package/src/scripts/_lib/subagent_spawn.ts +97 -0
  88. package/src/scripts/_lib/subagent_steering.ts +64 -0
  89. package/src/scripts/_lib/verify_budget.ts +54 -0
  90. package/src/scripts/ai_council/advisors.ts +4 -2
  91. package/src/scripts/ai_council/clients.ts +7 -1
  92. package/src/scripts/ai_council/config.ts +22 -17
  93. package/src/scripts/bench_ab_v2_run.ts +127 -0
  94. package/src/scripts/bench_ab_v2_stats.ts +39 -0
  95. package/src/scripts/check_council_config_location.ts +6 -5
  96. package/src/scripts/lint_discovery_vocabulary.ts +6 -0
  97. package/src/scripts/lint_framework_leakage_allowlist.json +21 -0
  98. package/src/scripts/lint_legal_pack.ts +139 -0
  99. package/src/scripts/probe_projection_fidelity.ts +7 -1
  100. package/src/scripts/rdp_gate_classify.ts +131 -0
  101. package/src/scripts/rdp_quality_eval.ts +437 -0
  102. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  103. 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 `agents/settings/.ai-council.yml`
6
- * enables an advisor (e.g. `contrarian` bound to `member: anthropic`), the
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: 120_000,
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 `agents/settings/.ai-council.yml` per the contract in
10
- * `docs/contracts/ai-council-config.md`. Replaces the fragmented
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 `agents/settings/.ai-council.yml` violates the schema. */
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 project → user-global search. Mirrors `EVENT4U_CONFIG_HOME` but
566
- * targets the config file itself (tests / power users).
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
- * 2. Project-local `<project_root>/agents/settings/.ai-council.yml`.
606
- * 3. User-global `~/.event4u/agent-config/settings/.ai-council.yml`
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 },