@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 +75 -36
- package/dist/core/reviewqueue.js +49 -0
- package/dist/synthesis/provider.js +4 -1
- package/dist/synthesis/synthesize.js +2 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
965
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
console.log(
|
|
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:
|
|
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.
|
|
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.",
|