auxilo-mcp 0.9.11 → 0.9.13

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.
@@ -12,7 +12,6 @@
12
12
  * runner no-ops immediately (runner.js checks it). runner.js also sets it in-process
13
13
  * before we're called; we set it on the child explicitly as belt-and-suspenders.
14
14
  */
15
- const { spawnSync } = require('child_process');
16
15
  const fs = require('fs');
17
16
  const path = require('path');
18
17
  const os = require('os');
@@ -24,6 +23,8 @@ const {
24
23
  rankIndexRowsForCandidate,
25
24
  estimatePromptTokens,
26
25
  } = require('../lib/extraction-index.js');
26
+ const providers = require('./providers/index.js');
27
+ const claudeCodeProvider = require('./providers/claude-code.js');
27
28
 
28
29
  // CI-5 (PUNCH-LIST §30, 2026-07-19): Auxilo is TECHNICAL-ONLY. The learning
29
30
  // taxonomy is these six tech categories; `communication` and `content-generation`
@@ -36,20 +37,36 @@ const PRIVATE_CATEGORIES = [...CATEGORIES, 'non-technical'];
36
37
  const RETIRED_CATEGORIES = ['communication', 'content-generation'];
37
38
 
38
39
  /**
39
- * SPEC3 slice A1 gate — score-at-extraction, BUILT BUT DARK by default.
40
+ * SPEC3 slice A1 gate — score-at-extraction, ON BY DEFAULT since 0.9.12
41
+ * (CLEAN-LANE-FLIP Phase B). Explicit opt-out only: AUXILO_SCORE_EXTRACTION=0
42
+ * (or 'false') disables scoring; any other value, including unset, scores.
40
43
  *
41
- * ┌─ CRITICAL SEQUENCING CONSTRAINT (SPEC3-BUILDER-REVIEW-LOOP §3.1/§8) ──────┐
42
- * │ Under a server WITHOUT the B1 extraction-channel hold, a clean /learn │
43
- * │ submission carrying a floor-passing quality_self_assessment publishes │
44
- * │ IMMEDIATELY (seamlessEligible). Turning this gate on against such a │
45
- * │ server silently flips hook extraction from "everything held" to "clean │
46
- * │ items auto-publish" — an unrecorded consent change (2026-06-10 class). │
47
- * │ Do NOT set AUXILO_SCORE_EXTRACTION=1 until the server holds │
48
- * │ submission_channel:'extraction' items behind standing consent (B1). │
49
- * └───────────────────────────────────────────────────────────────────────────┘
44
+ * ┌─ SEQUENCING CONSTRAINT — SATISFIED (SPEC3-BUILDER-REVIEW-LOOP §3.1/§8) ──┐
45
+ * │ This gate shipped dark (opt-in via AUXILO_SCORE_EXTRACTION=1) because │
46
+ * │ under a server WITHOUT the B1 extraction-channel hold, a clean /learn │
47
+ * │ submission carrying a floor-passing quality_self_assessment published │
48
+ * │ IMMEDIATELY (seamlessEligible) — arming scoring would have silently │
49
+ * │ flipped hook extraction from "everything held" to "clean items │
50
+ * │ auto-publish", an unrecorded consent change (2026-06-10 class). │
51
+ * │ │
52
+ * │ B1 SHIPPED (lib/clean-lane.js; server.js /learn): every submission │
53
+ * │ carrying submission_channel:'extraction' (runner.js stamps it │
54
+ * │ unconditionally) is HELD with reason 'standing_consent_off' unless the │
55
+ * │ account holds an active standing-consent grant — regardless of the │
56
+ * │ score. The server brake, not the absence of a score, is what keeps │
57
+ * │ unconsented items out of the catalog, so default-on scoring is safe: │
58
+ * │ the score can only move a held item into the ready_to_publish lane (one │
59
+ * │ counted approve away) or, under an active grant, into the clean lane the │
60
+ * │ contributor explicitly opted into. Without a score every extraction │
61
+ * │ submission held awaiting_quality forever, so a stock install could never │
62
+ * │ enter the clean lane at all — that was the Phase B defect this closes. │
63
+ * └──────────────────────────────────────────────────────────────────────────┘
50
64
  */
51
65
  function scoreExtractionEnabled(env = process.env) {
52
- return env.AUXILO_SCORE_EXTRACTION === '1';
66
+ const raw = env.AUXILO_SCORE_EXTRACTION;
67
+ if (raw === undefined || raw === null) return true;
68
+ const v = String(raw).trim().toLowerCase();
69
+ return !(v === '0' || v === 'false');
53
70
  }
