@davesheffer/hunch 0.17.3 → 0.18.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
@@ -86,6 +86,12 @@ afterward to pick up the `hunch_*` tools. Each teammate runs `hunch init` once;
86
86
  > Synthesis is billed to **your coding-assistant subscription** (Claude/Codex/Cursor CLI),
87
87
  > **never** a pay-per-token API key — and falls back to a deterministic heuristic if no CLI
88
88
  > is present. Details: [Synthesis & billing](https://hunch-pi.vercel.app/docs#synthesis).
89
+ >
90
+ > **Deep Synthesis** (`backfill --deep` / `sync --deep`): if you're signed into more than one
91
+ > CLI, fan the commit out to *all* of them and reconcile the drafts into one — confidence is
92
+ > **agreement-weighted** (capped below the enforcement threshold, so it stays advisory).
93
+ > Subscription-only, never on the guard path, and it degrades to the single-provider path with
94
+ > one CLI.
89
95
  > On Windows, prefer `hunch init` over a global `claude mcp add`; if tools don't appear,
90
96
  > `hunch doctor` heals it ([why](https://hunch-pi.vercel.app/docs#windows)).
91
97
 
@@ -168,7 +174,9 @@ env var, no shell-profile edit** (and `HUNCH_PRIVATE_DIR` still overrides per-sh
168
174
  default-off** (no config → fully inert), and **leak-safe by construction**: committed files and
169
175
  the CI PR comment render *public-only*, so a private record can't reach a public surface. Record
170
176
  sensitive items with `private: true` (`hunch_record_decision` / `hunch_record_correction`);
171
- post-commit synthesis can route there too. [docs](https://hunch-pi.vercel.app/docs#private)
177
+ post-commit synthesis can route there too, and `hunch private --auto-commit` (opt-in)
178
+ auto-commits + pushes each capture to the private repo — recursion-safe, staging only `.hunch/`.
179
+ → [docs](https://hunch-pi.vercel.app/docs#private)
172
180
 
173
181
  ## Continuous learning (CI)
174
182
 
package/dist/cli/index.js CHANGED
@@ -186,6 +186,7 @@ program
186
186
  .option("--since <spec>", "how far back, e.g. 90d", "90d")
187
187
  .option("--max <n>", "max commits to process", "40")
188
188
  .option("--concurrency <n>", "commits to synthesize in parallel (the LLM call is the bottleneck)", "4")
189
+ .option("--deep", "Deep Synthesis: ensemble every available subscription CLI per commit and reconcile their drafts (slower, higher-quality; advisory)")
189
190
  .action(async (opts) => {
190
191
  const { store, root } = storeFor();
191
192
  if (!isGitRepo(root))
@@ -200,7 +201,7 @@ program
200
201
  // and the store's JS-side reads/writes run synchronously between awaits (single
201
202
  // thread) — only the LLM spawns overlap. reindex() runs once, after the pool.
202
203
  await mapPool(commits, conc, async (sha) => {
203
- const r = await syncCommit(store, root, sha);
204
+ const r = await syncCommit(store, root, sha, { deep: opts.deep });
204
205
  if (r.status === "written") {
205
206
  written++;
206
207
  if (r.provider === "claude-cli")
@@ -229,6 +230,7 @@ program
229
230
  .option("--force", "re-synthesize even if a decision already exists for the commit")
230
231
  .option("--private", "write the synthesized decision into the private overlay (HUNCH_PRIVATE_DIR), not the public repo — for a repo whose memory is kept private")
231
232
  .option("--commit", "after a capture, also git add+commit+push the repo the decision landed in (opt-in; best-effort) — the private store under --private, else this repo")
233
+ .option("--deep", "Deep Synthesis: ensemble every available subscription CLI and reconcile their drafts (agreement-weighted, advisory). Slower; subscription-only")
232
234
  .action(async (sha, opts) => {
233
235
  const { store, root } = storeFor();
234
236
  if (!isGitRepo(root))
@@ -238,7 +240,7 @@ program
238
240
  return opts.quiet ? undefined : fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
239
241
  }
240
242
  store.json.ensureDirs();
241
- const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: opts.private });
243
+ const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: opts.private, deep: opts.deep });
242
244
  if (r.status === "written") {
243
245
  store.reindex();
244
246
  // Don't rewrite CLAUDE.md from the hook — it would dirty the working tree
@@ -418,6 +418,95 @@ export async function selectProvider() {
418
418
  }
419
419
  return new DeterministicProvider();
420
420
  }
421
+ // ---- Deep Synthesis: ensemble of subscription CLIs ------------------------
422
+ // Opt-in (backfill/sync --deep): fan a commit out to EVERY available subscription
423
+ // CLI, drop failures, and reconcile the drafts. Subscription-only (the workers are
424
+ // the same CLI providers, so ANTHROPIC_API_KEY stripping is inherited). NEVER used on
425
+ // the guard path; confidence is capped below the strict gate so output stays advisory.
426
+ /** All available subscription-CLI workers (claude/codex/cursor), excluding the
427
+ * deterministic fallback — the pool Deep Synthesis fans a commit out to. */
428
+ export async function selectWorkers() {
429
+ const out = [];
430
+ for (const p of PROVIDERS) {
431
+ if (p.name === "deterministic")
432
+ continue; // workers are real subscription CLIs only
433
+ if (await isAvailable(p))
434
+ out.push(p);
435
+ }
436
+ return out;
437
+ }
438
+ const tokens = (d) => new Set(`${d.title} ${d.decision}`.toLowerCase().match(/[a-z0-9_]{3,}/g) ?? []);
439
+ /** Mean pairwise Jaccard overlap of the drafts' identifying text (0..1) — how much the
440
+ * independent workers AGREE. Drives the merged confidence. */
441
+ function meanAgreement(drafts) {
442
+ if (drafts.length < 2)
443
+ return 1;
444
+ const sets = drafts.map(tokens);
445
+ let sum = 0, pairs = 0;
446
+ for (let i = 0; i < sets.length; i++)
447
+ for (let j = i + 1; j < sets.length; j++) {
448
+ const a = sets[i], b = sets[j];
449
+ let inter = 0;
450
+ for (const t of a)
451
+ if (b.has(t))
452
+ inter++;
453
+ const union = a.size + b.size - inter;
454
+ sum += union ? inter / union : 0;
455
+ pairs++; // two empty drafts don't meaningfully "agree"
456
+ }
457
+ return pairs ? sum / pairs : 1;
458
+ }
459
+ const dedupLines = (xs) => [...new Set(xs.map((s) => s.trim()).filter(Boolean))];
460
+ /** Reconcile N worker drafts into one. DETERMINISTIC (no second LLM call): the richest
461
+ * draft is the spine; alternatives/consequences are unioned; confidence is AGREEMENT-
462
+ * WEIGHTED and CAPPED at 0.78 — below STRICT_MIN_CONFIDENCE (0.8) — so an ensemble
463
+ * auto-draft can never arm enforcement. */
464
+ export function mergeDecisionDrafts(drafts) {
465
+ const primary = [...drafts].sort((a, b) => b.confidence - a.confidence || b.decision.length - a.decision.length)[0];
466
+ const agreement = meanAgreement(drafts);
467
+ return {
468
+ title: primary.title,
469
+ context: primary.context,
470
+ decision: primary.decision,
471
+ consequences: dedupLines(drafts.flatMap((d) => d.consequences)),
472
+ alternatives_rejected: dedupLines(drafts.flatMap((d) => d.alternatives_rejected)),
473
+ confidence: Math.min(0.78, 0.55 + 0.23 * agreement),
474
+ source: "llm_draft+ensemble",
475
+ };
476
+ }
477
+ export class EnsembleProvider {
478
+ workers;
479
+ name = "ensemble";
480
+ constructor(workers) {
481
+ this.workers = workers;
482
+ }
483
+ async available() { return this.workers.length > 0; }
484
+ async draftDecision(input) {
485
+ if (!this.workers.length)
486
+ throw new Error("ensemble: no subscription CLI workers available");
487
+ const settled = await Promise.allSettled(this.workers.map((w) => w.draftDecision(input)));
488
+ const drafts = settled.flatMap((s) => (s.status === "fulfilled" ? [s.value] : []));
489
+ if (!drafts.length)
490
+ throw new Error("ensemble: all workers failed");
491
+ return drafts.length === 1 ? drafts[0] : mergeDecisionDrafts(drafts);
492
+ }
493
+ async draftBug(input) {
494
+ // Bug ensembling is deferred — use the first worker that succeeds.
495
+ for (const w of this.workers) {
496
+ try {
497
+ return await w.draftBug(input);
498
+ }
499
+ catch { /* try next */ }
500
+ }
501
+ throw new Error("ensemble: all workers failed for bug");
502
+ }
503
+ }
504
+ /** Build the Deep-Synthesis provider, or null if no subscription CLI is available
505
+ * (the caller then falls back to the normal single-provider path). */
506
+ export async function selectEnsemble() {
507
+ const workers = await selectWorkers();
508
+ return workers.length ? new EnsembleProvider(workers) : null;
509
+ }
421
510
  // ---- prompt + parsing helpers --------------------------------------------
422
511
  // Above this size we stop shipping the raw patch and lean on the deterministic
423
512
  // STRUCTURED CHANGES summary + a small sample. A truncated head-slice of a giant
@@ -1,6 +1,6 @@
1
1
  import { commitMeta, commitDiff, headSha } from "../extractors/git.js";
2
2
  import { analyzeDiff } from "../extractors/diff.js";
3
- import { selectProvider, DeterministicProvider } from "./provider.js";
3
+ import { selectProvider, selectEnsemble, DeterministicProvider } from "./provider.js";
4
4
  import { decisionId, bugId, constraintId } from "../core/ids.js";
5
5
  import { pathMatchesGlob } from "../core/glob.js";
6
6
  import { draftTripwires, knownRepoDeps } from "./tripwires.js";
@@ -66,9 +66,14 @@ export async function syncCommit(store, root, sha, opts = {}) {
66
66
  // Significance gate: reserve the paid LLM for substantive commits; trivial ones
67
67
  // get the FREE deterministic draft (honestly labeled "inferred"/low-confidence,
68
68
  // so the Hunch stays accurate-by-provenance). --force always uses the provider.
69
- const provider = opts.force || isSignificant(meta, analysis, codeFiles)
70
- ? await selectProvider()
71
- : new DeterministicProvider();
69
+ // Deep Synthesis (--deep): ensemble every available subscription CLI and reconcile
70
+ // their drafts (agreement-weighted, confidence capped below the strict gate). Falls
71
+ // back to the normal single-provider path when no CLI is available. Opt-in only.
72
+ const provider = opts.deep
73
+ ? (await selectEnsemble()) ?? await selectProvider()
74
+ : opts.force || isSignificant(meta, analysis, codeFiles)
75
+ ? await selectProvider()
76
+ : new DeterministicProvider();
72
77
  const input = { subject: meta.subject, body: meta.body, files: codeFiles, diff, analysis };
73
78
  const draft = await draftDecisionSafe(provider, input);
74
79
  const components = store.json.loadAll("components");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
6
  "description": "Hunch — an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",