@davesheffer/hunch 0.19.1 → 0.20.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/dist/cli/index.js CHANGED
@@ -30,6 +30,7 @@ import { parseTestReport } from "../extractors/testreport.js";
30
30
  import { selectProvider } from "../synthesis/provider.js";
31
31
  import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists, commitAndPushHunch } from "../extractors/git.js";
32
32
  import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
33
+ import { partitionReview, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
33
34
  import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
34
35
  import { installMergeDriver } from "../integrations/mergeDriver.js";
35
36
  import { ensureGitignore } from "../integrations/gitignore.js";
@@ -950,63 +951,101 @@ program
950
951
  }
951
952
  });
952
953
  // ---- review (curate loop) -------------------------------------------------
954
+ /** Promote a draft to accepted/human-confirmed and CONFIRM its drafted tripwires —
955
+ * this is how the Veto Guard goes advisory → blocking ("confirm rides hunch review";
956
+ * dec_a466655539). Returns the new source tag and how many tripwires can now actually
957
+ * block (non-empty forbids) so bulk enforcement is never silent. */
958
+ function acceptDecision(store, d) {
959
+ const source = d.provenance.source.includes("llm_draft") ? "llm_draft+human_confirmed" : "human_confirmed";
960
+ const now = new Date().toISOString();
961
+ const confirmedTws = (d.rejected_tripwires ?? []).map((tw) => ({
962
+ ...tw,
963
+ provenance: {
964
+ ...tw.provenance,
965
+ source: tw.provenance.source.includes("human_confirmed")
966
+ ? tw.provenance.source
967
+ : tw.provenance.source.includes("llm_draft")
968
+ ? "llm_draft+human_confirmed"
969
+ : "human_confirmed",
970
+ last_verified: now,
971
+ },
972
+ }));
973
+ store.json.put("decisions", { ...d, status: "accepted", rejected_tripwires: confirmedTws, provenance: { ...d.provenance, source, confidence: 0.95, last_verified: now } });
974
+ const armed = confirmedTws.filter((tw) => tw.forbids.deps.length || tw.forbids.symbols.length || tw.forbids.patterns.length).length;
975
+ return { source, armed };
976
+ }
977
+ /** Print one draft for the review listing: id/status/source/confidence, the Critic's
978
+ * prune count (its visible value), the title, a decision snippet, and the raw synth line. */
979
+ function printReviewItem(it) {
980
+ const { d, synth } = it;
981
+ const pruneNote = synth.pruned ? ` · Critic pruned ${synth.pruned} unsupported` : "";
982
+ const synthLine = synth.raw ? `\n ↳ ${synth.raw}` : "";
983
+ console.log(` ${d.id} [${d.status}, ${d.provenance.source} ${d.provenance.confidence}]${pruneNote}\n ${d.title}\n ${d.decision.slice(0, 120)}${synthLine}`);
984
+ }
953
985
  program
954
986
  .command("review")
955
- .description("Triage low-confidence drafts: list, accept (promote), or reject.")
956
- .option("--accept <id>", "promote a decision to accepted/human-confirmed")
987
+ .description("Triage drafts: segmented list, accept/reject one, or batch-accept Critic-verified drafts.")
988
+ .option("--accept <id>", "promote a decision to accepted/human-confirmed (confirms its tripwires)")
957
989
  .option("--reject <id>", "delete a draft decision")
