brl-subagent 2.3.4

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/package.json +49 -0
  4. package/presets/code-reviewer.md +37 -0
  5. package/presets/data-analyst.md +33 -0
  6. package/presets/debugger.md +36 -0
  7. package/presets/dev-agent.md +30 -0
  8. package/presets/rapid-prototyper.md +25 -0
  9. package/presets/refactorer.md +34 -0
  10. package/presets/security-auditor.md +39 -0
  11. package/presets/tech-writer.md +33 -0
  12. package/presets/test-engineer.md +34 -0
  13. package/src/concurrency.ts +186 -0
  14. package/src/diff.ts +102 -0
  15. package/src/event-bus.ts +86 -0
  16. package/src/git.ts +187 -0
  17. package/src/history.ts +177 -0
  18. package/src/index.ts +3824 -0
  19. package/src/logging.ts +135 -0
  20. package/src/messaging.ts +164 -0
  21. package/src/metrics.ts +221 -0
  22. package/src/model-availability.ts +73 -0
  23. package/src/notify-completion.ts +207 -0
  24. package/src/params.ts +225 -0
  25. package/src/preflight.ts +150 -0
  26. package/src/presets.ts +425 -0
  27. package/src/prompt.ts +169 -0
  28. package/src/reports.ts +325 -0
  29. package/src/router.ts +133 -0
  30. package/src/runner.ts +877 -0
  31. package/src/sanitize.ts +365 -0
  32. package/src/scheduler.ts +187 -0
  33. package/src/session-manager.ts +1065 -0
  34. package/src/state.ts +590 -0
  35. package/src/templates.ts +469 -0
  36. package/src/transcript-tail.ts +327 -0
  37. package/src/transcript.ts +119 -0
  38. package/src/tui-format.ts +51 -0
  39. package/src/tui.ts +3103 -0
  40. package/src/types.ts +803 -0
  41. package/src/unit-run.ts +269 -0
  42. package/src/update.ts +109 -0
  43. package/src/validate.ts +302 -0
  44. package/templates/analyze-data.md +9 -0
  45. package/templates/code-review.md +10 -0
  46. package/templates/debug-issue.md +9 -0
  47. package/templates/implement-feature.md +9 -0
  48. package/templates/prototype.md +9 -0
  49. package/templates/refactor.md +9 -0
  50. package/templates/security-audit.md +10 -0
  51. package/templates/write-docs.md +9 -0
  52. package/templates/write-tests.md +10 -0
