auxilo-mcp 0.9.14 → 0.9.15

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.
@@ -14,7 +14,16 @@ const os = require('node:os');
14
14
  const path = require('node:path');
15
15
  const { findNearDuplicate, scoreChannels } = require('./similarity.js');
16
16
 
17
- const DEFAULT_INDEX_PATH = path.join(os.homedir(), '.auxilo', 'extracted-index.jsonl');
17
+ // TEST-HOME-ISOLATION incident 2: same AUXILO_HOME-over-os.homedir() fallback
18
+ // as scripts/providers/index.js's PROVIDERS_STATE_PATH and
19
+ // scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
20
+ // not imported — see those files' docblocks). This makes the default resolve
21
+ // through the same seam isolation already uses elsewhere, so a caller that
22
+ // forgets an explicit indexPath is still isolated by construction whenever
23
+ // AUXILO_HOME is set (scripts/test/run-isolated.js sets both AUXILO_HOME and
24
+ // HOME, so this is defense-in-depth, not the primary guard — every call site
25
+ // should still pass an explicit indexPath; see test/no-real-home-writes.test.js).
26
+ const DEFAULT_INDEX_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'extracted-index.jsonl');
18
27
  const HYDRATION_PAGE_SIZE = 500;
19
28
  const PROMPT_MEMORY_MAX_TOKENS = 1200;
20
29
  const PROMPT_MEMORY_MAX_ROWS = 40;
package/mcp-server.js CHANGED
@@ -198,7 +198,7 @@ async function postBulkChunks(headers, decisions) {
198
198
  }
199
199
 
200
200
  const server = new Server(
201
- { name: 'auxilo', version: '0.9.14' },
201
+ { name: 'auxilo', version: '0.9.15' },
202
202
  {
203
203
  capabilities: { tools: {} },
204
204
  instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auxilo-mcp",
3
- "version": "0.9.14",
3
+ "version": "0.9.15",
4
4
  "mcpName": "io.github.silent-architects/auxilo",
5
5
  "description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
6
6
  "main": "mcp-server.js",
@@ -32,7 +32,8 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "start": "node mcp-server.js",
35
- "test": "node --test test/*.test.js && node tests/test-mobile-nav-overlay.js"
35
+ "test": "node scripts/test/run-isolated.js",
36
+ "test:host": "node scripts/test/run-host.js"
36
37
  },
37
38
  "keywords": [
38
39
  "mcp",
@@ -458,6 +458,12 @@ async function defaultInvokeModel(transcript, invokeOpts, opts) {
458
458
  authStatus: result.authStatus,
459
459
  extractionModel: await resolveExtractionModelIdentity(result, opts),
460
460
  ...(result.authDiscrepancy !== undefined && { authDiscrepancy: result.authDiscrepancy }),
461
+ // EXTRACTION-RUN-LOG (0.9.15): additive passthrough for the one-line-per-run
462
+ // provider summary logged at the end of extractLocally() below. Only
463
+ // claude-code's runModel() currently sets these (argv/cliVersion); other
464
+ // providers simply omit them, which the log builder renders as 'n/a'.
465
+ ...(result.argv !== undefined && { argv: result.argv }),
466
+ ...(result.cliVersion !== undefined && { cliVersion: result.cliVersion }),
461
467
  };
462
468
  }
463
469
 
@@ -476,7 +482,20 @@ function defaultInvokeJudge(opts) {
476
482
  log: opts.log,
477
483
  mode: 'judge',
478
484
  });
479
- return { ok: result.ok, out: result.text, usage: result.usage, reason: result.reason };
485
+ return {
486
+ ok: result.ok,
487
+ out: result.text,
488
+ usage: result.usage,
489
+ reason: result.reason,
490
+ // EXTRACTION-RUN-LOG (0.9.15): threaded through so runAnchoredJudge below
491
+ // can report the judge call's status/argv/CLI version on the run summary
492
+ // log line. Additive — every existing invokeJudge stub in this repo's
493
+ // tests returns a subset of {ok,out,usage,reason} and these three simply
494
+ // come back undefined for them, changing nothing they assert.
495
+ reasonCode: result.reasonCode,
496
+ argv: result.argv,
497
+ cliVersion: result.cliVersion,
498
+ };
480
499
  };