990
+ .option("--accept-verified", "batch-accept every Critic-verified, well-grounded draft (>= --min-grounded)")
991
+ .option("--min-grounded <n>", "grounded-ness threshold for the ready group / --accept-verified", String(READY_MIN_GROUNDED))
958
992
  .action((opts) => {
959
993
  const { store, root } = storeFor();
994
+ const minGrounded = Number.isFinite(Number(opts.minGrounded)) ? Number(opts.minGrounded) : READY_MIN_GROUNDED;
960
995
  if (opts.accept) {
961
996
  const d = store.json.get("decisions", opts.accept);
962
- if (!d)
997
+ if (!d) {
998
+ store.close();
963
999
  return fail(`decision ${opts.accept} not found`);
964
- const source = d.provenance.source.includes("llm_draft") ? "llm_draft+human_confirmed" : "human_confirmed";
965
- const now = new Date().toISOString();
966
- // Accepting a decision also CONFIRMS its drafted tripwires — this is how the
967
- // Veto Guard goes from advisory to blocking ("confirm rides hunch review").
968
- const tws = d.rejected_tripwires ?? [];
969
- const confirmedTws = tws.map((tw) => ({
970
- ...tw,
971
- provenance: {
972
- ...tw.provenance,
973
- source: tw.provenance.source.includes("human_confirmed")
974
- ? tw.provenance.source
975
- : tw.provenance.source.includes("llm_draft")
976
- ? "llm_draft+human_confirmed"
977
- : "human_confirmed",
978
- last_verified: now,
979
- },
980
- }));
981
- store.json.put("decisions", { ...d, status: "accepted", rejected_tripwires: confirmedTws, provenance: { ...d.provenance, source, confidence: 0.95, last_verified: now } });
1000
+ }
1001
+ const { source, armed } = acceptDecision(store, d);
982
1002
  store.reindex();
983
1003
  updateClaudeMd(root, store);
984
- const twNote = confirmedTws.length ? `, ${confirmedTws.length} tripwire(s) now blocking` : "";
985
- console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${twNote})`);
1004
+ console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
986
1005
  }
987
1006
  else if (opts.reject) {
988
1007
  const ok2 = store.json.delete("decisions", opts.reject);
989
1008
  store.reindex();
990
1009
  console.log(ok2 ? `✓ rejected and removed ${opts.reject}` : `decision ${opts.reject} not found`);
991
1010
  }
1011
+ else if (opts.acceptVerified) {
1012
+ // Batch path: only Critic-verified, well-grounded drafts qualify — still the
1013
+ // human-driven accept gate (the operator runs this), just over a safe subset.
1014
+ const proposed = store.json.loadAll("decisions").filter((d) => d.status === "proposed");
1015
+ const { ready } = partitionReview(proposed, minGrounded);
1016
+ if (!ready.length) {
1017
+ console.log(`✓ No Critic-verified drafts at grounded ≥ ${minGrounded} to batch-accept.`);
1018
+ }
1019
+ else {
1020
+ let armedTotal = 0;
1021
+ for (const it of ready)
1022
+ armedTotal += acceptDecision(store, it.d).armed;
1023
+ store.reindex();
1024
+ updateClaudeMd(root, store);
1025
+ console.log(`✓ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
1026
+ for (const it of ready)
1027
+ console.log(` ${it.d.id} grounded=${it.synth.grounded ?? "?"} ${it.d.title}`);
1028
+ }
1029
+ }
992
1030
  else {
993
- const drafts = store.json.loadAll("decisions")
994
- .filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6)
995
- .sort((a, b) => a.provenance.confidence - b.provenance.confidence);
996
- if (!drafts.length) {
1031
+ const drafts = store.json.loadAll("decisions").filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6);
1032
+ const { ready, scrutiny } = partitionReview(drafts, minGrounded);
1033
+ if (!ready.length && !scrutiny.length) {
997
1034
  console.log("✓ No low-confidence drafts to review.");
998
1035
  }