54
71
 
55
72
  const EXTRACTION_PROMPT_BASE = `You are extracting reusable OPERATIONAL LEARNINGS from an AI agent's session transcript, to publish to a PUBLIC knowledge marketplace read by other AI agents.
@@ -132,158 +149,10 @@ function buildExtractionPrompt(opts = {}) {
132
149
  /** Back-compat export: the default (gate-evaluated-at-call) prompt. */
133
150
  const EXTRACTION_PROMPT = buildExtractionPrompt({ scoreExtraction: false });
134
151
 
135
- /** Resolve the `claude` binary — hook/launchd env may have a minimal PATH. */
136
- function resolveClaudeBin(opts = {}) {
137
- const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
138
- const existsSync = typeof opts.existsSync === 'function' ? opts.existsSync : fs.existsSync;
139
- const candidates = [
140
- path.join(homeDir, '.claude', 'local', 'claude'),
141
- '/usr/local/bin/claude',
142
- '/opt/homebrew/bin/claude',
143
- path.join(homeDir, '.local', 'bin', 'claude'),
144
- ];
145
- for (const c of candidates) {
146
- try {
147
- if (existsSync(c)) return c;
148
- } catch (_) { /* ignore */ }
149
- }
150
- // Absolute launchd fallbacks are absent; let PATH resolve the final option.
151
- return 'claude';
152
- }
153
-
154
- /** Build the subscription-auth-only environment shared by Claude CLI children. */
155
- function claudeChildEnv() {
156
- const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
157
- delete childEnv.ANTHROPIC_API_KEY;
158
- return childEnv;
159
- }
160
-
161
- /**
162
- * Ask Claude Code for its authoritative local auth state. Only the boolean
163
- * `loggedIn` field is classified; every other outcome is unknown so callers
164
- * can fall through to the real model invocation as the classifier of record.
165
- */
166
- function checkClaudeAuthStatus(opts = {}) {
167
- const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function'
168
- ? opts.spawnSyncImpl
169
- : spawnSync;
170
- const bin = typeof opts.claudeBin === 'string'
171
- ? opts.claudeBin
172
- : resolveClaudeBin();
173
- let res;
174
- try {
175
- res = spawnSyncImpl(bin, ['auth', 'status'], {
176
- encoding: 'utf-8',
177
- env: claudeChildEnv(),
178
- timeout: 5000,
179
- maxBuffer: 1024 * 1024,
180
- });
181
- } catch {
182
- return 'unknown';
183
- }
184
- if (!res || res.error || res.status !== 0) return 'unknown';
185
- try {
186
- const status = JSON.parse(String(res.stdout || ''));
187
- if (!status || typeof status.loggedIn !== 'boolean') return 'unknown';
188
- return status.loggedIn ? 'logged-in' : 'logged-out';
189
- } catch {
190
- return 'unknown';
191
- }
192
- }
193
-
194
- /**
195
- * Invoke the local Claude Code model headlessly (uses the USER's subscription auth).
196
- * Prompt+transcript go via stdin. Returns { ok, out, reason } plus auth/cause
197
- * metadata — never throws, so the SessionEnd hook degrades gracefully (the
198
- * proactive auxilo_contribute path is the reliable primary; this deterministic
199
- * hook is best-effort).
200
- */
201
- function extractWithClaudeCode(transcript, opts = {}) {
202
- const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function'
203
- ? opts.spawnSyncImpl
204
- : spawnSync;
205
- const bin = typeof opts.claudeBin === 'string'
206
- ? opts.claudeBin
207
- : resolveClaudeBin();
208
- const authStatus = checkClaudeAuthStatus({ spawnSyncImpl, claudeBin: bin });
209
- if (authStatus === 'logged-out') {
210
- return {
211
- ok: false,
212
- out: '',
213
- reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
214
- reasonCode: 'cli-unauthenticated',
215
- authStatus,
216
- };
217
- }
218
- const prompt = typeof opts.prompt === 'string'
219
- ? opts.prompt
220
- : buildExtractionPrompt({
221
- previousLessonsSection: opts.previousLessonsSection,
222
- captureVisibility: opts.captureVisibility,
223
- ...(opts.scoreExtraction !== undefined && { scoreExtraction: opts.scoreExtraction }),
224
- });
225
- const input = prompt + String(transcript).slice(0, 200000);
226
- // Do NOT pass ANTHROPIC_API_KEY through — we want the user's logged-in Claude
227
- // subscription (OAuth), not an API key (which would bill someone). Delete it.
228
- let res;
229
- try {
230
- res = spawnSyncImpl(bin, ['-p'], {
231
- input,
232
- encoding: 'utf-8',
233
- env: claudeChildEnv(),
234
- timeout: 120000,
235
- maxBuffer: 20 * 1024 * 1024,
236
- });
237
- } catch (error) {
238
- return {
239
- ok: false,
240
- out: '',
241
- reason: `spawn failed (${bin}): ${error.message}`,
242
- reasonCode: 'unknown',
243
- authStatus,
244
- };
245
- }
246
- if (!res) {
247
- return {
248
- ok: false,
249
- out: '',
250
- reason: `spawn failed (${bin}): no process result`,
251
- reasonCode: 'unknown',
252
- authStatus,
253
- };
254
- }
255
- const out = String(res.stdout || '');
256
- if (res.error) {
257
- return {
258
- ok: false,
259
- out: '',
260
- reason: `spawn failed (${bin}): ${res.error.message}`,
261
- reasonCode: 'unknown',
262
- authStatus,
263
- };
264
- }
265
- // Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
266
- if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
267
- return {
268
- ok: false,
269
- out,
270
- reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
271
- reasonCode: 'cli-unauthenticated',
272
- authStatus,
273
- ...(authStatus === 'logged-in' && { authDiscrepancy: true }),
274
- };
275
- }
276
- if (res.status !== 0) {
277
- return {
278
- ok: false,
279
- out,
280
- reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}`,
281
- reasonCode: 'model-error',
282
- authStatus,
283
- };
284
- }
285
- return { ok: true, out, reason: null, authStatus };
286
- }
152
+ // resolveClaudeBin, claudeChildEnv, checkClaudeAuthStatus, extractWithClaudeCode
153
+ // MOVED to scripts/providers/claude-code.js (EXTRACT-PER-CLIENT W1 PART A).
154
+ // Re-exported below (module.exports) from claudeCodeProvider — required because
155
+ // test/ext-0806b-silent-skip.test.js imports them directly from this module.
287
156
 