481
500
  }
482
501
 
@@ -516,12 +535,26 @@ function parseJudgeDecisions(raw, candidates, rankings) {
516
535
 
517
536
  async function runAnchoredJudge(candidates, indexState, opts = {}) {
518
537
  const input = Array.isArray(candidates) ? candidates : [];
538
+ // judgeAttempted/judgeSucceeded (EXTRACTION-RUN-LOG, 0.9.15): additive
539
+ // status fields for the provider-run log line only — every field already on
540
+ // `empty` (kept/dropped/called/prompt_tokens/completion_tokens) keeps its
541
+ // exact pre-existing meaning and every existing caller's assertions on those
542
+ // fields are unaffected. judgeAttempted=true means invokeJudge was actually
543
+ // called (regardless of outcome); judgeSucceeded=true means it returned
544
+ // ok:true AND its output parsed into real decisions (the one path that
545
+ // actually judged anything, as opposed to failing open and keeping
546
+ // everything). This is a finer distinction than `called` — `called` is
547
+ // true even for a malformed-JSON response (an attempted-and-failed case) —
548
+ // so the log builder uses these two instead of `called` to tell
549
+ // "ran"/"failed"/"skipped(no-candidates)" apart accurately.
519
550
  const empty = {
520
551
  kept: input.slice(),
521
552
  dropped: [],
522
553
  called: false,
523
554
  prompt_tokens: 0,
524
555
  completion_tokens: 0,
556
+ judgeAttempted: false,
557
+ judgeSucceeded: false,
525
558
  };
526
559
  if (!input.length || !indexState || !indexState.usable ||
527
560
  !Array.isArray(indexState.rows) || !indexState.rows.length) {
@@ -538,17 +571,29 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
538
571
  result = await invokeJudge(prompt, { candidates: input, rankings });
539
572
  } catch (error) {
540
573
  loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${error.message}`);
541
- return empty;
574
+ return { ...empty, judgeAttempted: true };
542
575
  }
543
576
  if (!result || !result.ok) {
544
577
  loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${result && result.reason ? result.reason : 'unknown error'}`);
545
- return empty;
578
+ return {
579
+ ...empty,
580
+ judgeAttempted: true,
581
+ judgeReasonCode: result && result.reasonCode,
582
+ judgeArgv: result && result.argv,
583
+ judgeCliVersion: result && result.cliVersion,
584
+ };
546
585
  }
547
586
  const parsed = parseJudgeDecisions(result.out, input, rankings);
548
587
  const usage = judgeUsage(result.usage, prompt, result.out);
588
+ const judgeMeta = {
589
+ judgeAttempted: true,
590
+ judgeReasonCode: result.reasonCode,
591
+ judgeArgv: result.argv,
592
+ judgeCliVersion: result.cliVersion,
593
+ };
549
594
  if (!parsed.ok) {
550
595
  loudLocal(opts, `anchored judge malformed; keeping all candidates: ${parsed.reason}`);
551
- return { ...empty, called: true, ...usage };
596
+ return { ...empty, called: true, ...usage, ...judgeMeta, judgeSucceeded: false };
552
597
  }
553
598
 
554
599
  const kept = [];
@@ -575,7 +620,7 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
575
620
  kept.push(input[index]);
576
621
  }
577
622
  }
578
- return { kept, dropped, called: true, ...usage };
623
+ return { kept, dropped, called: true, ...usage, ...judgeMeta, judgeSucceeded: true };
579
624
  }
580
625
 