999
1036
  else {
1000
- console.log(`${drafts.length} draft(s) awaiting review (lowest confidence first):\n`);
1001
- for (const d of drafts) {
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}`);
1037
+ if (ready.length) {
1038
+ console.log(`✓ ${ready.length} ready to confirm — Critic-verified, grounded ≥ ${minGrounded} (best first):\n`);
1039
+ for (const it of ready)
1040
+ printReviewItem(it);
1041
+ console.log(`\n Batch-confirm all: hunch review --accept-verified\n`);
1042
+ }
1043
+ if (scrutiny.length) {
1044
+ console.log(`⚠ ${scrutiny.length} need scrutiny — unverified / low-grounded (lowest confidence first):\n`);
1045
+ for (const it of scrutiny)
1046
+ printReviewItem(it);
1008
1047
  }
1009
- console.log(`\nAccept: hunch review --accept <id>\nReject: hunch review --reject <id>`);
1048
+ console.log(`\nAccept: hunch review --accept <id> Reject: hunch review --reject <id>`);
1010
1049
  }
1011
1050
  }
1012
1051
  store.close();
@@ -0,0 +1,49 @@
1
+ /** Extract the `synth:` line out of a decision's evidence and parse its `k=v` fields. */
2
+ export function parseSynth(evidence) {
3
+ const line = (evidence ?? []).find((e) => e.startsWith("synth:"));
4
+ if (!line)
5
+ return {};
6
+ const body = line.slice("synth:".length).trim();
7
+ const num = (k) => {
8
+ const m = new RegExp(`\\b${k}=(-?[0-9]*\\.?[0-9]+)`).exec(body);
9
+ return m ? Number(m[1]) : undefined;
10
+ };
11
+ const str = (k) => {
12
+ const m = new RegExp(`\\b${k}=([A-Za-z0-9_.\\-]+)`).exec(body);
13
+ return m ? m[1] : undefined;
14
+ };
15
+ return {
16
+ raw: body,
17
+ provider: str("provider"),
18
+ grounded: num("grounded"),
19
+ samples: num("samples"),
20
+ agreement: num("agreement"),
21
+ pruned: num("pruned"),
22
+ verify: str("verify"),
23
+ };
24
+ }
25
+ /** Grounded-ness at/above which a Critic-verified draft is a "quick yes". */
26
+ export const READY_MIN_GROUNDED = 0.7;
27
+ /** A draft is "ready to confirm" only when the Critic actually audited it (source
28
+ * includes "verified") AND judged it well-grounded. A high confidence number alone
29
+ * is NOT enough — an un-audited draft always needs human eyes. */
30
+ export function isReady(d, synth, minGrounded = READY_MIN_GROUNDED) {
31
+ return d.provenance.source.includes("verified") && (synth.grounded ?? 0) >= minGrounded;
32
+ }
33
+ /** Split review drafts into ready-to-confirm (best-grounded first) and needs-scrutiny
34
+ * (lowest-confidence first). A draft is never in both groups. */
35
+ export function partitionReview(drafts, minGrounded = READY_MIN_GROUNDED) {
36
+ const items = drafts.map((d) => ({
37
+ d,
38
+ synth: parseSynth(d.provenance.evidence),
39
+ verified: d.provenance.source.includes("verified"),
40
+ }));
41
+ const ready = items
42
+ .filter((it) => isReady(it.d, it.synth, minGrounded))
43
+ .sort((a, b) => (b.synth.grounded ?? 0) - (a.synth.grounded ?? 0));
44
+ const scrutiny = items
45
+ .filter((it) => !isReady(it.d, it.synth, minGrounded))
46
+ .sort((a, b) => a.d.provenance.confidence - b.d.provenance.confidence);
47
+ return { ready, scrutiny };
48
+ }
49
+ //# sourceMappingURL=reviewqueue.js.map
@@ -636,11 +636,14 @@ function flaggedMatches(entry, flagged) {
636
636
  export function applyVerdict(draft, v) {
637
637
  const alternatives_rejected = draft.alternatives_rejected.filter((a) => !flaggedMatches(a, v.unsupported_alternatives));
638
638
  const consequences = draft.consequences.filter((c) => !flaggedMatches(c, v.unsupported_claims));
639
+ // The Critic's visible value: how many unsupported items it removed (alternatives
640
+ // never become tripwires; consequences never mislead). Surfaced in `hunch review`.
641
+ const pruned = (draft.alternatives_rejected.length - alternatives_rejected.length) + (draft.consequences.length - consequences.length);
639
642
  const grounded = clamp01(v.grounded);
640
643
  // Penalize weak grounding; (0.5 + 0.5*grounded) ∈ [0.5,1], so this only lowers.
641
644
  const confidence = Math.min(draft.confidence, Math.round(draft.confidence * (0.5 + 0.5 * grounded) * 100) / 100);
642
645
  const source = draft.source.includes("verified") ? draft.source : `${draft.source}+verified`;
643
- return { ...draft, alternatives_rejected, consequences, confidence, grounded, source, verifyOutcome: "applied" };
646
+ return { ...draft, alternatives_rejected, consequences, confidence, grounded, source, verifyOutcome: "applied", pruned };
644
647
  }
645
648
  /** Run the Critic pass and apply it, degrading to the un-audited draft when the
646
649
  * provider can't verify (deterministic / no CLI) or the call keeps failing. Never
@@ -98,6 +98,8 @@ export async function syncCommit(store, root, sha, opts = {}) {
98
98
  // (unavailable / failed) so a skipped audit is never mistaken for a clean one.
99
99
  else if (draft.verifyOutcome && draft.verifyOutcome !== "applied")
100
100
  synthBits.push(`verify=${draft.verifyOutcome}`);
101
+ if (draft.pruned)
102
+ synthBits.push(`pruned=${draft.pruned}`); // the Critic's visible value
101
103
  const synthEvidence = `synth:${synthBits.join(" ")}`;
102
104
  const components = store.json.loadAll("components");
103
105
  const relatedComponents = components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.19.1",
3
+ "version": "0.20.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.",