@davesheffer/hunch 1.6.0 → 1.7.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.
package/README.md CHANGED
@@ -62,6 +62,12 @@ it adds a native lifecycle adapter too.
62
62
  Your memory is plain JSON that you own. Hunch adds a SQLite index only as a rebuildable derived
63
63
  layer—your decisions never disappear into a proprietary hosted memory system.
64
64
 
65
+ Synthesis is just as portable: Hunch can use Claude Code, Codex, or Cursor through the subscription
66
+ CLI you choose. It never guesses which of several installed subscriptions to bill—set your local,
67
+ gitignored preference with `hunch provider codex-cli` (or `claude-cli` / `cursor-agent`); otherwise
68
+ Hunch uses a subscription only when exactly one is available, and falls back to deterministic local
69
+ drafting when the choice is ambiguous.
70
+
65
71
  ```text
66
72
  Claude Code ─┐
67
73
  Cursor ├── MCP ──> .hunch/ reasoning graph ──> deterministic checks
package/dist/cli/index.js CHANGED
@@ -28,7 +28,7 @@ import { selectEmbedder } from "../store/embedder.js";
28
28
  import { indexRepo } from "../extractors/indexer.js";
29
29
  import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
30
30
  import { parseTestReport } from "../extractors/testreport.js";
31
- import { selectProvider } from "../synthesis/provider.js";
31
+ import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, } from "../synthesis/provider.js";
32
32
  import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, pullHunch, gitUntrackCached, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
33
33
  import { writeTeamConfig, ensureTeamOverlay, readTeamConfig } from "../integrations/team.js";
34
34
  import { runbookId, decisionId } from "../core/ids.js";
@@ -445,7 +445,20 @@ function configureOverlay(dir, opts, mode) {
445
445
  // a store elsewhere on disk. Resolution (env || local.json) re-resolves against root.
446
446
  const rel = relative(root, hunchDir);
447
447
  const stored = rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosixTarget(rel) : hunchDir;
448
- writeFileAtomic(join(paths.hunch, "local.json"), JSON.stringify({ privateDir: stored, autoCommit: !!opts.autoCommit, mode }, null, 2) + "\n");
448
+ const localFile = join(paths.hunch, "local.json");
449
+ let existingLocal = {};
450
+ if (existsSync(localFile)) {
451
+ try {
452
+ const parsed = JSON.parse(readFileSync(localFile, "utf8"));
453
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
454
+ throw new Error("not an object");
455
+ existingLocal = parsed;
456
+ }
457
+ catch {
458
+ return fail(`refusing to overwrite malformed local configuration: ${localFile}`);
459
+ }
460
+ }
461
+ writeFileAtomic(localFile, JSON.stringify({ ...existingLocal, privateDir: stored, autoCommit: !!opts.autoCommit, mode }, null, 2) + "\n");
449
462
  ensureGitignore(root); // keeps .hunch/local.json + .hunch-private/ out of git
450
463
  // SHARED mode with a remote: publish the store's URL in a COMMITTED team.json, so a
451
464
  // fresh clone / new teammate / headless agent auto-connects on `hunch init` (or MCP
@@ -1543,6 +1556,48 @@ program
1543
1556
  const next = writeConfig(paths, { firmness: level }).firmness;
1544
1557
  console.log(`✓ firmness set to ${next} (takes effect on the next agent edit — no restart needed).`);
1545
1558
  });
1559
+ // ---- provider (per-user synthesis subscription choice) -------------------
1560
+ program
1561
+ .command("provider")
1562
+ .description("Show or set the local coding-assistant subscription Hunch may use for synthesis. Never changes team config.")
1563
+ .argument("[name]", `auto | ${SYNTH_PREFERENCES.filter((p) => p !== "auto").join(" | ")} (omit to inspect)`)
1564
+ .action(async (value) => {
1565
+ const root = findRoot();
1566
+ if (value != null) {
1567
+ const preference = value.trim();
1568
+ if (!SYNTH_PREFERENCES.includes(preference)) {
1569
+ return fail(`provider must be one of: ${SYNTH_PREFERENCES.join(", ")}`);
1570
+ }
1571
+ try {
1572
+ writeSynthesisPreference(root, preference);
1573
+ }
1574
+ catch (error) {
1575
+ return fail(error instanceof Error ? error.message : String(error));
1576
+ }
1577
+ console.log(`✓ local synthesis preference set to ${preference} (gitignored; it never changes a teammate's billing choice).`);
1578
+ }
1579
+ const resolution = await resolveSynthesisProvider({ root });
1580
+ const envValue = process.env.HUNCH_SYNTH_PROVIDER?.trim();
1581
+ const local = readSynthesisPreference(root);
1582
+ const hasValidEnv = !!envValue && SYNTH_PREFERENCES.includes(envValue);
1583
+ console.log(`selected: ${resolution.provider.name} (${resolution.source})`);
1584
+ console.log(`preference: ${hasValidEnv ? `environment: ${envValue}` : `local: ${local}`}`);
1585
+ if (envValue && !hasValidEnv)
1586
+ console.log(dim(`HUNCH_SYNTH_PROVIDER=${envValue} is unknown and is being ignored.`));
1587
+ console.log("available:");
1588
+ for (const status of resolution.statuses) {
1589
+ const billing = status.subscription ? ` — ${status.subscription}` : "";
1590
+ console.log(` ${status.available ? "✓" : "·"} ${status.name}: ${status.label}${billing}`);
1591
+ }
1592
+ if (resolution.source === "ambiguous") {
1593
+ const choices = resolution.statuses.filter((s) => s.name !== "deterministic" && s.available).map((s) => `hunch provider ${s.name}`);
1594
+ console.log(dim("Multiple subscription CLIs are available, so Hunch uses the free deterministic fallback rather than guessing which plan to spend."));
1595
+ console.log(`choose one: ${choices.join(" or ")}`);
1596
+ }
1597
+ else if (resolution.source === "unavailable-preference") {
1598
+ console.log(dim(`Your ${resolution.preference} preference is not available; Hunch is using the local deterministic fallback.`));
1599
+ }
1600
+ });
1546
1601
  // ---- status (enforcement readiness at a glance) ---------------------------