package/src/logging.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * brl-subagent — Structured Logging (F10)
3
+ *
4
+ * Provides leveled, structured logging with file output and rotation.
5
+ * Logs are written to .pi/subagent-logs/ in the project directory.
6
+ *
7
+ * Usage:
8
+ * const log = createLogger("runner", cwd);
9
+ * log.info("Subagent started", { model, thinkingLevel });
10
+ * log.error("Subagent failed", { error: err.message });
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import type { LogLevel } from "./types";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Configuration
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Maximum single log file size before rotation (5MB) */
22
+ const MAX_LOG_SIZE = 5 * 1024 * 1024;
23
+
24
+ /** Maximum number of rotated log files to keep */
25
+ const MAX_LOG_FILES = 5;
26
+
27
+ /** Log levels in order of verbosity */
28
+ const LOG_LEVELS: Record<LogLevel, number> = {
29
+ debug: 0,
30
+ info: 1,
31
+ warn: 2,
32
+ error: 3,
33
+ };
34
+
35
+ /** Current minimum log level — can be changed at runtime */
36
+ let minLevel: LogLevel = "info";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Logger
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface Logger {
43
+ debug(message: string, data?: Record<string, unknown>): void;
44
+ info(message: string, data?: Record<string, unknown>): void;
45
+ warn(message: string, data?: Record<string, unknown>): void;
46
+ error(message: string, data?: Record<string, unknown>): void;
47
+ }
48
+
49
+ export function setLogLevel(level: LogLevel): void {
50
+ minLevel = level;
51
+ }
52
+
53
+ /**
54
+ * Create a logger for a specific module.
55
+ * Logs are written to `.pi/subagent-logs/<prefix>.log` relative to `cwd`.
56
+ */
57
+ export function createLogger(prefix: string, cwd?: string): Logger {
58
+ const logDir = cwd ? path.join(cwd, ".pi", "subagent-logs") : undefined;
59
+
60
+ // Ensure log directory exists — owner-only (0o700) so other local users
61
+ // cannot list log files (F6 / issue #29). File writes below already use
62
+ // 0o600.
63
+ if (logDir) {
64
+ try {
65
+ fs.mkdirSync(logDir, { recursive: true, mode: 0o700 });
66
+ } catch {
67
+ // Can't create log dir — fall back to console-only logging
68
+ }
69
+ }
70
+
71
+ function log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
72
+ if (LOG_LEVELS[level] < LOG_LEVELS[minLevel]) return;
73
+
74
+ const timestamp = new Date().toISOString();
75
+ const entry = {
76
+ timestamp,
77
+ level,
78
+ prefix,
79
+ message,
80
+ ...(data ? { data } : {}),
81
+ };
82
+
83
+ const line = JSON.stringify(entry);
84
+
85
+ // Console output
86
+ const consoleMethod = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
87
+ consoleMethod(`[${prefix}] ${level.toUpperCase()}: ${message}`);
88
+
89
+ // File output
90
+ if (logDir) {
91
+ try {
92
+ const logFile = path.join(logDir, `${prefix}.log`);
93
+ rotateIfNeeded(logFile);
94
+ fs.appendFileSync(logFile, line + "\n", { encoding: "utf-8", mode: 0o600 });
95
+ } catch {
96
+ // Silently fail — logging should never crash the extension
97
+ }
98
+ }
99
+ }
100
+
101
+ return {
102
+ debug: (m, d) => log("debug", m, d),
103
+ info: (m, d) => log("info", m, d),
104
+ warn: (m, d) => log("warn", m, d),
105
+ error: (m, d) => log("error", m, d),
106
+ };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Log rotation
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function rotateIfNeeded(logFile: string): void {
114
+ try {
115
+ const stat = fs.statSync(logFile);
116
+ if (stat.size < MAX_LOG_SIZE) return;
117
+
118
+ // Rotate: remove oldest, shift others, rename current
119
+ for (let i = MAX_LOG_FILES - 1; i >= 0; i--) {
120
+ const oldFile = i === 0 ? logFile : `${logFile}.${i}`;
121
+ const newFile = `${logFile}.${i + 1}`;
122
+ try {
123
+ if (i === MAX_LOG_FILES - 1) {
124
+ fs.unlinkSync(oldFile);
125
+ } else {
126
+ fs.renameSync(oldFile, newFile);
127
+ }
128
+ } catch {
129
+ // File might not exist — that's fine
130
+ }
131
+ }
132
+ } catch {
133
+ // Log file doesn't exist yet — nothing to rotate
134
+ }
135
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * brl-subagent — Subagent-to-Subagent Messaging (E10)
3
+ *
4
+ * Provides an Intercom class that manages inter-subagent communication
5
+ * via a shared message channel. Subagents can send targeted or broadcast
6
+ * messages using a special output format parsed by the conductor.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface Message {
14
+ from: string;
15
+ content: string;
16
+ timestamp: number;
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Regex for extracting TO: patterns from subagent output
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Matches standalone lines like:
25
+ * [TO:agent-id]:some message text
26
+ * [TO:*]:broadcast message text
27
+ *
28
+ * Anchored to the line start (with optional leading whitespace) using the
29
+ * multiline flag, so ONLY standalone lines whose trimmed content begins
30
+ * with "[TO:" are matched — a mid-sentence mention (e.g. "append
31
+ * [TO:target]:msg ...") is NOT treated as a message.
32
+ *
33
+ * Not global: extractMessages builds its own stateful /g instance to
34
+ * avoid lastIndex leaks between calls.
35
+ *
36
+ * Group 1 = target id or "*"
37
+ * Group 2 = message content (rest of line after colon, trimmed by caller)
38
+ */
39
+ export const TO_PATTERN = /^\s*\[TO:([^*\]]+|\*)\]:([^\n]+)/m;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Intercom class
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export class Intercom {
46
+ /**
47
+ * Map of subagent ID → queue of incoming messages.
48
+ * Keys are created lazily when a message is sent to a subagent.
49
+ */
50
+ private messages: Map<string, Message[]> = new Map();
51
+
52
+ /**
53
+ * Known subagent IDs. Populated via `register()` as subagents are spawned.
54
+ * Used by `broadcast` to determine recipients.
55
+ */
56
+ private knownIds: Set<string> = new Set();
57
+
58
+ /**
59
+ * Register a subagent ID so it can receive broadcasts.
60
+ */
61
+ register(id: string): void {
62
+ this.knownIds.add(id);
63
+ }
64
+
65
+ /**
66
+ * Remove a subagent ID from the known set (for cleanup).
67
+ */
68
+ unregister(id: string): void {
69
+ this.knownIds.delete(id);
70
+ }
71
+
72
+ /**
73
+ * Send a message from one subagent to another.
74
+ */
75
+ send(fromId: string, toId: string, content: string): void {
76
+ if (!this.messages.has(toId)) {
77
+ this.messages.set(toId, []);
78
+ }
79
+ this.messages.get(toId)!.push({
80
+ from: fromId,
81
+ content,
82
+ timestamp: Date.now(),
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Return all messages for a subagent without clearing the queue.
88
+ */
89
+ receive(id: string): Message[] {
90
+ return this.messages.get(id) ?? [];
91
+ }
92
+
93
+ /**
94
+ * Return all messages for a subagent and clear the queue.
95
+ */
96
+ receiveAndClear(id: string): Message[] {
97
+ const msgs = this.messages.get(id) ?? [];
98
+ this.messages.set(id, []);
99
+ return msgs;
100
+ }
101
+
102
+ /**
103
+ * Returns true if there are pending messages for this subagent.
104
+ */
105
+ hasMessages(id: string): boolean {
106
+ const q = this.messages.get(id);
107
+ return q !== undefined && q.length > 0;
108
+ }
109
+
110
+ /**
111
+ * Send a message to ALL known subagent IDs except the sender and any
112
+ * explicitly excluded IDs.
113
+ */
114
+ broadcast(fromId: string, content: string, excludeIds?: string[]): void {
115
+ const exclude = new Set(excludeIds ?? []);
116
+ exclude.add(fromId); // never send back to self
117
+
118
+ for (const id of this.knownIds) {
119
+ if (!exclude.has(id)) {
120
+ this.send(fromId, id, content);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Return the list of all known subagent IDs.
127
+ */
128
+ getKnownIds(): string[] {
129
+ return [...this.knownIds];
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Output parsing helpers
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Extract all [TO:...] messages from a subagent's output text.
139
+ * Returns an array of { target, content } objects.
140
+ */
141
+ export function extractMessages(
142
+ output: string,
143
+ ): Array<{ target: string; content: string }> {
144
+ const results: Array<{ target: string; content: string }> = [];
145
+ const regex = new RegExp(TO_PATTERN.source, `${TO_PATTERN.flags}g`);
146
+ let match: RegExpExecArray | null;
147
+ while ((match = regex.exec(output)) !== null) {
148
+ results.push({
149
+ target: match[1],
150
+ content: match[2].trim(),
151
+ });
152
+ }
153
+ return results;
154
+ }
155
+
156
+ /**
157
+ * Format a list of incoming messages into a string that can be appended
158
+ * to a subagent's task prompt.
159
+ */
160
+ export function formatPendingMessages(messages: Message[]): string {
161
+ return messages
162
+ .map((m) => `From ${m.from}: ${m.content}`)
163
+ .join("\n");
164
+ }
package/src/metrics.ts ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * brl-subagent — SLA Metrics (E4)
3
+ *
4
+ * Computes performance metrics from run history and detects degradation
5
+ * against a baseline.
6
+ */
7
+
8
+ import type { SubagentRun, SLAMetrics, DegradationReport, ErrorCategory } from "./types";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Percentile helper
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Compute the p-th percentile of a sorted numeric array (0-100).
16
+ * Uses linear interpolation between ranks for accuracy.
17
+ */
18
+ function percentile(sorted: number[], p: number): number {
19
+ if (sorted.length === 0) return 0;
20
+ if (sorted.length === 1) return sorted[0];
21
+ const rank = (p / 100) * (sorted.length - 1);
22
+ const lower = Math.floor(rank);
23
+ const upper = Math.ceil(rank);
24
+ if (lower === upper) return sorted[lower];
25
+ const weight = rank - lower;
26
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // SLA metric computation
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Compute SLA metrics from a list of SubagentRun records.
35
+ *
36
+ * Duration calculations use only runs that have finishedAt and durationMs set.
37
+ * Runs with status "running" are excluded from duration stats but counted in totalRuns.
38
+ *
39
+ * @param runs - Array of SubagentRun records (most recent first is fine; sorting happens internally)
40
+ * @returns Computed SLAMetrics
41
+ */
42
+ export function computeSLAMetrics(runs: SubagentRun[]): SLAMetrics {
43
+ const totalRuns = runs.length;
44
+
45
+ if (totalRuns === 0) {
46
+ return {
47
+ totalRuns: 0,
48
+ successRate: 0,
49
+ failureRate: 0,
50
+ averageDurationMs: 0,
51
+ p50DurationMs: 0,
52
+ p95DurationMs: 0,
53
+ p99DurationMs: 0,
54
+ totalCost: 0,
55
+ averageCost: 0,
56
+ errorCategoryBreakdown: {},
57
+ };
58
+ }
59
+
60
+ // Count successes and failures (running runs are neither)
61
+ const succeeded = runs.filter((r) => r.status === "done").length;
62
+ const failed = runs.filter((r) => r.status === "failed").length;
63
+ const finishedRuns = succeeded + failed;
64
+
65
+ const successRate = finishedRuns > 0 ? succeeded / finishedRuns : 0;
66
+ const failureRate = finishedRuns > 0 ? failed / finishedRuns : 0;
67
+
68
+ // Duration stats — only runs with durationMs set
69
+ const durations = runs
70
+ .filter((r) => r.durationMs !== undefined && r.durationMs !== null)
71
+ .map((r) => r.durationMs!);
72
+
73
+ const sortedDurations = [...durations].sort((a, b) => a - b);
74
+
75
+ const averageDurationMs =
76
+ durations.length > 0
77
+ ? durations.reduce((sum, d) => sum + d, 0) / durations.length
78
+ : 0;
79
+
80
+ const p50DurationMs = percentile(sortedDurations, 50);
81
+ const p95DurationMs = percentile(sortedDurations, 95);
82
+ const p99DurationMs = percentile(sortedDurations, 99);
83
+
84
+ // Cost stats
85
+ const costs = runs.map((r) => r.cost ?? 0);
86
+ const totalCost = costs.reduce((sum, c) => sum + c, 0);
87
+ const averageCost = totalRuns > 0 ? totalCost / totalRuns : 0;
88
+
89
+ // Error category breakdown
90
+ const errorCategoryBreakdown: Record<string, number> = {};
91
+ for (const run of runs) {
92
+ const cat = run.originalParams?.errorCategory as ErrorCategory | undefined;
93
+ if (cat) {
94
+ errorCategoryBreakdown[cat] = (errorCategoryBreakdown[cat] ?? 0) + 1;
95
+ }
96
+ }
97
+
98
+ return {
99
+ totalRuns,
100
+ successRate,
101
+ failureRate,
102
+ averageDurationMs,
103
+ p50DurationMs,
104
+ p95DurationMs,
105
+ p99DurationMs,
106
+ totalCost,
107
+ averageCost,
108
+ errorCategoryBreakdown,
109
+ };
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Degradation detection
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Compare current SLA metrics against a baseline to detect degradation.
118
+ *
119
+ * Degradation is flagged if:
120
+ * - Success rate dropped by more than 10 percentage points, OR
121
+ * - P95 latency increased by more than 2x
122
+ *
123
+ * @param current - Current SLA metrics
124
+ * @param baseline - Baseline SLA metrics to compare against
125
+ * @returns DegradationReport with comparison results and recommendations
126
+ */
127
+ // ---------------------------------------------------------------------------
128
+ // Cost trend & sparkline (E1: Dashboard)
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Return the last N run costs as an array of numbers, most recent last.
133
+ * Filters to runs that have a cost field set.
134
+ */
135
+ export function computeCostTrend(runs: SubagentRun[], count: number): number[] {
136
+ return runs
137
+ .filter((r) => r.cost !== undefined && r.cost !== null)
138
+ .slice(-count)
139
+ .map((r) => r.cost!);
140
+ }
141
+
142
+ /**
143
+ * Unicode sparkline characters, ordered from lowest to highest.
144
+ */
145
+ const SPARKLINE_CHARS = ['\u2581', '\u2582', '\u2583', '\u2584', '\u2585', '\u2586', '\u2587', '\u2588'];
146
+
147
+ /**
148
+ * Convert an array of numbers to a Unicode sparkline string.
149
+ * Values are scaled to the 8-character range. Empty array returns empty string.
150
+ */
151
+ export function formatSparkline(values: number[]): string {
152
+ if (values.length === 0) return '';
153
+ const min = Math.min(...values);
154
+ const max = Math.max(...values);
155
+ const range = max - min;
156
+ if (range === 0) {
157
+ // All values equal — show middle character
158
+ return values.map(() => SPARKLINE_CHARS[3]).join('');
159
+ }
160
+ return values
161
+ .map((v) => {
162
+ const normalized = (v - min) / range;
163
+ const index = Math.min(Math.floor(normalized * SPARKLINE_CHARS.length), SPARKLINE_CHARS.length - 1);
164
+ return SPARKLINE_CHARS[index];
165
+ })
166
+ .join('');
167
+ }
168
+
169
+ export function computeDegradation(
170
+ current: SLAMetrics,
171
+ baseline: SLAMetrics,
172
+ ): DegradationReport {
173
+ const recommendations: string[] = [];
174
+
175
+ // Success rate change (percentage points, negative = drop)
176
+ const successRateChange =
177
+ baseline.successRate > 0
178
+ ? (current.successRate - baseline.successRate) * 100
179
+ : 0;
180
+
181
+ // P95 latency change (ratio: current / baseline)
182
+ const p95Change =
183
+ baseline.p95DurationMs > 0
184
+ ? current.p95DurationMs / baseline.p95DurationMs
185
+ : current.p95DurationMs > 0
186
+ ? Infinity
187
+ : 0;
188
+
189
+ // Degradation criteria
190
+ const successRateDegraded = successRateChange < -10;
191
+ const p95Degraded = p95Change > 2;
192
+ const degraded = successRateDegraded || p95Degraded;
193
+
194
+ // Generate recommendations
195
+ if (successRateDegraded) {
196
+ recommendations.push(
197
+ `Success rate dropped by ${Math.abs(successRateChange).toFixed(1)}% ` +
198
+ `(from ${(baseline.successRate * 100).toFixed(1)}% to ${(current.successRate * 100).toFixed(1)}%). ` +
199
+ `Check error category breakdown for root cause.`,
200
+ );
201
+ }
202
+
203
+ if (p95Degraded) {
204
+ recommendations.push(
205
+ `P95 latency increased ${p95Change.toFixed(1)}x ` +
206
+ `(from ${baseline.p95DurationMs.toFixed(0)}ms to ${current.p95DurationMs.toFixed(0)}ms). ` +
207
+ `Consider increasing timeout or reducing task complexity.`,
208
+ );
209
+ }
210
+
211
+ if (!degraded) {
212
+ recommendations.push("No significant degradation detected. Metrics are within normal range.");
213
+ }
214
+
215
+ return {
216
+ degraded,
217
+ successRateChange,
218
+ p95Change,
219
+ recommendations,
220
+ };
221
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Auth-aware model availability check (issue #4).
3
+ *
4
+ * A model may exist in pi's CATALOG (built-in model list) while its provider
5
+ * has no configured credentials (no API key / OAuth). Spawning a subagent with
6
+ * such a model fails at spawn time ("No API key found") instead of triggering
7
+ * the preset-model fallback. This module checks BOTH catalog presence AND
8
+ * provider auth configuration before a model is considered available.
9
+ *
10
+ * Structural subset of the pi SDK `ModelRegistry` (see
11
+ * @earendil-works/pi-coding-agent/dist/core/model-registry.d.ts):
12
+ * - find(provider, modelId): Model | undefined
13
+ * - getAll(): Model[]
14
+ * - hasConfiguredAuth(model): boolean
15
+ * - getProviderAuthStatus(provider): AuthStatus // { configured: boolean; ... }
16
+ *
17
+ * Kept free of pi runtime imports so it can be unit-tested in isolation.
18
+ */
19
+
20
+ export interface ModelLike {
21
+ provider: string;
22
+ id: string;
23
+ }
24
+
25
+ export interface ProviderAuthStatusLike {
26
+ configured?: boolean;
27
+ source?: string;
28
+ label?: string;
29
+ }
30
+
31
+ export interface ModelRegistryLike {
32
+ find?(provider: string, modelId: string): ModelLike | undefined;
33
+ getAll?(): ModelLike[];
34
+ hasConfiguredAuth?(model: ModelLike): boolean;
35
+ getProviderAuthStatus?(provider: string): ProviderAuthStatusLike | undefined;
36
+ }
37
+
38
+ /**
39
+ * Check that provider/model exists in the catalog AND that the provider has
40
+ * configured auth. Falls back to catalog-only presence when the registry
41
+ * exposes no auth API (old behavior).
42
+ */
43
+ export function modelIsAvailable(
44
+ registry: ModelRegistryLike | undefined,
45
+ m: { provider: string; id: string },
46
+ ): boolean {
47
+ try {
48
+ // Find the model in the catalog
49
+ let model: ModelLike | undefined;
50
+ if (registry?.find) {
51
+ model = registry.find(m.provider, m.id);
52
+ }
53
+ if (!model) {
54
+ const models = registry?.getAll?.() ?? [];
55
+ model = models.find((x) => x.provider === m.provider && x.id === m.id);
56
+ }
57
+ if (!model) return false;
58
+
59
+ // Auth-aware check: model exists in catalog BUT provider must have configured auth
60
+ if (typeof registry?.hasConfiguredAuth === "function") {
61
+ return registry.hasConfiguredAuth(model);
62
+ }
63
+ // Fallback: getProviderAuthStatus (AuthStatus = { configured: boolean; ... })
64
+ if (typeof registry?.getProviderAuthStatus === "function") {
65
+ const status = registry.getProviderAuthStatus(m.provider);
66
+ return status?.configured === true;
67
+ }
68
+ // No auth API — trust the catalog (old behavior)
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }