@davesheffer/hunch 0.18.1 → 0.19.1
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 +8 -5
- package/dist/cli/index.js +22 -4
- package/dist/synthesis/provider.js +161 -6
- package/dist/synthesis/synthesize.js +28 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,11 +87,14 @@ afterward to pick up the `hunch_*` tools. Each teammate runs `hunch init` once;
|
|
|
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
89
|
>
|
|
90
|
-
> **Deep Synthesis** (`backfill --deep` / `sync --deep`):
|
|
91
|
-
>
|
|
92
|
-
> **
|
|
93
|
-
>
|
|
94
|
-
>
|
|
90
|
+
> **Deep Synthesis** (`backfill --deep` / `sync --deep`): reconcile multiple independent drafts
|
|
91
|
+
> into one — fan out across every signed-in CLI, or, with a single CLI, sample it N times for
|
|
92
|
+
> **self-consistency** (`--samples`, default 2). Confidence is **agreement-weighted** (capped
|
|
93
|
+
> below the enforcement threshold, so it stays advisory). Add `--verify` (auto under `--deep`)
|
|
94
|
+
> for a **Critic pass** that audits each draft against its commit — pruning unsupported
|
|
95
|
+
> rejected-alternatives before they become tripwires and down-weighting weak grounding; it only
|
|
96
|
+
> ever *lowers* confidence, never arming enforcement. Subscription-only, never on the guard path;
|
|
97
|
+
> degrades to the single-provider draft when no CLI is available.
|
|
95
98
|
> On Windows, prefer `hunch init` over a global `claude mcp add`; if tools don't appear,
|
|
96
99
|
> `hunch doctor` heals it ([why](https://hunch-pi.vercel.app/docs#windows)).
|
|
97
100
|
|
package/dist/cli/index.js
CHANGED
|
@@ -180,6 +180,13 @@ async function mapPool(items, limit, fn) {
|
|
|
180
180
|
};
|
|
181
181
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
182
182
|
}
|
|
183
|
+
/** Parse a `--samples` flag into a finite positive count, or undefined so the ensemble
|
|
184
|
+
* uses its default depth. A typo'd value (`--samples abc`) must NOT become NaN — that
|
|
185
|
+
* would silently collapse --deep to the deterministic fallback. */
|
|
186
|
+
function parseSamples(v) {
|
|
187
|
+
const n = Number(v);
|
|
188
|
+
return v != null && Number.isFinite(n) && n > 0 ? Math.trunc(n) : undefined;
|
|
189
|
+
}
|
|
183
190
|
program
|
|
184
191
|
.command("backfill")
|
|
185
192
|
.description("Replay git history to seed decisions (cold-start fix).")
|
|
@@ -187,6 +194,8 @@ program
|
|
|
187
194
|
.option("--max <n>", "max commits to process", "40")
|
|
188
195
|
.option("--concurrency <n>", "commits to synthesize in parallel (the LLM call is the bottleneck)", "4")
|
|
189
196
|
.option("--deep", "Deep Synthesis: ensemble every available subscription CLI per commit and reconcile their drafts (slower, higher-quality; advisory)")
|
|
197
|
+
.option("--verify", "Critic pass: audit each draft against its commit, prune unsupported alternatives/consequences, down-weight weak grounding (extra subscription call; advisory)")
|
|
198
|
+
.option("--samples <n>", "self-consistency depth when only one CLI is installed: sample it n times per commit and reconcile (default 2 under --deep)")
|
|
190
199
|
.action(async (opts) => {
|
|
191
200
|
const { store, root } = storeFor();
|
|
192
201
|
if (!isGitRepo(root))
|
|
@@ -200,11 +209,13 @@ program
|
|
|
200
209
|
// each commit drafts independently and writes its OWN decision file atomically,
|
|
201
210
|
// and the store's JS-side reads/writes run synchronously between awaits (single
|
|
202
211
|
// thread) — only the LLM spawns overlap. reindex() runs once, after the pool.
|
|
212
|
+
const samples = parseSamples(opts.samples);
|
|
203
213
|
await mapPool(commits, conc, async (sha) => {
|
|
204
|
-
const r = await syncCommit(store, root, sha, { deep: opts.deep });
|
|
214
|
+
const r = await syncCommit(store, root, sha, { deep: opts.deep, verify: opts.verify, samples });
|
|
205
215
|
if (r.status === "written") {
|
|
206
216
|
written++;
|
|
207
|
-
|
|
217
|
+
// Any non-deterministic provider (claude/codex/cursor/ensemble) is an LLM draft.
|
|
218
|
+
if (r.provider && r.provider !== "deterministic")
|
|
208
219
|
llm++;
|
|
209
220
|
else
|
|
210
221
|
heuristic++;
|
|
@@ -231,6 +242,8 @@ program
|
|
|
231
242
|
.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")
|
|
232
243
|
.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
244
|
.option("--deep", "Deep Synthesis: ensemble every available subscription CLI and reconcile their drafts (agreement-weighted, advisory). Slower; subscription-only")
|
|
245
|
+
.option("--verify", "Critic pass: audit the draft against its commit, prune unsupported alternatives/consequences, down-weight weak grounding (extra subscription call; advisory)")
|
|
246
|
+
.option("--samples <n>", "self-consistency depth when only one CLI is installed: sample it n times and reconcile (default 2 under --deep)")
|
|
234
247
|
.action(async (sha, opts) => {
|
|
235
248
|
const { store, root } = storeFor();
|
|
236
249
|
if (!isGitRepo(root))
|
|
@@ -240,7 +253,7 @@ program
|
|
|
240
253
|
return opts.quiet ? undefined : fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
241
254
|
}
|
|
242
255
|
store.json.ensureDirs();
|
|
243
|
-
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: opts.private, deep: opts.deep });
|
|
256
|
+
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: opts.private, deep: opts.deep, verify: opts.verify, samples: parseSamples(opts.samples) });
|
|
244
257
|
if (r.status === "written") {
|
|
245
258
|
store.reindex();
|
|
246
259
|
// Don't rewrite CLAUDE.md from the hook — it would dirty the working tree
|
|
@@ -986,7 +999,12 @@ program
|
|
|
986
999
|
else {
|
|
987
1000
|
console.log(`${drafts.length} draft(s) awaiting review (lowest confidence first):\n`);
|
|
988
1001
|
for (const d of drafts) {
|
|
989
|
-
|
|
1002
|
+
// Surface synthesis telemetry (provider / reconciliation breadth / verifier
|
|
1003
|
+
// grounding) parked in evidence, so the reviewer sees WHY the confidence is
|
|
1004
|
+
// what it is and can confirm or reject at a glance.
|
|
1005
|
+
const synth = (d.provenance.evidence ?? []).find((e) => e.startsWith("synth:"));
|
|
1006
|
+
const synthLine = synth ? `\n ↳ ${synth.slice("synth:".length).trim()}` : "";
|
|
1007
|
+
console.log(` ${d.id} [${d.status}, ${d.provenance.source} ${d.provenance.confidence}]\n ${d.title}\n ${d.decision.slice(0, 120)}${synthLine}`);
|
|
990
1008
|
}
|
|
991
1009
|
console.log(`\nAccept: hunch review --accept <id>\nReject: hunch review --reject <id>`);
|
|
992
1010
|
}
|
|
@@ -131,6 +131,27 @@ const BUG_TOOL = {
|
|
|
131
131
|
required: ["title", "symptom", "root_cause", "severity"],
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
|
+
const VERIFY_TOOL = {
|
|
135
|
+
name: "emit_verdict",
|
|
136
|
+
description: "Emit a skeptical audit of a synthesized decision against its commit.",
|
|
137
|
+
input_schema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
grounded: { type: "number", description: "0..1: how well decision+consequences are supported by the ACTUAL diff. Be strict." },
|
|
141
|
+
unsupported_alternatives: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: { type: "string" },
|
|
144
|
+
description: "VERBATIM entries from alternatives_rejected that the diff/message does NOT evidence (likely hallucinated). Copy them exactly.",
|
|
145
|
+
},
|
|
146
|
+
unsupported_claims: {
|
|
147
|
+
type: "array",
|
|
148
|
+
items: { type: "string" },
|
|
149
|
+
description: "VERBATIM consequences not supported by the diff.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ["grounded", "unsupported_alternatives", "unsupported_claims"],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
134
155
|
// --------------------------------------------------------------------------
|
|
135
156
|
// Base for headless-CLI SUBSCRIPTION providers. Each one drives a coding-assistant
|
|
136
157
|
// CLI billed to the user's own subscription (never a pay-per-token API key — see
|
|
@@ -176,6 +197,17 @@ class CliSynthProvider {
|
|
|
176
197
|
throw new Error(`${this.name}: no usable bug JSON in output`);
|
|
177
198
|
return draft;
|
|
178
199
|
}
|
|
200
|
+
/** The Critic pass: audit a draft against its commit. Same subscription-only
|
|
201
|
+
* run() path (API keys stripped), so this never bills the pay-per-token API.
|
|
202
|
+
* Throws on unusable output so verifyDecisionSafe degrades to the un-audited
|
|
203
|
+
* draft (a verifier failure must never lose the draft — dec_18a81c8291). */
|
|
204
|
+
async verifyDecision(input, draft) {
|
|
205
|
+
const text = await this.run(`${VERIFY_SYSTEM}\n\n${verifyPrompt(input, draft)}\n\n${jsonInstruction(VERIFY_TOOL.input_schema)}`);
|
|
206
|
+
const verdict = verdictFromText(text);
|
|
207
|
+
if (!verdict)
|
|
208
|
+
throw new Error(`${this.name}: no usable verdict JSON in output`);
|
|
209
|
+
return verdict;
|
|
210
|
+
}
|
|
179
211
|
}
|
|
180
212
|
// A model id comes from a HUNCH_*_MODEL env var and ends up as an argv token that,
|
|
181
213
|
// on Windows, pexecIn joins into the cmd.exe line (shell:true, to resolve the npm
|
|
@@ -460,7 +492,8 @@ const dedupLines = (xs) => [...new Set(xs.map((s) => s.trim()).filter(Boolean))]
|
|
|
460
492
|
/** Reconcile N worker drafts into one. DETERMINISTIC (no second LLM call): the richest
|
|
461
493
|
* draft is the spine; alternatives/consequences are unioned; confidence is AGREEMENT-
|
|
462
494
|
* WEIGHTED and CAPPED at 0.78 — below STRICT_MIN_CONFIDENCE (0.8) — so an ensemble
|
|
463
|
-
* auto-draft can never arm enforcement.
|
|
495
|
+
* auto-draft can never arm enforcement. `samples`/`agreement` ride along as advisory
|
|
496
|
+
* telemetry for `hunch review` (not schema-bound). */
|
|
464
497
|
export function mergeDecisionDrafts(drafts) {
|
|
465
498
|
const primary = [...drafts].sort((a, b) => b.confidence - a.confidence || b.decision.length - a.decision.length)[0];
|
|
466
499
|
const agreement = meanAgreement(drafts);
|
|
@@ -472,19 +505,40 @@ export function mergeDecisionDrafts(drafts) {
|
|
|
472
505
|
alternatives_rejected: dedupLines(drafts.flatMap((d) => d.alternatives_rejected)),
|
|
473
506
|
confidence: Math.min(0.78, 0.55 + 0.23 * agreement),
|
|
474
507
|
source: "llm_draft+ensemble",
|
|
508
|
+
samples: drafts.length,
|
|
509
|
+
agreement: Math.round(agreement * 100) / 100,
|
|
475
510
|
};
|
|
476
511
|
}
|
|
512
|
+
// Default self-consistency depth when only ONE subscription CLI is installed (the
|
|
513
|
+
// common case): sample it this many times and reconcile, so single-CLI users get
|
|
514
|
+
// ensemble-like robustness. Tunable per-call via `--samples`.
|
|
515
|
+
const DEFAULT_SAMPLES = 2;
|
|
477
516
|
export class EnsembleProvider {
|
|
478
517
|
workers;
|
|
479
518
|
name = "ensemble";
|
|
480
|
-
|
|
519
|
+
samples;
|
|
520
|
+
constructor(workers, opts = {}) {
|
|
481
521
|
this.workers = workers;
|
|
522
|
+
// Default 1 (single worker → passthrough); the self-consistency policy default
|
|
523
|
+
// lives at the selection layer (selectEnsemble). Coerce to a finite integer in a
|
|
524
|
+
// sane 1..5 band — a NaN here would make decisionTasks build ZERO tasks and throw,
|
|
525
|
+
// silently collapsing --deep to the deterministic fallback (callers also sanitize).
|
|
526
|
+
const n = Math.trunc(Number(opts.samples));
|
|
527
|
+
this.samples = Number.isFinite(n) ? Math.max(1, Math.min(5, n)) : 1;
|
|
482
528
|
}
|
|
483
529
|
async available() { return this.workers.length > 0; }
|
|
530
|
+
/** The draft tasks to fan out: one per distinct CLI when several are installed
|
|
531
|
+
* (cross-model ensemble), else N self-consistency samples of the single CLI. */
|
|
532
|
+
decisionTasks(input) {
|
|
533
|
+
if (this.workers.length >= 2)
|
|
534
|
+
return this.workers.map((w) => () => w.draftDecision(input));
|
|
535
|
+
const w = this.workers[0];
|
|
536
|
+
return Array.from({ length: this.samples }, () => () => w.draftDecision(input));
|
|
537
|
+
}
|
|
484
538
|
async draftDecision(input) {
|
|
485
539
|
if (!this.workers.length)
|
|
486
540
|
throw new Error("ensemble: no subscription CLI workers available");
|
|
487
|
-
const settled = await Promise.allSettled(this.
|
|
541
|
+
const settled = await Promise.allSettled(this.decisionTasks(input).map((t) => t()));
|
|
488
542
|
const drafts = settled.flatMap((s) => (s.status === "fulfilled" ? [s.value] : []));
|
|
489
543
|
if (!drafts.length)
|
|
490
544
|
throw new Error("ensemble: all workers failed");
|
|
@@ -502,11 +556,112 @@ export class EnsembleProvider {
|
|
|
502
556
|
}
|
|
503
557
|
}
|
|
504
558
|
/** 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
|
-
|
|
559
|
+
* (the caller then falls back to the normal single-provider path). `samples` sets
|
|
560
|
+
* the self-consistency depth for the single-CLI case. */
|
|
561
|
+
export async function selectEnsemble(opts = {}) {
|
|
562
|
+
const workers = await selectWorkers();
|
|
563
|
+
// The self-consistency policy default (DEFAULT_SAMPLES) is applied HERE, not in the
|
|
564
|
+
// provider — so a single CLI under --deep is sampled N times, while direct
|
|
565
|
+
// construction stays passthrough. `--samples 1` opts back out.
|
|
566
|
+
return workers.length ? new EnsembleProvider(workers, { samples: opts.samples ?? DEFAULT_SAMPLES }) : null;
|
|
567
|
+
}
|
|
568
|
+
/** Pick a CLI provider to run the Critic pass (subscription-only, like the workers).
|
|
569
|
+
* Returns null when no assistant CLI is installed — verification then no-ops and the
|
|
570
|
+
* un-audited draft stands (graceful degradation; dec_18a81c8291). */
|
|
571
|
+
export async function selectVerifier() {
|
|
507
572
|
const workers = await selectWorkers();
|
|
508
|
-
return workers
|
|
573
|
+
return workers[0] ?? null;
|
|
574
|
+
}
|
|
575
|
+
// ---- Verification (the Critic pass) ---------------------------------------
|
|
576
|
+
// Audit a draft against the commit it came from, then PRUNE unsupported
|
|
577
|
+
// alternatives/consequences and LOWER confidence on weak grounding. It may only
|
|
578
|
+
// reduce trust, never raise it past the cap — auto-drafts stay advisory and a human
|
|
579
|
+
// `hunch review --accept` remains the ONLY path to enforcement (dec_9a2f2fe72a).
|
|
580
|
+
const VERIFY_SYSTEM = `You are a skeptical auditor for an Engineering Memory OS. You are given a
|
|
581
|
+
synthesized decision record and the ACTUAL commit it was derived from. Your job is to
|
|
582
|
+
flag everything the record asserts that the evidence does NOT support — be strict; when
|
|
583
|
+
in doubt, flag it. Do not invent new content; only judge what is present.`;
|
|
584
|
+
function verifyPrompt(input, draft) {
|
|
585
|
+
const alts = draft.alternatives_rejected.length
|
|
586
|
+
? draft.alternatives_rejected.map((a, i) => ` ${i + 1}. ${a}`).join("\n")
|
|
587
|
+
: " (none)";
|
|
588
|
+
const cons = draft.consequences.length ? draft.consequences.map((c) => ` - ${c}`).join("\n") : " (none)";
|
|
589
|
+
return [
|
|
590
|
+
`COMMIT SUBJECT: ${input.subject}`,
|
|
591
|
+
input.body ? `COMMIT BODY:\n${input.body}` : "",
|
|
592
|
+
input.analysis ? `STRUCTURED CHANGES: ${summarizeDiff(input.analysis)}` : "",
|
|
593
|
+
renderDiff(input),
|
|
594
|
+
`CANDIDATE DECISION UNDER AUDIT:`,
|
|
595
|
+
` decision: ${draft.decision}`,
|
|
596
|
+
` consequences:\n${cons}`,
|
|
597
|
+
` alternatives_rejected:\n${alts}`,
|
|
598
|
+
`\nReturn grounded (0..1) and the VERBATIM alternatives_rejected / consequences the evidence does NOT support.`,
|
|
599
|
+
].filter(Boolean).join("\n\n");
|
|
600
|
+
}
|
|
601
|
+
/** Map model text → VerifyVerdict, or null when nothing usable parses (→ the caller
|
|
602
|
+
* keeps the un-audited draft). Tolerant of arrays-as-strings and missing fields. */
|
|
603
|
+
export function verdictFromText(text) {
|
|
604
|
+
for (const obj of extractJsonObjects(text)) {
|
|
605
|
+
const hasGrounded = typeof obj.grounded === "number";
|
|
606
|
+
const ua = asStrArr(obj.unsupported_alternatives);
|
|
607
|
+
const uc = asStrArr(obj.unsupported_claims);
|
|
608
|
+
if (!hasGrounded && !ua.length && !uc.length)
|
|
609
|
+
continue; // unrelated object
|
|
610
|
+
const grounded = typeof obj.grounded === "number" ? clamp01(obj.grounded) : 1;
|
|
611
|
+
return { grounded, unsupported_alternatives: ua, unsupported_claims: uc };
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
616
|
+
/** True if `flagged` names `entry` — exact normalized match, or a substantial
|
|
617
|
+
* (≥8 char) containment either way, to absorb minor rewording by the auditor
|
|
618
|
+
* without nuking unrelated entries. */
|
|
619
|
+
function flaggedMatches(entry, flagged) {
|
|
620
|
+
const e = norm(entry);
|
|
621
|
+
if (!e)
|
|
622
|
+
return false;
|
|
623
|
+
return flagged.some((f) => {
|
|
624
|
+
const n = norm(f);
|
|
625
|
+
if (!n)
|
|
626
|
+
return false;
|
|
627
|
+
if (n === e)
|
|
628
|
+
return true;
|
|
629
|
+
return n.length >= 8 && e.length >= 8 && (e.includes(n) || n.includes(e));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
/** Apply a verdict to a draft: drop unsupported alternatives (so they never scaffold
|
|
633
|
+
* tripwires) and consequences, and scale confidence DOWN by grounding. Confidence is
|
|
634
|
+
* clamped so it can only fall — verification never arms a stronger claim than the
|
|
635
|
+
* draft already made (R2). Records `grounded` as advisory telemetry. */
|
|
636
|
+
export function applyVerdict(draft, v) {
|
|
637
|
+
const alternatives_rejected = draft.alternatives_rejected.filter((a) => !flaggedMatches(a, v.unsupported_alternatives));
|
|
638
|
+
const consequences = draft.consequences.filter((c) => !flaggedMatches(c, v.unsupported_claims));
|
|
639
|
+
const grounded = clamp01(v.grounded);
|
|
640
|
+
// Penalize weak grounding; (0.5 + 0.5*grounded) ∈ [0.5,1], so this only lowers.
|
|
641
|
+
const confidence = Math.min(draft.confidence, Math.round(draft.confidence * (0.5 + 0.5 * grounded) * 100) / 100);
|
|
642
|
+
const source = draft.source.includes("verified") ? draft.source : `${draft.source}+verified`;
|
|
643
|
+
return { ...draft, alternatives_rejected, consequences, confidence, grounded, source, verifyOutcome: "applied" };
|
|
644
|
+
}
|
|
645
|
+
/** Run the Critic pass and apply it, degrading to the un-audited draft when the
|
|
646
|
+
* provider can't verify (deterministic / no CLI) or the call keeps failing. Never
|
|
647
|
+
* throws. Marks the OUTCOME on the draft (applied / unavailable / failed) so the
|
|
648
|
+
* degradation is visible in telemetry instead of silent. Retries once: under --deep
|
|
649
|
+
* the Critic call stacks after sampling, and a single transient failure on that
|
|
650
|
+
* extra call shouldn't drop the audit. */
|
|
651
|
+
export async function verifyDecisionSafe(verifier, input, draft) {
|
|
652
|
+
if (!verifier?.verifyDecision)
|
|
653
|
+
return { ...draft, verifyOutcome: "unavailable" };
|
|
654
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
655
|
+
try {
|
|
656
|
+
return applyVerdict(draft, await verifier.verifyDecision(input, draft));
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
/* transient (network / unparseable verdict) — retry once, then give up */
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { ...draft, verifyOutcome: "failed" };
|
|
509
663
|
}
|
|
664
|
+
const clamp01 = (n) => (Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1);
|
|
510
665
|
// ---- prompt + parsing helpers --------------------------------------------
|
|
511
666
|
// Above this size we stop shipping the raw patch and lean on the deterministic
|
|
512
667
|
// 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, selectEnsemble, DeterministicProvider } from "./provider.js";
|
|
3
|
+
import { selectProvider, selectEnsemble, selectVerifier, verifyDecisionSafe, 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";
|
|
@@ -69,13 +69,36 @@ export async function syncCommit(store, root, sha, opts = {}) {
|
|
|
69
69
|
// Deep Synthesis (--deep): ensemble every available subscription CLI and reconcile
|
|
70
70
|
// their drafts (agreement-weighted, confidence capped below the strict gate). Falls
|
|
71
71
|
// back to the normal single-provider path when no CLI is available. Opt-in only.
|
|
72
|
+
// --verify forces the LLM provider (auditing a deterministic draft is pointless) and,
|
|
73
|
+
// like --deep, runs the Critic pass below. Subscription-only throughout (con_2ce3f2a547).
|
|
74
|
+
const wantVerify = !!(opts.verify || opts.deep);
|
|
72
75
|
const provider = opts.deep
|
|
73
|
-
? (await selectEnsemble()) ?? await selectProvider()
|
|
74
|
-
: opts.force || isSignificant(meta, analysis, codeFiles)
|
|
76
|
+
? (await selectEnsemble({ samples: opts.samples })) ?? await selectProvider()
|
|
77
|
+
: opts.force || opts.verify || isSignificant(meta, analysis, codeFiles)
|
|
75
78
|
? await selectProvider()
|
|
76
79
|
: new DeterministicProvider();
|
|
77
80
|
const input = { subject: meta.subject, body: meta.body, files: codeFiles, diff, analysis };
|
|
78
|
-
|
|
81
|
+
let draft = await draftDecisionSafe(provider, input);
|
|
82
|
+
// The Critic pass: audit the draft against the commit, PRUNE unsupported alternatives
|
|
83
|
+
// (BEFORE they scaffold tripwires below) and consequences, and lower confidence on weak
|
|
84
|
+
// grounding. No-ops when no assistant CLI is available; never raises trust (dec_9a2f2fe72a).
|
|
85
|
+
if (wantVerify)
|
|
86
|
+
draft = await verifyDecisionSafe(await selectVerifier(), input, draft);
|
|
87
|
+
// Advisory synthesis telemetry for `hunch review` — which provider ran, how many drafts
|
|
88
|
+
// were reconciled, their agreement, and the verifier's grounding. Rides in `evidence`
|
|
89
|
+
// (no schema change → respects forward-migration invariant con_947c578b2c).
|
|
90
|
+
const synthBits = [`provider=${provider.name}`];
|
|
91
|
+
if (draft.samples)
|
|
92
|
+
synthBits.push(`samples=${draft.samples}`);
|
|
93
|
+
if (draft.agreement != null)
|
|
94
|
+
synthBits.push(`agreement=${draft.agreement}`);
|
|
95
|
+
if (draft.grounded != null)
|
|
96
|
+
synthBits.push(`grounded=${draft.grounded}`);
|
|
97
|
+
// The Critic was requested (--verify/--deep) but didn't apply — surface WHY
|
|
98
|
+
// (unavailable / failed) so a skipped audit is never mistaken for a clean one.
|
|
99
|
+
else if (draft.verifyOutcome && draft.verifyOutcome !== "applied")
|
|
100
|
+
synthBits.push(`verify=${draft.verifyOutcome}`);
|
|
101
|
+
const synthEvidence = `synth:${synthBits.join(" ")}`;
|
|
79
102
|
const components = store.json.loadAll("components");
|
|
80
103
|
const relatedComponents = components
|
|
81
104
|
.filter((c) => codeFiles.some((f) => c.paths.some((g) => pathMatchesGlob(f, g))))
|
|
@@ -119,7 +142,7 @@ export async function syncCommit(store, root, sha, opts = {}) {
|
|
|
119
142
|
provenance: {
|
|
120
143
|
source: draft.source,
|
|
121
144
|
confidence: draft.confidence,
|
|
122
|
-
evidence: [`commit:${meta.shortSha}`, ...codeFiles.slice(0, 8)],
|
|
145
|
+
evidence: [`commit:${meta.shortSha}`, synthEvidence, ...codeFiles.slice(0, 8)],
|
|
123
146
|
last_verified: new Date().toISOString(), // when the Hunch last re-derived this
|
|
124
147
|
},
|
|
125
148
|
date: meta.date, // the commit date
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
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.",
|