581
626
  /**
@@ -630,6 +675,68 @@ function immutableSet(ids) {
630
675
 
631
676
  const EXTRACTABLE_SOURCES = immutableSet(EXTRACTABLE_SOURCE_IDS);
632
677
 
678
+ // ─── Provider-run log line (EXTRACTION-RUN-LOG, PUNCH-LIST P3, 0.9.15) ─────
679
+ //
680
+ // One line per extractLocally() run, written to the runner's log at the point
681
+ // each run completes — never prompt/transcript content. reasonCode/argv/
682
+ // cliVersion are provider-internal diagnostics only (every provider's own
683
+ // `reason` string is secret-free by the same contract runModel() documents in
684
+ // scripts/providers/index.js), so this line carries no learning text.
685
+ const PRE_SPAWN_SKIP_REASON_CODES = new Set([
686
+ 'cli-unauthenticated',
687
+ 'cli-not-installed',
688
+ 'cli-billing-helper-configured',
689
+ 'cli-settings-isolation-unsupported',
690
+ 'provider-not-configured',
691
+ 'providers-file-mode-unsafe',
692
+ 'provider-not-installed',
693
+ 'no-usable-provider',
694
+ 'no-model-provider-available',
695
+ ]);
696
+
697
+ function formatArgvForLog(argv) {
698
+ if (!Array.isArray(argv) || !argv.length) return 'n/a';
699
+ return argv.map((arg) => (arg === '' ? "''" : arg)).join(' ');
700
+ }
701
+
702
+ /**
703
+ * `finder` and `judge` report whether each call was actually attempted
704
+ * (spawned) this run, not whether it succeeded — a spawn that ran and then
705
+ * hit a model error still counts as "ran" (it happened; the failure is in
706
+ * `reason`, not in whether isolation applied). `hooks` is `claude-code`-
707
+ * specific: 'isolated' whenever a claude-code spawn this run carried
708
+ * --setting-sources (the only state a spawn can be in per the fail-closed
709
+ * gate in scripts/providers/claude-code.js — it never spawns without the
710
+ * flag), 'unsupported' when the CLI was found not to support the flag at
711
+ * all, 'n/a' for a non-claude-code provider (codex-cli/byo-key isolate by a
712
+ * different mechanism entirely, out of this row's scope).
713
+ */
714
+ function logProviderRunSummary(opts, runId, modelResult, judged) {
715
+ try {
716
+ const log = typeof opts.log === 'function' ? opts.log : console.error;
717
+ const provider = (modelResult.extractionModel && modelResult.extractionModel.provider) || 'unknown';
718
+ const finderRan = Boolean(modelResult.ok) || !PRE_SPAWN_SKIP_REASON_CODES.has(modelResult.reasonCode);
719
+ const finder = finderRan ? 'ran' : 'skipped';
720
+ let judgeState;
721
+ if (!judged || !judged.judgeAttempted) judgeState = 'skipped(no-candidates)';
722
+ else if (judged.judgeSucceeded) judgeState = 'ran';
723
+ else judgeState = 'failed';
724
+ const argv = (finderRan && modelResult.argv) || (judged && judged.judgeArgv) || null;
725
+ const cliVersion = modelResult.cliVersion || (judged && judged.judgeCliVersion) || null;
726
+ const finderUnsupported = modelResult.reasonCode === 'cli-settings-isolation-unsupported';
727
+ const judgeUnsupported = Boolean(judged && judged.judgeReasonCode === 'cli-settings-isolation-unsupported');
728
+ const hooks = provider === 'claude-code'
729
+ ? ((finderUnsupported || judgeUnsupported) ? 'unsupported' : 'isolated')
730
+ : 'n/a';
731
+ log(
732
+ `[providers] run=${runId || 'unknown'} provider=${provider} cli=${cliVersion || '-'} ` +
733
+ `finder=${finder} judge=${judgeState} flags=${formatArgvForLog(argv)} hooks=${hooks}`
734
+ );
735
+ } catch {
736
+ // Logging must never block or fail extraction.
737
+ }
738
+ }
739
+
633
740
  async function extractLocally(transcript, sourceType, opts = {}) {
634
741
  if (sourceType && !EXTRACTABLE_SOURCES.has(sourceType)) {
635
742
  return { learnings: [], skipped: `local extraction not implemented for "${sourceType}" — agent contributes via auxilo_contribute` };
@@ -671,6 +778,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
671
778
  const modelResult = await invokeModel(transcript, { prompt });
672
779
  const { ok, out, reason } = modelResult;
673
780
  if (!ok) {
781
+ logProviderRunSummary(opts, opts.runId, modelResult, null);
674
782
  return {
675
783
  learnings: [],
676
784
  skipped: reason,
@@ -737,6 +845,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
737
845
  `lexical_filter=${lexicalDropped.length}, anchored_judge=${judged.dropped.length}`
738
846
  );
739
847
  }
848
+ logProviderRunSummary(opts, opts.runId, modelResult, judged);
740
849
  return {
741
850
  learnings: judged.kept,
742
851
  dedup_dropped: allDropped.length,
@@ -764,4 +873,6 @@ module.exports = {
764
873
  extractWithClaudeCode: claudeCodeProvider.extractWithClaudeCode,
765
874
  checkClaudeAuthStatus: claudeCodeProvider.checkClaudeAuthStatus,
766
875
  resolveClaudeBin: claudeCodeProvider.resolveClaudeBin,
876
+ // EXTRACTION-RUN-LOG (0.9.15) — exported for direct unit coverage.
877
+ formatArgvForLog, logProviderRunSummary, PRE_SPAWN_SKIP_REASON_CODES,
767
878
  };
@@ -37,8 +37,23 @@ const os = require('os');
37
37
  /** Same literal value as scripts/providers/index.js's PROVIDERS_STATE_PATH —
38
38
  * duplicated (not imported) to avoid a circular require (index.js loads this
39
39
  * module dynamically via loadOptionalProvider). test/byo-key-provider.test.js
40
- * pins the two paths equal. */
41
- const DEFAULT_PROVIDERS_STATE_PATH = path.join(os.homedir(), '.auxilo', 'providers.json');
40
+ * pins the two paths equal.
41
+ *
42
+ * TEST-HOME-ISOLATION: `AUXILO_HOME`, when set, wins over `os.homedir()` —
43
+ * a dedicated override for auxilo's own state directory, distinct from the
44
+ * general-purpose `HOME` every other os.homedir()-based path in this repo
45
+ * reads (settings.json, the VERSION stamp, credentials.json, ...). Per-call
46
+ * `opts.providersStatePath` (every test in this repo passes one) still wins
47
+ * over BOTH — this only narrows what an omitted opts falls back to, closing
48
+ * the gap that let a bare `bin/auxilo-cli.js provider status|clear` call (no
49
+ * opts seam of its own — see cmdProvider in bin/auxilo-cli.js) or any future
50
+ * test that forgets its own override reach the real ~/.auxilo/providers.json
51
+ * (TEST-HOME-ISOLATION incident, 2026-09-06). Evaluated once at module load,
52
+ * same as before — every entry point that cares (scripts/test/run-isolated.js
53
+ * for `npm test`, scripts/check-test-count.sh for the CI gate) sets both
54
+ * AUXILO_HOME and HOME before node starts, so this still resolves correctly
55
+ * even though it's a frozen constant, not a per-call lookup. */
56
+ const DEFAULT_PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
42
57
 
43
58
  const VENDOR_DEFAULT_BASE_URL = Object.freeze({
44
59
  anthropic: 'https://api.anthropic.com/v1',
@@ -41,6 +41,93 @@ function resolveClaudeBin(opts = {}) {
41
41
  return 'claude';
42
42
  }
43
43
 
44
+ // ─── Child settings/hooks isolation (EXTRACTION-CHILD-HOOKS, PUNCH-LIST P1,
45
+ // 0.9.15) ────────────────────────────────────────────────────────────────
46
+ //
47
+ // Every `claude -p` extraction child previously loaded the OPERATOR'S OWN
48
+ // ~/.claude/settings.json and therefore fired their personal SessionStart
49
+ // hooks (mandate.sh, session-context.sh, ...) — output that reaches the
50
+ // extraction/judge prompt without ever passing the package's scrubber. That
51
+ // is a privacy defect: content from the machine's own hook configuration
52
+ // (potentially personal notes, live queries, etc.) enters a transcript that
53
+ // gets sent to the resolved provider.
54
+ //
55
+ // `--setting-sources <sources>` ("Comma-separated list of setting sources to
56
+ // load (user, project, local)") is documented on both installed CLIs probed
57
+ // during investigation (2.1.12, 2.1.260) — scratchpad hooks-0914/
58
+ // EXTRACTION-CHILD-HOOKS-FINDINGS.md. This build (hooks-0915) tested LIVE
59
+ // whether an EMPTY list (`--setting-sources ''`) — the narrowest, fully
60
+ // cwd-independent value, loading none of user/project/local — is accepted:
61
+ // it is (2.1.12, exit 0, hook_response count 0, well-formed result). Shipping
62
+ // `''` means no fresh-temp-cwd workaround is needed: an empty source list
63
+ // loads nothing regardless of the child's cwd, unlike the `project,local`
64
+ // fallback (which still honors a target repo's own `.claude/settings.json`).
65
+ const SETTING_SOURCES_VALUE = '';
66
+ const SETTING_SOURCES_ARGS = Object.freeze(['--setting-sources', SETTING_SOURCES_VALUE]);
67
+
68
+ /**
69
+ * Fail-closed detection of "this CLI build doesn't understand
70
+ * --setting-sources at all" (an old install predating the flag, or a rename).
71
+ * Detected from the SAME spawn that already carries the flag — no extra
72
+ * `--help`/`--version` probe call, so the happy-path spawn count for every
73
+ * existing caller/test is unchanged. A CLI rejecting an unknown flag exits
74
+ * non-zero with a message naming the flag (commander.js-style "error:
75
+ * unknown option '--setting-sources'"); this pattern-matches for that
76
+ * specific shape rather than treating every non-zero exit as unsupported (a
77
+ * real model/auth error must NOT be misreported as isolation-unsupported).
78
+ * Cached module-wide for the lifetime of the process ("once per run"): the
79
+ * first spawn that hits this failure marks the CLI unsupported and every
80
+ * subsequent runModel() call in the same process short-circuits BEFORE
81
+ * spawning again — it must never spawn without the flag, and re-attempting a
82
+ * doomed spawn every call would be silent waste, not safety.
83
+ */
84
+ let cachedSettingSourcesUnsupported; // undefined = not yet observed; true once detected
85
+
86
+ function looksLikeUnsupportedSettingSourcesFlag(res) {
87
+ if (!res || res.status === 0) return false;
88
+ const combined = `${String(res.stdout || '')}\n${String(res.stderr || '')}`;
89
+ return /--setting-sources/.test(combined) && /\b(unknown|unrecognized|invalid)\b.{0,20}\b(option|argument|flag)\b/i.test(combined);
90
+ }
91
+
92
+ function settingSourcesIsolationUnsupportedResult(authStatus) {
93
+ return {
94
+ ok: false,
95
+ text: '',
96
+ usage: null,
97
+ reason: 'installed Claude Code CLI does not support --setting-sources; extraction declines to run a child that would load the operator\'s own settings/hooks unisolated',
98
+ reasonCode: 'cli-settings-isolation-unsupported',
99
+ authStatus: authStatus || 'unknown',
100
+ };
101
+ }
102
+
103
+ /** Test-only: reset the module-level isolation-support cache between fixtures. */
104
+ function _resetSettingSourcesCacheForTests() {
105
+ cachedSettingSourcesUnsupported = undefined;
106
+ }
107
+
108
+ // ─── CLI version, for diagnostics only (no subprocess spawn) ───────────────
109
+ //
110
+ // Resolves the installed package's own package.json version by following the
111
+ // resolved binary's real path (e.g. `/usr/local/bin/claude` -> `.../
112
+ // node_modules/@anthropic-ai/claude-code/cli.js`) and reading the sibling
113
+ // package.json — filesystem-only, so it never adds a spawn to the extraction
114
+ // path (verified live: realpath + package.json read, no `claude --version`
115
+ // call). Best-effort: any failure (bare `claude` unresolved via PATH, an
116
+ // install layout that doesn't carry a sibling package.json, a fixture path in
117
+ // tests) yields null, never throws.
118
+ function getClaudeCliVersion(bin, opts = {}) {
119
+ const realpathSyncImpl = typeof opts.realpathSyncImpl === 'function' ? opts.realpathSyncImpl : fs.realpathSync;
120
+ const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
121
+ try {
122
+ const real = realpathSyncImpl(bin);
123
+ const pkgPath = path.join(path.dirname(real), 'package.json');
124
+ const pkg = JSON.parse(readFileSyncImpl(pkgPath, 'utf8'));
125
+ return pkg && typeof pkg.version === 'string' ? pkg.version : null;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
44
131
  // ─── Env scrub (EXTRACT-TOOLS-LOCK, PUNCH-LIST) ────────────────────────────
45
132
  //
46
133
  // SME-confirmed list (claude-code-guide, verified against official docs and
@@ -79,6 +166,20 @@ const SCRUBBED_CLIENT_ENV_VARS = Object.freeze([
79
166
  'CLOUD_ML_REGION',
80
167
  ]);
81
168
 
169
+ // ─── Spawn argv (EXTRACTION-ZERO-TOOL-CALLS control) ───────────────────────
170
+ //
171
+ // Named + frozen so a byte-pinned test (test/extraction-zero-tool-calls.test.js,
172
+ // test/claude-code-provider.test.js) can assert the exact argv without
173
+ // duplicating the literal, and so a future flag change is a conscious,
174
+ // greppable edit here rather than a silent literal tweak buried in the two
175
+ // runXMode() functions below. No behavior change: the two spawnSyncImpl()
176
+ // call sites below now pass these constants instead of inline array literals
177
+ // of the identical contents. 0.9.15 (EXTRACTION-CHILD-HOOKS) appends
178
+ // SETTING_SOURCES_ARGS to both — the child loads none of user/project/local
179
+ // settings, so the operator's own SessionStart hooks never fire.
180
+ const EXTRACT_MODE_ARGV = Object.freeze(['-p', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
181
+ const JUDGE_MODE_ARGV = Object.freeze(['-p', '--output-format', 'json', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
182
+
82
183
  /**
83
184
  * Build the subscription-auth-only environment shared by BOTH the extraction and
84
185
  * judge Claude CLI children — one function, no drift between the two spawns.
@@ -297,6 +398,7 @@ function detect(opts = {}) {
297
398
  function runExtractMode(opts) {
298
399
  const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
299
400
  const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
401
+ if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
300
402
  const authStatus = checkAuthStatus({ spawnSyncImpl, claudeBin: bin, ...opts });
301
403
  if (authStatus === 'logged-out') {
302
404
  return {
@@ -310,11 +412,18 @@ function runExtractMode(opts) {
310
412
  }
311
413
  const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
312
414
  const stdin = prompt + String(opts.input || '').slice(0, 200000);
415
+ // --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
416
+ // judge spawn below — an extraction run leaves no session file behind either.
417
+ // --setting-sources '' (EXTRACTION-CHILD-HOOKS, 0.9.15): the child loads none
418
+ // of user/project/local settings, so the operator's own SessionStart hooks
419
+ // never fire and their output never reaches this prompt.
420
+ const argv = EXTRACT_MODE_ARGV;
421
+ const cliVersion = getClaudeCliVersion(bin, opts);
313
422
  let res;
314
423
  try {
315
424
  // --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
316
425
  // judge spawn below — an extraction run leaves no session file behind either.
317
- res = spawnSyncImpl(bin, ['-p', '--no-session-persistence', '--tools', ''], {
426
+ res = spawnSyncImpl(bin, argv, {
318
427
  input: stdin,
319
428
  encoding: 'utf-8',
320
429
  env: claudeChildEnv(),
@@ -322,14 +431,18 @@ function runExtractMode(opts) {
322
431
  maxBuffer: 20 * 1024 * 1024,
323
432
  });
324
433
  } catch (error) {
325
- return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus };
434
+ return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
326
435
  }
327
436
  if (!res) {
328
- return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus };
437
+ return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus, argv, cliVersion };
438
+ }
439
+ if (looksLikeUnsupportedSettingSourcesFlag(res)) {
440
+ cachedSettingSourcesUnsupported = true;
441
+ return { ...settingSourcesIsolationUnsupportedResult(authStatus), argv, cliVersion };
329
442
  }
330
443
  const out = String(res.stdout || '');
331
444
  if (res.error) {
332
- return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus };
445
+ return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
333
446
  }
334
447
  // Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
335
448
  if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
@@ -340,6 +453,8 @@ function runExtractMode(opts) {
340
453
  reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
341
454
  reasonCode: 'cli-unauthenticated',
342
455
  authStatus,
456
+ argv,
457
+ cliVersion,
343
458
  ...(authStatus === 'logged-in' && { authDiscrepancy: true }),
344
459
  };
345
460
  }
@@ -351,9 +466,11 @@ function runExtractMode(opts) {
351
466
  reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}`,
352
467
  reasonCode: 'model-error',
353
468
  authStatus,
469
+ argv,
470
+ cliVersion,
354
471
  };
355
472
  }
356
- return { ok: true, text: out, usage: null, reason: null, authStatus };
473
+ return { ok: true, text: out, usage: null, reason: null, authStatus, argv, cliVersion };
357
474
  }
358
475
 
359
476
  /**
@@ -376,14 +493,18 @@ function normalizeJudgeUsage(rawUsage) {
376
493
  return { input_tokens: inputSum, output_tokens: output };
377
494
  }
378
495
 
379
- /** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move. */
496
+ /** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move plus
497
+ * the same --setting-sources '' isolation the extraction spawn above gains (0.9.15). */
380
498
  function runJudgeMode(opts) {
381
499
  const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
382
500
  const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
501
+ if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
383
502
  const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
503
+ const argv = JUDGE_MODE_ARGV;
504
+ const cliVersion = getClaudeCliVersion(bin, opts);
384
505
  let res;
385
506
  try {
386
- res = spawnSyncImpl(bin, ['-p', '--output-format', 'json', '--no-session-persistence', '--tools', ''], {
507
+ res = spawnSyncImpl(bin, argv, {
387
508
  input: prompt,
388
509
  encoding: 'utf8',
389
510
  env: claudeChildEnv(),
@@ -391,17 +512,21 @@ function runJudgeMode(opts) {
391
512
  maxBuffer: 20 * 1024 * 1024,
392
513
  });
393
514
  } catch (error) {
394
- return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
515
+ return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
395
516
  }
396
517
  if (!res) {
397
- return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown' };
518
+ return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
519
+ }
520
+ if (looksLikeUnsupportedSettingSourcesFlag(res)) {
521
+ cachedSettingSourcesUnsupported = true;
522
+ return { ...settingSourcesIsolationUnsupportedResult('unknown'), argv, cliVersion };
398
523
  }
399
524
  const stdout = String(res.stdout || '');
400
525
  if (res.error) {
401
- return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
526
+ return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
402
527
  }
403
528
  if (/Please run \/login|authentication_error|401/i.test(stdout) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
404
- return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown' };
529
+ return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown', argv, cliVersion };
405
530
  }
406
531
  if (res.status !== 0) {
407
532
  return {
@@ -411,18 +536,20 @@ function runJudgeMode(opts) {
411
536
  reason: `local judge exited ${res.status}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
412
537
  reasonCode: 'model-error',
413
538
  authStatus: 'unknown',
539
+ argv,
540
+ cliVersion,
414
541
  };
415
542
  }
416
543
  let wrapper;
417
544
  try {
418
545
  wrapper = JSON.parse(stdout);
419
546
  } catch {
420
- return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown' };
547
+ return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
421
548
  }
422
549
  if (!wrapper || typeof wrapper.result !== 'string' || wrapper.is_error === true) {
423
- return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown' };
550
+ return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
424
551
  }
425
- return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown' };
552
+ return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown', argv, cliVersion };
426
553
  }
427
554
 
428
555
  /**
@@ -497,4 +624,18 @@ module.exports = {
497
624
  // Exported for direct unit coverage (test/extract-w1-fix2.test.js, GOV-3 item 6).
498
625
  managedSettingsPathForPlatform,
499
626
  MANAGED_SETTINGS_PATH_BY_PLATFORM,
627
+ // Exported for direct byte-pinned coverage (test/extraction-zero-tool-calls.test.js,
628
+ // test/claude-code-provider.test.js; TRUST-PAGE control — SITE-PM: put the
629
+ // zero-tool-call assertion in the test suite). Carries the 0.9.15 argv
630
+ // (SETTING_SOURCES_ARGS included).
631
+ EXTRACT_MODE_ARGV,
632
+ JUDGE_MODE_ARGV,
633
+ // EXTRACTION-CHILD-HOOKS (0.9.15) — exported for direct unit coverage
634
+ // (test/claude-code-provider.test.js) and for extract-local.js's provider-run
635
+ // log line (getClaudeCliVersion).
636
+ SETTING_SOURCES_VALUE,
637
+ SETTING_SOURCES_ARGS,
638
+ getClaudeCliVersion,
639
+ looksLikeUnsupportedSettingSourcesFlag,
640
+ _resetSettingSourcesCacheForTests,
500
641
  };
@@ -78,7 +78,13 @@ const PROVIDERS = {
78
78
  'byo-key': byoKey,
79
79
  };
80
80
 
81
- const PROVIDERS_STATE_PATH = path.join(os.homedir(), '.auxilo', 'providers.json');
81
+ // TEST-HOME-ISOLATION: same AUXILO_HOME-over-os.homedir() fallback as
82
+ // scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
83
+ // not imported — see that file's docblock; test/byo-key-provider.test.js
84
+ // pins the two byte-equal). opts.providersStatePath, threaded through every
85
+ // resolveProvider()/runModel()/persistSelected() call site in this repo's
86
+ // tests, still wins over both.
87
+ const PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
82
88
 
83
89
  function readProvidersState(statePath) {
84
90
  try {
@@ -232,6 +238,11 @@ const NON_RETRYABLE_FOR_THIS_PROVIDER = new Set([
232
238
  'cli-billing-helper-configured',
233
239
  'provider-not-configured',
234
240
  'providers-file-mode-unsafe',
241
+ // EXTRACTION-CHILD-HOOKS (0.9.15): the resolved claude-code CLI doesn't
242
+ // support --setting-sources, so it can never run isolated — same
243
+ // "cannot run at all right now" class as the codes above, safe to try the
244
+ // next provider in PROVIDER_ORDER rather than reporting a hard failure.
245
+ 'cli-settings-isolation-unsupported',
235
246
  ]);
236
247
 
237
248
  /**
package/scripts/runner.js CHANGED
@@ -645,6 +645,11 @@ async function postExtractDetailed(transcript, sessionId, sourceType, _scrubRepo
645
645
  captureVisibility: opts.captureVisibility || CAPTURE_VISIBILITY,
646
646
  log: runnerLog,
647
647
  auditLog: auditDropLog,
648
+ // EXTRACTION-RUN-LOG (0.9.15): the identifier the provider-run summary
649
+ // log line reports as `run=`. sessionId is already this call's natural
650
+ // run identity (it's what extraction_id: `client-${sessionId}` uses
651
+ // below); opts.runId (if a caller supplied one) wins over it.
652
+ runId: opts.runId || sessionId,
648
653
  }));
649
654
  } catch (err) {
650
655
  throw new Error(`Local extraction failed: ${err.message}`);