288
157
  /**
289
158
  * A1: validate a model-emitted quality_self_assessment. Returns the normalized
@@ -507,59 +376,97 @@ ${JSON.stringify(payload)}`;
507
376
  return { prompt, rankings };
508
377
  }
509
378
 
379
+ // invokeJudgeWithClaudeCode MOVED into scripts/providers/claude-code.js's
380
+ // runModel(mode:'judge') (EXTRACT-PER-CLIENT W1 PART A). Not re-exported here —
381
+ // grep confirmed no test imports it directly from this module (no dead surface
382
+ // carried).
383
+ //
384
+ // judgeUsage's Claude/Anthropic-specific summing (input_tokens +
385
+ // cache_creation_input_tokens + cache_read_input_tokens) moved with it, into
386
+ // claude-code.js's normalizeJudgeUsage() — that's the provider-specific half.
387
+ // This slimmer, provider-agnostic remainder is what's left once a provider's
388
+ // runModel already returns usage normalized to {input_tokens, output_tokens}
389
+ // (or null): apply the SAME text-length-estimate fallback the original judgeUsage
390
+ // used, generically, for whichever provider ran.
510
391
  function judgeUsage(usage, prompt, completion) {
511
392
  const numeric = usage && typeof usage === 'object' ? usage : {};
512
393
  const directInput = Number(numeric.input_tokens) || 0;
513
- const cacheCreation = Number(numeric.cache_creation_input_tokens) || 0;
514
- const cacheRead = Number(numeric.cache_read_input_tokens) || 0;
515
394
  const output = Number(numeric.output_tokens) || 0;
516
395
  return {
517
- prompt_tokens: directInput + cacheCreation + cacheRead ||
518
- estimatePromptTokens(prompt),
396
+ prompt_tokens: directInput || estimatePromptTokens(prompt),
519
397
  completion_tokens: output || estimatePromptTokens(completion),
520
398
  };
521
399
  }
522
400
 
523
- function invokeJudgeWithClaudeCode(prompt) {
524
- const bin = resolveClaudeBin();
525
- const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
526
- delete childEnv.ANTHROPIC_API_KEY;
527
- const res = spawnSync(
528
- bin,
529
- ['-p', '--output-format', 'json', '--no-session-persistence', '--tools', ''],
530
- {
531
- input: prompt,
532
- encoding: 'utf8',
533
- env: childEnv,
534
- timeout: 120000,
535
- maxBuffer: 20 * 1024 * 1024,
536
- }
537
- );
538
- const stdout = String(res.stdout || '');
539
- if (res.error) {
540
- return { ok: false, out: '', reason: `judge spawn failed (${bin}): ${res.error.message}` };
541
- }
542
- if (/Please run \/login|authentication_error|401/i.test(stdout) ||
543
- /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
544
- return { ok: false, out: '', reason: 'local judge model is not authenticated' };
545
- }
546
- if (res.status !== 0) {
547
- return {
548
- ok: false,
549
- out: '',
550
- reason: `local judge exited ${res.status}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
551
- };
401
+ /**
402
+ * PART C — resolve the extraction_model identity for a runModel result.
403
+ * Prefers the additive `identity` field a provider's runModel result may
404
+ * carry (byo-key.js always sets one: {provider:'byo-key', model, version,
405
+ * vendor}). claude-code.js/codex-cli.js shipped (PART A/B) before this stamp
406
+ * existed and don't set one — rather than touch those provider modules
407
+ * (outside this part's disjoint file scope, AGENTS.md's one-build rule),
408
+ * this falls back to the resolved provider id alone (model/version/vendor
409
+ * null) so every provider gets SOME stamp, never silently none. Best-effort:
410
+ * a resolution failure here must never block extraction itself.
411
+ */
412
+ async function resolveExtractionModelIdentity(runModelResult, opts) {
413
+ if (runModelResult && runModelResult.identity && typeof runModelResult.identity === 'object') {
414
+ return runModelResult.identity;
552
415
  }
553
- let wrapper;
554
416
  try {
555
- wrapper = JSON.parse(stdout);
556
- } catch {
557
- return { ok: false, out: '', reason: 'local judge returned malformed JSON wrapper' };
558
- }
559
- if (!wrapper || typeof wrapper.result !== 'string' || wrapper.is_error === true) {
560
- return { ok: false, out: '', reason: 'local judge returned no successful result' };
561
- }
562
- return { ok: true, out: wrapper.result, usage: wrapper.usage || null, reason: null };
417
+ const resolved = await providers.resolveProvider(opts);
418
+ if (resolved && resolved.ok && resolved.id) {
419
+ return { provider: resolved.id, model: null, version: null, vendor: null };
420
+ }
421
+ } catch { /* identity is best-effort; never block extraction on it */ }
422
+ return null;
423
+ }
424
+
425
+ /**
426
+ * Default invokeModel (extractLocally) — routes through the resolved provider's
427
+ * runModel(mode:'extract'), mapped back to the legacy {ok, out, reason,
428
+ * reasonCode, authStatus} shape extractLocally's caller already expects. opts
429
+ * (spawnSyncImpl/claudeBin/etc.) thread straight through so the injection
430
+ * pattern used by every extractLocally test still works when a test exercises
431
+ * this default path instead of supplying its own opts.invokeModel.
432
+ */
433
+ async function defaultInvokeModel(transcript, invokeOpts, opts) {
434
+ const result = await providers.runModel({
435
+ ...opts,
436
+ prompt: invokeOpts.prompt,
437
+ input: transcript,
438
+ timeoutMs: 120000,
439
+ log: opts.log,
440
+ mode: 'extract',
441
+ });
442
+ return {
443
+ ok: result.ok,
444
+ out: result.text,
445
+ reason: result.reason,
446
+ reasonCode: result.reasonCode,
447
+ authStatus: result.authStatus,
448
+ extractionModel: await resolveExtractionModelIdentity(result, opts),
449
+ ...(result.authDiscrepancy !== undefined && { authDiscrepancy: result.authDiscrepancy }),
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Default invokeJudge (runAnchoredJudge) — routes through the resolved
455
+ * provider's runModel(mode:'judge'), mapped back to the legacy {ok, out, usage,
456
+ * reason} shape runAnchoredJudge already consumes. Same opts thread-through as
457
+ * defaultInvokeModel above.
458
+ */
459
+ function defaultInvokeJudge(opts) {
460
+ return async (prompt) => {
461
+ const result = await providers.runModel({
462
+ ...opts,
463
+ prompt,
464
+ timeoutMs: 120000,
465
+ log: opts.log,
466
+ mode: 'judge',
467
+ });
468
+ return { ok: result.ok, out: result.text, usage: result.usage, reason: result.reason };
469
+ };
563
470
  }
564
471
 
565
472
  function parseJudgeDecisions(raw, candidates, rankings) {
@@ -614,7 +521,7 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
614
521
  if (rankings.some((rows) => rows.length === 0)) return empty;
615
522
  const invokeJudge = typeof opts.invokeJudge === 'function'
616
523
  ? opts.invokeJudge
617
- : invokeJudgeWithClaudeCode;
524
+ : defaultInvokeJudge(opts);
618
525
  let result;
619
526
  try {
620
527
  result = await invokeJudge(prompt, { candidates: input, rankings });
@@ -749,7 +656,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
749
656
  });
750
657
  const invokeModel = typeof opts.invokeModel === 'function'
751
658
  ? opts.invokeModel
752
- : (text, invokeOpts) => extractWithClaudeCode(text, invokeOpts);
659
+ : (text, invokeOpts) => defaultInvokeModel(text, invokeOpts, opts);
753
660
  const modelResult = await invokeModel(transcript, { prompt });
754
661
  const { ok, out, reason } = modelResult;
755
662
  if (!ok) {
@@ -763,13 +670,21 @@ async function extractLocally(transcript, sourceType, opts = {}) {
763
670
  }),
764
671
  };
