@plexor-dev/claude-code-plugin-staging 0.1.0-beta.26 → 0.1.0-beta.27

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 (2) hide show
  1. package/lib/supervisor.js +282 -0
  2. package/package.json +1 -1
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Plexor Supervisor Emitter — Phases 1-4
3
+ *
4
+ * Phase 1: Basic routing summary
5
+ * [PLEXOR: Routed to {provider}/{model}, {latency}ms, {routing_source}]
6
+ *
7
+ * Phase 2: Enhanced routing with cohort from response body fields
8
+ * [PLEXOR: {provider}/{model}, {latency}ms, {source} | {cohort}]
9
+ *
10
+ * Phase 3: Zero-tool escalation detection (agent_halt / escalation signals)
11
+ * [PLEXOR: Zero-tool escalation: {provider1} → {provider2}]
12
+ *
13
+ * Phase 4: Scaffolding gate blocked detection
14
+ * [PLEXOR: Scaffolding gate: {model} blocked, using {alternative}]
15
+ *
16
+ * This module is consumed by track-response.js to surface routing
17
+ * decisions to the developer without requiring them to parse verbose logs.
18
+ */
19
+
20
+ const CYAN = '\x1b[36m';
21
+ const YELLOW = '\x1b[33m';
22
+ const RED = '\x1b[31m';
23
+ const RESET = '\x1b[0m';
24
+
25
+ class SupervisorEmitter {
26
+ /**
27
+ * @param {object} [opts]
28
+ * @param {boolean} [opts.enabled] — honour PLEXOR_SUPERVISOR env var (default true)
29
+ */
30
+ constructor(opts = {}) {
31
+ const envFlag = process.env.PLEXOR_SUPERVISOR;
32
+ if (envFlag !== undefined) {
33
+ this.enabled = !/^(0|false|no|off)$/i.test(String(envFlag));
34
+ } else {
35
+ this.enabled = opts.enabled !== false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Build the Phase 2 enhanced supervisor summary from a gateway response.
41
+ * Reads plexor_provider_used, plexor_selected_model, plexor_latency_ms,
42
+ * plexor_routing_source from the response body (not just headers).
43
+ *
44
+ * Format: [PLEXOR: provider/model, latencyms, source | cohort]
45
+ *
46
+ * @param {object} response — the full LLM response object
47
+ * @param {object} [plexorMeta] — the _plexor metadata block (may be absent)
48
+ * @returns {string|null}
49
+ */
50
+ buildSummary(response, plexorMeta) {
51
+ if (!response || typeof response !== 'object') {
52
+ return null;
53
+ }
54
+
55
+ const provider = this._resolveProvider(response, plexorMeta);
56
+ const model = this._resolveModel(response, plexorMeta);
57
+ const latencyMs = this._resolveLatency(response, plexorMeta);
58
+ const routingSource = this._resolveRoutingSource(response, plexorMeta);
59
+ const cohort = this._resolveCohort(response, plexorMeta);
60
+
61
+ // Need at least provider or model to emit anything useful
62
+ if (!provider && !model) {
63
+ return null;
64
+ }
65
+
66
+ const target = [provider, model].filter(Boolean).join('/');
67
+ const parts = [target];
68
+
69
+ if (latencyMs !== null) {
70
+ parts.push(`${latencyMs}ms`);
71
+ }
72
+
73
+ if (routingSource) {
74
+ parts.push(routingSource);
75
+ }
76
+
77
+ let line = parts.join(', ');
78
+
79
+ if (cohort) {
80
+ line += ` | ${cohort}`;
81
+ }
82
+
83
+ return `[PLEXOR: ${line}]`;
84
+ }
85
+
86
+ /**
87
+ * Phase 3: Detect zero-tool escalation signals in the response.
88
+ * Fires when agent_halt is set or escalation_chain / fallback provider data
89
+ * indicates a provider switch due to tool incapability.
90
+ *
91
+ * @param {object} response
92
+ * @param {object} [plexorMeta]
93
+ * @returns {string|null} — escalation message or null
94
+ */
95
+ buildEscalationNotice(response, plexorMeta) {
96
+ if (!response || typeof response !== 'object') {
97
+ return null;
98
+ }
99
+
100
+ const agentHalt = this._toBool(
101
+ response?.plexor_agent_halt ??
102
+ response?.plexor?.agent_halt ??
103
+ plexorMeta?.agent_halt
104
+ );
105
+
106
+ const escalationChain =
107
+ response?.plexor_escalation_chain ||
108
+ response?.plexor?.escalation_chain ||
109
+ plexorMeta?.escalation_chain ||
110
+ null;
111
+
112
+ const fallbackUsed = this._toBool(
113
+ response?.plexor_fallback_used ??
114
+ response?.fallback_used ??
115
+ response?.plexor?.fallback_used
116
+ );
117
+
118
+ const originalProvider =
119
+ response?.plexor_original_provider ||
120
+ response?.plexor?.original_provider ||
121
+ plexorMeta?.original_provider ||
122
+ null;
123
+
124
+ const currentProvider = this._resolveProvider(response, plexorMeta);
125
+
126
+ // Case 1: Explicit escalation chain present (e.g., ["openai-mini", "openai"])
127
+ if (Array.isArray(escalationChain) && escalationChain.length >= 2) {
128
+ const from = escalationChain[0];
129
+ const to = escalationChain[escalationChain.length - 1];
130
+ return `[PLEXOR: Zero-tool escalation: ${from} \u2192 ${to}]`;
131
+ }
132
+
133
+ // Case 2: agent_halt with fallback — provider switched
134
+ if (agentHalt && fallbackUsed && originalProvider && currentProvider && originalProvider !== currentProvider) {
135
+ return `[PLEXOR: Zero-tool escalation: ${originalProvider} \u2192 ${currentProvider}]`;
136
+ }
137
+
138
+ // Case 3: agent_halt alone (escalation happened but we may not know the full chain)
139
+ if (agentHalt && fallbackUsed) {
140
+ const from = originalProvider || 'original';
141
+ const to = currentProvider || 'fallback';
142
+ return `[PLEXOR: Zero-tool escalation: ${from} \u2192 ${to}]`;
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Phase 4: Detect scaffolding gate blocks.
150
+ * Fires when scaffolding_gate_blocked is present in the response,
151
+ * indicating a model was blocked by the scaffolding gate and an
152
+ * alternative was used.
153
+ *
154
+ * @param {object} response
155
+ * @param {object} [plexorMeta]
156
+ * @returns {string|null}
157
+ */
158
+ buildScaffoldingGateNotice(response, plexorMeta) {
159
+ if (!response || typeof response !== 'object') {
160
+ return null;
161
+ }
162
+
163
+ const gateBlocked = this._toBool(
164
+ response?.plexor_scaffolding_gate_blocked ??
165
+ response?.scaffolding_gate_blocked ??
166
+ response?.plexor?.scaffolding_gate_blocked ??
167
+ plexorMeta?.scaffolding_gate_blocked
168
+ );
169
+
170
+ if (!gateBlocked) {
171
+ return null;
172
+ }
173
+
174
+ const blockedModel =
175
+ response?.plexor_scaffolding_blocked_model ||
176
+ response?.plexor?.scaffolding_blocked_model ||
177
+ plexorMeta?.scaffolding_blocked_model ||
178
+ response?.plexor_original_model ||
179
+ response?.plexor?.original_model ||
180
+ plexorMeta?.original_model ||
181
+ 'model';
182
+
183
+ const alternative =
184
+ response?.plexor_selected_model ||
185
+ response?.plexor?.selected_model ||
186
+ plexorMeta?.recommended_model ||
187
+ response?.model ||
188
+ 'alternative';
189
+
190
+ return `[PLEXOR: Scaffolding gate: ${blockedModel} blocked, using ${alternative}]`;
191
+ }
192
+
193
+ /**
194
+ * Emit all applicable supervisor lines to stderr if enabled.
195
+ *
196
+ * @param {object} response
197
+ * @param {object} [plexorMeta]
198
+ */
199
+ emit(response, plexorMeta) {
200
+ if (!this.enabled) {
201
+ return;
202
+ }
203
+
204
+ // Phase 4: Scaffolding gate (highest priority — emit first if present)
205
+ const scaffoldingNotice = this.buildScaffoldingGateNotice(response, plexorMeta);
206
+ if (scaffoldingNotice) {
207
+ process.stderr.write(`${RED}${scaffoldingNotice}${RESET}\n`);
208
+ }
209
+
210
+ // Phase 3: Escalation notice
211
+ const escalationNotice = this.buildEscalationNotice(response, plexorMeta);
212
+ if (escalationNotice) {
213
+ process.stderr.write(`${YELLOW}${escalationNotice}${RESET}\n`);
214
+ }
215
+
216
+ // Phase 2: Enhanced routing summary (always emitted when data available)
217
+ const summary = this.buildSummary(response, plexorMeta);
218
+ if (summary) {
219
+ process.stderr.write(`${CYAN}${summary}${RESET}\n`);
220
+ }
221
+ }
222
+
223
+ // ---- private helpers ----
224
+
225
+ _resolveProvider(response, meta) {
226
+ return (
227
+ response?.plexor_provider_used ||
228
+ response?.plexor?.provider_used ||
229
+ meta?.recommended_provider ||
230
+ null
231
+ );
232
+ }
233
+
234
+ _resolveModel(response, meta) {
235
+ return (
236
+ response?.plexor_selected_model ||
237
+ response?.plexor?.selected_model ||
238
+ meta?.recommended_model ||
239
+ response?.model ||
240
+ null
241
+ );
242
+ }
243
+
244
+ _resolveLatency(response, meta) {
245
+ const raw =
246
+ response?.plexor_latency_ms ??
247
+ response?.plexor?.latency_ms ??
248
+ meta?.latency_ms ??
249
+ null;
250
+ if (raw === null || raw === undefined) {
251
+ return null;
252
+ }
253
+ const n = Number(raw);
254
+ return Number.isFinite(n) ? Math.round(n) : null;
255
+ }
256
+
257
+ _resolveRoutingSource(response, meta) {
258
+ return (
259
+ response?.plexor_routing_source ||
260
+ response?.plexor?.routing_source ||
261
+ meta?.source ||
262
+ null
263
+ );
264
+ }
265
+
266
+ _resolveCohort(response, meta) {
267
+ return (
268
+ response?.plexor_cohort ||
269
+ response?.plexor?.cohort ||
270
+ meta?.cohort ||
271
+ null
272
+ );
273
+ }
274
+
275
+ _toBool(value) {
276
+ if (value === true || value === 'true' || value === '1' || value === 1) return true;
277
+ if (value === false || value === 'false' || value === '0' || value === 0) return false;
278
+ return null;
279
+ }
280
+ }
281
+
282
+ module.exports = { SupervisorEmitter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexor-dev/claude-code-plugin-staging",
3
- "version": "0.1.0-beta.26",
3
+ "version": "0.1.0-beta.27",
4
4
  "description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
5
5
  "main": "lib/constants.js",
6
6
  "bin": {