1547
1602
  program
1548
1603
  .command("status")
@@ -1965,7 +2020,7 @@ program
1965
2020
  // and any per-draft failure degrades to "not judged" (kept for a human).
1966
2021
  const verdicts = new Map();
1967
2022
  if (opts.llm !== false && !opts.private) {
1968
- const provider = await selectProvider();
2023
+ const provider = await selectProvider({ root });
1969
2024
  if (provider.judgeDraft) {
1970
2025
  // The candidate pool for duplicate_of / restatement: the LIVE, vouched records.
1971
2026
  const existing = all
@@ -2284,7 +2339,7 @@ program
2284
2339
  let prose;
2285
2340
  let adoptionProse;
2286
2341
  if (opts.llm !== false) {
2287
- const provider = await selectProvider();
2342
+ const provider = await selectProvider({ root });
2288
2343
  if (provider.draftProse) {
2289
2344
  console.log(`Prose via ${provider.name} (subscription); the drift-bearing skeleton stays deterministic.`);
2290
2345
  prose = (pack, excerpts) => provider.draftProse(wikiPrompt(pack, excerpts));
@@ -2516,24 +2571,24 @@ program
2516
2571
  const onDisk = readManifest(hunchPaths(root)).schema_version;
2517
2572
  const schemaNote = onDisk === SCHEMA_VERSION ? "" : onDisk > SCHEMA_VERSION ? ` ⚠ newer than this Hunch (v${SCHEMA_VERSION}) — upgrade hunch` : ` ⚠ run \`hunch migrate\``;
2518
2573
  console.log(`schema: v${onDisk} (hunch v${SCHEMA_VERSION})${schemaNote}`);
2519
- const provider = await selectProvider();
2520
- console.log(`synthesis: ${provider.name}`);
2521
- // Synthesis is billed to the user's SUBSCRIPTION via a coding-assistant CLI,
2522
- // never a pay-per-token API key. Surface which one — or what's missing.
2523
- const SUB = {
2524
- "claude-cli": { label: "Claude subscription (claude CLI)", strip: "ANTHROPIC_API_KEY" },
2525
- "codex-cli": { label: "ChatGPT subscription (codex CLI)", strip: "OPENAI_API_KEY" },
2526
- "cursor-agent": { label: "Cursor subscription (cursor-agent CLI)" },
2527
- };
2528
- const sub = SUB[provider.name];
2529
- if (sub) {
2530
- const hadKey = sub.strip && !!process.env[sub.strip];
2531
- console.log(` ↳ LLM synthesis billed to your ${sub.label}` +
2532
- (hadKey ? ` (${sub.strip} in env is stripped never billed to the API)` : ``));
2574
+ const resolution = await resolveSynthesisProvider({ root });
2575
+ const provider = resolution.provider;
2576
+ console.log(`synthesis: ${provider.name} (${resolution.source})`);
2577
+ const selected = resolution.statuses.find((s) => s.name === provider.name);
2578
+ if (selected?.subscription) {
2579
+ console.log(` ↳ LLM synthesis uses your ${selected.subscription}; provider API credentials are not used.`);
2580
+ }
2581
+ else if (resolution.source === "ambiguous") {
2582
+ const names = resolution.statuses.filter((s) => s.name !== "deterministic" && s.available).map((s) => s.name);
2583
+ console.log(dim(` ↳ ${names.join(", ")} are available; Hunch will not guess which subscription to spend.`));
2584
+ console.log(dim(` choose one locally: ${names.map((name) => `hunch provider ${name}`).join(" or ")}`));
2585
+ }
2586
+ else if (resolution.source === "unavailable-preference") {
2587
+ console.log(dim(` ${resolution.preference} was selected but is unavailable; using the offline heuristic.`));
2533
2588
  }
2534
2589
  else {
2535
- console.log(dim(` ↳ no assistant CLI found — synthesis uses the offline heuristic (advisory, low-confidence)`));
2536
- console.log(dim(` for full synthesis install one: Claude Code (\`claude /login\`), Codex (\`codex login\`), or Cursor (\`cursor-agent login\`)`));
2590
+ console.log(dim(` ↳ no assistant CLI found — synthesis uses the offline heuristic (advisory, low-confidence).`));
2591
+ console.log(dim(` install or log into Claude Code, Codex, or Cursor; then select one with \`hunch provider <name>\`.`));
2537
2592
  }
2538
2593
  const c = store.reindex().counts;
2539
2594
  console.log(`hunch: ${c.symbols} symbols, ${c.edges} edges, ${c.components} components, ${c.decisions} decisions, ${c.bugs} bugs, ${c.constraints} constraints`);
@@ -1,23 +1,26 @@
1
1
  /**
2
2
  * Pluggable synthesis provider for the WRITE path (DESIGN.md §4 / §7).
3
3
  *
4
- * LLM synthesis is driven by the user's Claude **subscription** via the `claude`
5
- * CLI — never the pay-per-token Anthropic API. We try, in order:
6
- * claude-cli deterministic-fallback. The fallback always works (no creds, no
7
- * network) and emits a LOW-confidence draft, honoring the design rule that
8
- * auto-captured memory is advisory and cheap to discard.
4
+ * LLM synthesis is driven by the user's chosen coding-assistant subscription
5
+ * CLI — never a pay-per-token API. Claude Code, Codex, and Cursor use different
6
+ * auth surfaces, but every provider returns the same shape. When more than one
7
+ * subscription CLI is available, Hunch deliberately does NOT guess whose plan
8
+ * to spend: the user chooses once with `hunch provider <name>` (stored locally)
9
+ * or overrides per shell with HUNCH_SYNTH_PROVIDER. Ambiguous auto mode stays
10
+ * deterministic and free.
9
11
  *
10
- * Subscription, not API: Claude Code's auth precedence puts `ANTHROPIC_API_KEY`
11
- * (and `ANTHROPIC_AUTH_TOKEN`) ABOVE subscription OAuth, and in headless `-p`
12
- * mode the API key is *always* used when present. So we strip those vars from
13
- * the child env (see ClaudeCliProvider.run) to force the CLI down to subscription
14
- * OAuth / CLAUDE_CODE_OAUTH_TOKEN. There is intentionally NO API-key provider.
12
+ * Subscription, not API: provider-specific API credentials are removed from the
13
+ * child env wherever the CLI would otherwise prefer them. There is intentionally
14
+ * NO direct API-key provider.
15
15
  *
16
16
  * Every provider returns the same shape so the rest of the system never knows
17
17
  * (or cares) which one ran.
18
18
  */
19
19
  import { spawn } from "node:child_process";
20
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
20
21
  import { tmpdir } from "node:os";
22
+ import { dirname, join } from "node:path";
23
+ import { writeFileAtomic } from "../core/io.js";
21
24
  import { summarizeDiff } from "../extractors/diff.js";
22
25
  const IS_WIN = process.platform === "win32";
23
26
  /**
@@ -96,6 +99,16 @@ export function pexecIn(cmd, args, opts = {}) {
96
99
  child.stdin.end();
97
100
  });
98
101
  }
102
+ /** Every selectable synthesis mode. `auto` is a preference value rather than a
103
+ * provider: it uses a subscription only when exactly one usable CLI is found. */
104
+ export const SYNTH_PROVIDER_NAMES = ["claude-cli", "codex-cli", "cursor-agent", "deterministic"];
105
+ export const SYNTH_PREFERENCES = ["auto", ...SYNTH_PROVIDER_NAMES];
106
+ const PROVIDER_INFO = {
107
+ "claude-cli": { label: "Claude Code", subscription: "Claude subscription" },
108
+ "codex-cli": { label: "Codex", subscription: "ChatGPT subscription" },
109
+ "cursor-agent": { label: "Cursor Agent", subscription: "Cursor subscription" },
110
+ deterministic: { label: "Deterministic local fallback", subscription: null },
111
+ };
99
112
  const SYSTEM = `You are the synthesis engine of an Engineering Memory OS. You turn raw
100
113
  developer activity (a git commit diff, or a test failure) into a single structured
101
114
  "why" record. Be precise and evidence-grounded; never invent facts not supported by
@@ -169,7 +182,7 @@ const VERIFY_TOOL = {
169
182
  // --------------------------------------------------------------------------
170
183
  // Base for headless-CLI SUBSCRIPTION providers. Each one drives a coding-assistant
171
184
  // CLI billed to the user's own subscription (never a pay-per-token API key — see
172
- // dec_5a7c0733f7). The prompt always goes over STDIN (never argv — keeps untrusted
185
+ // dec_65b058de66). The prompt always goes over STDIN (never argv — keeps untrusted
173
186
  // diff content out of any shell pexecIn uses on Windows), and the CLI's text output
174
187
  // is handed to the SAME mappers, so the rest of the system is provider-agnostic.
175
188
  // --------------------------------------------------------------------------
@@ -447,9 +460,9 @@ export function extractCodexText(out) {
447
460
  return agentTexts[agentTexts.length - 1];
448
461
  return texts.length ? texts[texts.length - 1] : out;
449
462
  }
450
- // Priority order: try each subscription CLI, then the always-available heuristic.
451
- // HUNCH_SYNTH_PROVIDER forces one by name (claude-cli / codex-cli / cursor-agent /
452
- // deterministic).
463
+ // This registry is deliberately NOT a priority order. Auto mode only spends a
464
+ // subscription when it can identify exactly one usable CLI; see
465
+ // resolveSynthesisProvider below.
453
466
  const PROVIDERS = [
454
467
  new ClaudeCliProvider(),
455
468
  new CodexCliProvider(),
@@ -457,31 +470,126 @@ const PROVIDERS = [
457
470
  new DeterministicProvider(),
458
471
  ];
459
472
  // Availability rarely changes within a process (a CLI doesn't get installed mid-run),
460
- // and selectProvider() runs on every sync/recordFailure so memoize each probe.
461
- // Especially matters in the long-lived MCP server and on machines with NO assistant
462
- // CLI, where an uncached pass spawns one failing `--version` per provider every time.
463
- const availCache = new Map();
473
+ // and selection runs on every sync/recordFailure. Cache by object identity rather than
474
+ // name so injected test registries never inherit a stale result from another provider.
475
+ const availCache = new WeakMap();
464
476
  function isAvailable(p) {
465
- let v = availCache.get(p.name);
477
+ let v = availCache.get(p);
466
478
  if (!v) {
467
479
  v = p.available().catch(() => false);
468
- availCache.set(p.name, v);
480
+ availCache.set(p, v);
469
481
  }
470
482
  return v;
471
483
  }
472
- /** Choose the first available provider, honoring HUNCH_SYNTH_PROVIDER override. */
473
- export async function selectProvider() {
474
- const forced = process.env.HUNCH_SYNTH_PROVIDER;
475
- if (forced) {
476
- const p = PROVIDERS.find((x) => x.name === forced);
477
- if (p && (await isAvailable(p)))
478
- return p;
484
+ function isSynthPreference(value) {
485
+ return !!value && SYNTH_PREFERENCES.includes(value);
486
+ }
487
+ function fallbackProvider(providers) {
488
+ return providers.find((p) => p.name === "deterministic") ?? new DeterministicProvider();
489
+ }
490
+ function localPreferencePath(root) {
491
+ return join(root, ".hunch", "local.json");
492
+ }
493
+ /** Read a per-user, gitignored choice. Invalid/missing local state is treated as auto;
494
+ * `writeSynthesisPreference` refuses to overwrite malformed data so this forgiveness
495
+ * never destroys someone else's local settings. */
496
+ export function readSynthesisPreference(root) {
497
+ try {
498
+ const file = localPreferencePath(root);
499
+ if (!existsSync(file))
500
+ return "auto";
501
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
502
+ return typeof parsed.synthProvider === "string" && isSynthPreference(parsed.synthProvider)
503
+ ? parsed.synthProvider
504
+ : "auto";
479
505
  }
480
- for (const p of PROVIDERS) {
481
- if (await isAvailable(p))
482
- return p;
506
+ catch {
507
+ return "auto";
508
+ }
509
+ }
510
+ /** Persist the user's provider choice only in `.hunch/local.json`, which is never a
511
+ * repository policy. That means each developer controls their own subscription spend. */
512
+ export function writeSynthesisPreference(root, preference) {
513
+ if (!isSynthPreference(preference))
514
+ throw new Error(`unknown synthesis provider preference: ${preference}`);
515
+ const file = localPreferencePath(root);
516
+ let local = {};
517
+ if (existsSync(file)) {
518
+ const raw = readFileSync(file, "utf8");
519
+ if (raw.trim()) {
520
+ try {
521
+ const parsed = JSON.parse(raw);
522
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
523
+ throw new Error("not an object");
524
+ local = parsed;
525
+ }
526
+ catch {
527
+ throw new Error(`refusing to overwrite malformed local configuration: ${file}`);
528
+ }
529
+ }
530
+ }
531
+ mkdirSync(dirname(file), { recursive: true });
532
+ writeFileAtomic(file, `${JSON.stringify({ ...local, synthProvider: preference }, null, 2)}\n`);
533
+ }
534
+ async function statusesFor(providers) {
535
+ const statuses = [];
536
+ for (const provider of providers) {
537
+ if (!SYNTH_PROVIDER_NAMES.includes(provider.name))
538
+ continue;
539
+ const name = provider.name;
540
+ const info = PROVIDER_INFO[name];
541
+ statuses.push({ name, ...info, available: await isAvailable(provider) });
542
+ }
543
+ return statuses;
544
+ }
545
+ /** Resolve the provider without ever inferring which of several installed products is
546
+ * the one the user intends to spend. Precedence is deliberate: a one-shell override,
547
+ * then a per-user local preference, then safe auto-detection. */
548
+ export async function resolveSynthesisProvider(opts = {}) {
549
+ const providers = opts.providers ?? PROVIDERS;
550
+ const env = opts.env ?? process.env;
551
+ const statuses = await statusesFor(providers);
552
+ const fallback = fallbackProvider(providers);
553
+ const find = (name) => providers.find((p) => p.name === name);
554
+ const usable = async (name) => {
555
+ const provider = find(name);
556
+ return provider && await isAvailable(provider) ? provider : undefined;
557
+ };
558
+ const environment = env.HUNCH_SYNTH_PROVIDER?.trim();
559
+ if (environment && isSynthPreference(environment) && environment !== "auto") {
560
+ const selected = await usable(environment);
561
+ if (selected)
562
+ return { provider: selected, source: "environment", preference: environment, statuses };
563
+ return { provider: fallback, source: "unavailable-preference", preference: environment, statuses };
564
+ }
565
+ // `HUNCH_SYNTH_PROVIDER=auto` is useful in CI or a shell profile: it explicitly
566
+ // suppresses the local preference and re-enters the safe auto policy.
567
+ const preference = environment === "auto"
568
+ ? "auto"
569
+ : opts.root ? readSynthesisPreference(opts.root) : "auto";
570
+ if (preference !== "auto") {
571
+ const selected = await usable(preference);
572
+ if (selected)
573
+ return { provider: selected, source: "local", preference, statuses };
574
+ return { provider: fallback, source: "unavailable-preference", preference, statuses };
575
+ }
576
+ const available = statuses.filter((status) => status.name !== "deterministic" && status.available);
577
+ if (available.length === 1) {
578
+ const selected = await usable(available[0].name);
579
+ if (selected)
580
+ return { provider: selected, source: "single-available", preference, statuses };
483
581
  }
484
- return new DeterministicProvider();
582
+ return {
583
+ provider: fallback,
584
+ source: available.length > 1 ? "ambiguous" : "none",
585
+ preference,
586
+ statuses,
587
+ };
588
+ }
589
+ /** The provider used by normal synthesis. See `resolveSynthesisProvider` for a
590
+ * diagnosable result with the selection source and every candidate's availability. */
591
+ export async function selectProvider(opts = {}) {
592
+ return (await resolveSynthesisProvider(opts)).provider;
485
593
  }
486
594
  // ---- Deep Synthesis: ensemble of subscription CLIs ------------------------
487
595
  // Opt-in (backfill/sync --deep): fan a commit out to EVERY available subscription
@@ -490,9 +598,9 @@ export async function selectProvider() {
490
598
  // the guard path; confidence is capped below the strict gate so output stays advisory.
491
599
  /** All available subscription-CLI workers (claude/codex/cursor), excluding the
492
600
  * deterministic fallback — the pool Deep Synthesis fans a commit out to. */
493
- export async function selectWorkers() {
601
+ export async function selectWorkers(opts = {}) {
494
602
  const out = [];
495
- for (const p of PROVIDERS) {
603
+ for (const p of opts.providers ?? PROVIDERS) {
496
604
  if (p.name === "deterministic")
497
605
  continue; // workers are real subscription CLIs only
498
606
  if (await isAvailable(p))
@@ -592,7 +700,7 @@ export class EnsembleProvider {
592
700
  * (the caller then falls back to the normal single-provider path). `samples` sets
593
701
  * the self-consistency depth for the single-CLI case. */
594
702
  export async function selectEnsemble(opts = {}) {
595
- const workers = await selectWorkers();
703
+ const workers = await selectWorkers(opts);
596
704
  // The self-consistency policy default (DEFAULT_SAMPLES) is applied HERE, not in the
597
705
  // provider — so a single CLI under --deep is sampled N times, while direct
598
706
  // construction stays passthrough. `--samples 1` opts back out.
@@ -601,9 +709,9 @@ export async function selectEnsemble(opts = {}) {
601
709
  /** Pick a CLI provider to run the Critic pass (subscription-only, like the workers).
602
710
  * Returns null when no assistant CLI is installed — verification then no-ops and the
603
711
  * un-audited draft stands (graceful degradation; dec_18a81c8291). */
604
- export async function selectVerifier() {
605
- const workers = await selectWorkers();
606
- return workers[0] ?? null;
712
+ export async function selectVerifier(opts = {}) {
713
+ const { provider } = await resolveSynthesisProvider(opts);
714
+ return provider.name === "deterministic" ? null : provider;
607
715
  }
608
716
  // ---- Verification (the Critic pass) ---------------------------------------
609
717
  // Audit a draft against the commit it came from, then PRUNE unsupported
@@ -98,9 +98,9 @@ export async function syncCommit(store, root, sha, opts = {}) {
98
98
  const provider = localOnly
99
99
  ? new DeterministicProvider()
100
100
  : opts.deep
101
- ? (await selectEnsemble({ samples: opts.samples })) ?? await selectProvider()
101
+ ? (await selectEnsemble({ samples: opts.samples })) ?? await selectProvider({ root })
102
102
  : opts.force || opts.verify || isSignificant(meta, analysis, codeFiles)
103
- ? await selectProvider()
103
+ ? await selectProvider({ root })
104
104
  : new DeterministicProvider();
105
105
  const input = { subject: meta.subject, body: meta.body, files: codeFiles, diff, analysis };
106
106
  let draft = await draftDecisionSafe(provider, input);
@@ -108,7 +108,7 @@ export async function syncCommit(store, root, sha, opts = {}) {
108
108
  // (BEFORE they scaffold tripwires below) and consequences, and lower confidence on weak
109
109
  // grounding. No-ops when no assistant CLI is available; never raises trust (dec_9a2f2fe72a).
110
110
  if (wantVerify)
111
- draft = await verifyDecisionSafe(await selectVerifier(), input, draft);
111
+ draft = await verifyDecisionSafe(await selectVerifier({ root }), input, draft);
112
112
  // Advisory synthesis telemetry for `hunch review` — which provider ran, how many drafts
113
113
  // were reconciled, their agreement, and the verifier's grounding. Rides in `evidence`
114
114
  // (no schema change → respects forward-migration invariant con_947c578b2c).
@@ -199,7 +199,7 @@ export async function recordFailure(store, root, failure, opts = {}) {
199
199
  // A private bug may contain a stack trace, customer data, or secrets. Keep the
200
200
  // whole capture local unless the caller deliberately routes it through a shared
201
201
  // (non-private) workflow.
202
- const provider = opts.private ? new DeterministicProvider() : await selectProvider();
202
+ const provider = opts.private ? new DeterministicProvider() : await selectProvider({ root });
203
203
  const input = {
204
204
  test: failure.test,
205
205
  message: failure.message,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
6
  "description": "Architectural Conformance for AI-generated code: a git-native graph that deterministically blocks AI changes which break your architecture — the semantic invariants (layering, must-reach, dependency direction) pattern-SAST can't express — grounded in the decisions and bugs behind each rule, across any MCP assistant (Claude Code, Cursor, Copilot, Windsurf, Antigravity, Codex).",