765
672
  }
673
+ // PART C: which provider/model actually ran this extraction, stamped onto
674
+ // every learning it produces (below). Absent when the caller supplied its
675
+ // own opts.invokeModel that doesn't report one (every existing test does
676
+ // this) — extraction_model then simply never appears, byte-identical to
677
+ // pre-PART-C behavior.
678
+ const extractionModel = modelResult.extractionModel || null;
766
679
  const parsed = parseExtractionOutput(out, opts);
767
680
  const promptDropResult = applyPromptMemoryDrops(
768
681
  parsed.prompt_drops,
769
682
  promptMemory,
770
683
  opts
771
684
  );
772
- const candidates = [...parsed.learnings, ...promptDropResult.restored];
685
+ const candidates = [...parsed.learnings, ...promptDropResult.restored].map((l) => (
686
+ extractionModel ? { ...l, extraction_model: extractionModel } : l
687
+ ));
773
688
 
774
689
  // Re-read immediately before the lexical filter. If the index is deleted,
775
690
  // becomes unreadable, or is corrupted while the model runs, the filter is
@@ -824,11 +739,18 @@ async function extractLocally(transcript, sourceType, opts = {}) {
824
739
  }
825
740
 
826
741
  module.exports = {
827
- extractLocally, extractWithClaudeCode, checkClaudeAuthStatus, EXTRACTABLE_SOURCES, EXTRACTABLE_SOURCE_IDS,
828
- parseLearnings, parseExtractionOutput, resolveClaudeBin,
742
+ extractLocally, EXTRACTABLE_SOURCES, EXTRACTABLE_SOURCE_IDS,
743
+ parseLearnings, parseExtractionOutput,
829
744
  CATEGORIES, PRIVATE_CATEGORIES, RETIRED_CATEGORIES,
830
745
  EXTRACTION_PROMPT, buildExtractionPrompt, scoreExtractionEnabled,
831
746
  validateQualityAssessment, QUALITY_DIMENSIONS,
832
747
  buildAnchoredJudgePrompt, parseJudgeDecisions, runAnchoredJudge,
833
- recordDropAudit, invokeJudgeWithClaudeCode, JUDGE_TOP_K,
748
+ recordDropAudit, JUDGE_TOP_K,
749
+ // Re-exported from scripts/providers/claude-code.js (moved there in PART A) —
750
+ // kept here ONLY because test/ext-0806b-silent-skip.test.js imports these
751
+ // three directly from this module (grep-confirmed; invokeJudgeWithClaudeCode
752
+ // and judgeUsage had no such importer and are NOT carried forward).
753
+ extractWithClaudeCode: claudeCodeProvider.extractWithClaudeCode,
754
+ checkClaudeAuthStatus: claudeCodeProvider.checkClaudeAuthStatus,
755
+ resolveClaudeBin: claudeCodeProvider.resolveClaudeBin,
834
756
  };