@davesheffer/hunch 1.12.0 → 1.12.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/dist/cli/index.js +108 -16
- package/dist/core/docanchors.js +27 -3
- package/dist/core/docscan.js +5 -1
- package/dist/core/served.js +105 -0
- package/dist/mcp/server.js +10 -3
- package/package.json +3 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -58,6 +58,7 @@ import { isHumanConfirmed } from "../core/strictgate.js";
|
|
|
58
58
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
59
59
|
import { computeStats, formatStats } from "../core/stats.js";
|
|
60
60
|
import { injectionMode, resetSessionInjections } from "../core/hookcache.js";
|
|
61
|
+
import { recordServed, servedSummary } from "../core/served.js";
|
|
61
62
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
62
63
|
import { PIPELINE_LOOP, UNVERIFIED_NAG, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, savePipelineState, stopVerdict, } from "../core/pipeline.js";
|
|
63
64
|
import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
|
|
@@ -3718,6 +3719,47 @@ program
|
|
|
3718
3719
|
store.close();
|
|
3719
3720
|
}
|
|
3720
3721
|
});
|
|
3722
|
+
program
|
|
3723
|
+
.command("served")
|
|
3724
|
+
.description("Delivery receipts: which memory records actually reached an agent, how often, and which never have")
|
|
3725
|
+
.option("--json", "emit the raw ledger summary")
|
|
3726
|
+
.action((opts) => {
|
|
3727
|
+
const { store, root } = storeFor();
|
|
3728
|
+
try {
|
|
3729
|
+
const summary = servedSummary(root);
|
|
3730
|
+
if (opts.json) {
|
|
3731
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (!summary.total) {
|
|
3735
|
+
console.log("No delivery receipts yet — they accrue as the pre-edit hook and subagent grounding fire on this machine.");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
console.log(`\nHunch — delivery receipts (this machine)\n`);
|
|
3739
|
+
console.log(` ${summary.total} deliveries · ${summary.distinct_records} distinct record(s) · ${summary.distinct_sessions} session(s) · ${summary.first_at?.slice(0, 10)} → ${summary.last_at?.slice(0, 10)}\n`);
|
|
3740
|
+
const titleOf = (row) => {
|
|
3741
|
+
if (row.kind === "decisions")
|
|
3742
|
+
return store.recs("decisions").find((r) => r.id === row.record_id)?.title ?? row.record_id;
|
|
3743
|
+
if (row.kind === "constraints")
|
|
3744
|
+
return store.recs("constraints").find((r) => r.id === row.record_id)?.statement.slice(0, 70) ?? row.record_id;
|
|
3745
|
+
return row.record_id;
|
|
3746
|
+
};
|
|
3747
|
+
console.log(" Most delivered:");
|
|
3748
|
+
for (const row of summary.rows.slice(0, 10)) {
|
|
3749
|
+
console.log(` ${String(row.serves).padStart(4)}× (+${row.refreshes} still-current) ${row.record_id} — ${titleOf(row)}`);
|
|
3750
|
+
}
|
|
3751
|
+
const servedIds = new Set(summary.rows.map((r) => r.record_id));
|
|
3752
|
+
const neverServed = [
|
|
3753
|
+
...store.recs("constraints").filter((c) => c.status === "active" && !servedIds.has(c.id)).map((c) => c.id),
|
|
3754
|
+
...store.recs("decisions").filter((d) => d.status === "accepted" && !d.superseded_by && !servedIds.has(d.id)).map((d) => d.id),
|
|
3755
|
+
];
|
|
3756
|
+
console.log(`\n Never delivered on this machine: ${neverServed.length} in-force record(s)${neverServed.length ? ` — compact candidates start here (${neverServed.slice(0, 5).join(", ")}${neverServed.length > 5 ? ", …" : ""})` : ""}`);
|
|
3757
|
+
console.log(` Prevented violations live in \`hunch stats\` (events ledger); receipts here are the delivery half.\n`);
|
|
3758
|
+
}
|
|
3759
|
+
finally {
|
|
3760
|
+
store.close();
|
|
3761
|
+
}
|
|
3762
|
+
});
|
|
3721
3763
|
program
|
|
3722
3764
|
.command("hook")
|
|
3723
3765
|
.description("Agent-agnostic hook handler: normalizes Claude, VS Code, Cursor, Windsurf, and Antigravity events into Hunch context and strict policy checks. Reads hook JSON on stdin.")
|
|
@@ -3809,28 +3851,67 @@ program
|
|
|
3809
3851
|
// A delegated agent starts with NONE of the parent session's grounding:
|
|
3810
3852
|
// session orientation never fired inside it and only per-edit PreToolUse
|
|
3811
3853
|
// follows it in — so read-only agents (Explore/Plan) could work fully
|
|
3812
|
-
// blind.
|
|
3813
|
-
//
|
|
3854
|
+
// blind. Slice by what the agent TYPE is about to do (dec_a788cc039b):
|
|
3855
|
+
// explorers get the indexed shape, planners get live decisions + what
|
|
3856
|
+
// was already rejected, everyone else gets the invariant digest. Public
|
|
3857
|
+
// store only; cheap reads.
|
|
3814
3858
|
const s = new HunchStore(paths);
|
|
3815
3859
|
try {
|
|
3816
|
-
const
|
|
3817
|
-
|
|
3818
|
-
.
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3860
|
+
const clip1 = (text, max) => {
|
|
3861
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
3862
|
+
return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat;
|
|
3863
|
+
};
|
|
3864
|
+
const type = (evt.agent_type ?? "").toLowerCase();
|
|
3865
|
+
const L = [];
|
|
3866
|
+
const served = [];
|
|
3867
|
+
if (/explore|search|investigat/.test(type)) {
|
|
3868
|
+
// Orient from the graph, not grep rounds: the component map IS the shape.
|
|
3869
|
+
const components = s.advisoryRecs("components").filter((c) => c.status === "active");
|
|
3870
|
+
if (!components.length)
|
|
3871
|
+
return;
|
|
3872
|
+
L.push(`🧠 Hunch — repo shape for a delegated explorer: ${components.length} component(s).`);
|
|
3873
|
+
for (const c of components.slice(0, 12)) {
|
|
3874
|
+
L.push(`- ${c.name}${c.paths.length ? ` (${c.paths.slice(0, 2).join(", ")})` : ""}${c.responsibility ? ` — ${clip1(c.responsibility, 90)}` : ""}`);
|
|
3875
|
+
served.push({ kind: "components", record_id: c.id });
|
|
3876
|
+
}
|
|
3877
|
+
if (components.length > 12)
|
|
3878
|
+
L.push(`…and ${components.length - 12} more — hunch_structure() for the full map.`);
|
|
3879
|
+
L.push("Orient: hunch_structure(target) · hunch_why(target) · hunch_context(task).");
|
|
3880
|
+
}
|
|
3881
|
+
else if (/plan|architect|design/.test(type)) {
|
|
3882
|
+
// A plan drafted blind re-proposes what the graph already rejected.
|
|
3883
|
+
const decisions = s.advisoryRecs("decisions")
|
|
3884
|
+
.filter((d) => d.status === "accepted")
|
|
3885
|
+
.sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
3886
|
+
if (!decisions.length)
|
|
3887
|
+
return;
|
|
3888
|
+
L.push(`🧠 Hunch — live decisions for a delegated planner (${decisions.length} in force; plans must not re-propose the rejected).`);
|
|
3889
|
+
for (const d of decisions.slice(0, 6)) {
|
|
3890
|
+
L.push(`- ${d.title} (${d.id})${d.alternatives_rejected.length ? ` — rejected: ${clip1(d.alternatives_rejected[0], 80)}` : ""}`);
|
|
3891
|
+
served.push({ kind: "decisions", record_id: d.id });
|
|
3892
|
+
}
|
|
3893
|
+
L.push("Before finalizing a plan: hunch_why(target) · hunch_current_decision(topic) · hunch_check_constraints(scope).");
|
|
3894
|
+
}
|
|
3895
|
+
else {
|
|
3896
|
+
const sevRank = { blocking: 0, warning: 1, advisory: 2 };
|
|
3897
|
+
const constraints = s.advisoryRecs("constraints")
|
|
3898
|
+
.filter((c) => c.status === "active")
|
|
3899
|
+
.sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
|
|
3900
|
+
if (!constraints.length)
|
|
3901
|
+
return;
|
|
3902
|
+
L.push(`🧠 Hunch — delegated agent grounding: ${constraints.length} invariant(s) in force in this repo.`);
|
|
3903
|
+
for (const c of constraints.slice(0, 8)) {
|
|
3904
|
+
L.push(`- [${c.severity}] ${clip1(c.statement, 140)}${c.scope.length ? ` (scope: ${c.scope.slice(0, 3).join(", ")})` : ""}`);
|
|
3905
|
+
served.push({ kind: "constraints", record_id: c.id });
|
|
3906
|
+
}
|
|
3907
|
+
if (constraints.length > 8)
|
|
3908
|
+
L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
|
|
3909
|
+
L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
|
|
3827
3910
|
}
|
|
3828
|
-
if (constraints.length > 8)
|
|
3829
|
-
L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
|
|
3830
|
-
L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
|
|
3831
3911
|
// No dedup here: the hook event carries the PARENT session id, but each
|
|
3832
3912
|
// spawned agent is a fresh empty context — deduping would ground the
|
|
3833
3913
|
// first Explore and silently starve every later one.
|
|
3914
|
+
recordServed(root, served.map((r) => ({ ...r, event: "served", target: `(subagent:${evt.agent_type ?? "any"})`, session_id: evt.session_id })));
|
|
3834
3915
|
emitContext(provider, "SubagentStart", L.join("\n"));
|
|
3835
3916
|
}
|
|
3836
3917
|
finally {
|
|
@@ -3999,10 +4080,21 @@ program
|
|
|
3999
4080
|
// Identical grounding already shown this session → one-line delta instead of
|
|
4000
4081
|
// the full 10-16KB block. Any record change re-sends the full text; the
|
|
4001
4082
|
// strict-gate deny path above never routes through this (dec_244397d920).
|
|
4083
|
+
// Delivery receipts (dec_925f4bcaad): the ledger of what actually reached
|
|
4084
|
+
// an agent. A full injection is a serve; a delta one-liner attests the
|
|
4085
|
+
// earlier serve is still standing. Never throws, never blocks.
|
|
4086
|
+
const receipts = (event) => recordServed(root, [
|
|
4087
|
+
...ctx.constraints.map((c) => ({ event, kind: "constraints", record_id: c.id, target, session_id: evt.session_id })),
|
|
4088
|
+
...ctx.decisions.map((d) => ({ event, kind: "decisions", record_id: d.id, target, session_id: evt.session_id })),
|
|
4089
|
+
...ctx.bugs.map((b) => ({ event, kind: "bugs", record_id: b.id, target, session_id: evt.session_id })),
|
|
4090
|
+
...ctx.findings.map((f) => ({ event, kind: "findings", record_id: f.id, target, session_id: evt.session_id })),
|
|
4091
|
+
]);
|
|
4002
4092
|
if (injectionMode(evt.session_id, `pre:${target}`, text) === "delta") {
|
|
4093
|
+
receipts("refreshed");
|
|
4003
4094
|
emitContext(provider, "PreToolUse", `Hunch grounding for ${target}: unchanged this session (${ctx.decisions.length} decision(s), ${ctx.constraints.length} invariant(s) shown earlier — still current; hunch_why("${target}") to re-expand).`);
|
|
4004
4095
|
return;
|
|
4005
4096
|
}
|
|
4097
|
+
receipts("served");
|
|
4006
4098
|
emitContext(provider, "PreToolUse", text);
|
|
4007
4099
|
}
|
|
4008
4100
|
catch {
|
package/dist/core/docanchors.js
CHANGED
|
@@ -28,16 +28,40 @@ function fencedRanges(text) {
|
|
|
28
28
|
ranges.push([open.start, text.length]);
|
|
29
29
|
return ranges;
|
|
30
30
|
}
|
|
31
|
+
/** Character ranges covered by inline code spans (`…`), same rationale as
|
|
32
|
+
* fencedRanges: prose quoting a marker in backticks is showing an example.
|
|
33
|
+
* CommonMark-lite: an opener run pairs with the next run of the SAME length
|
|
34
|
+
* on the same line; unpaired runs never open a span. */
|
|
35
|
+
function inlineSpanRanges(text) {
|
|
36
|
+
const ranges = [];
|
|
37
|
+
let offset = 0;
|
|
38
|
+
for (const line of text.split("\n")) {
|
|
39
|
+
let pending = null;
|
|
40
|
+
const runs = /`+/g;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = runs.exec(line))) {
|
|
43
|
+
if (!pending)
|
|
44
|
+
pending = { len: m[0].length, start: m.index };
|
|
45
|
+
else if (m[0].length === pending.len) {
|
|
46
|
+
ranges.push([offset + pending.start, offset + m.index + m[0].length - 1]);
|
|
47
|
+
pending = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
offset += line.length + 1;
|
|
51
|
+
}
|
|
52
|
+
return ranges;
|
|
53
|
+
}
|
|
31
54
|
/** Parse every hunch:topic marker out of a markdown document. Markers inside
|
|
32
|
-
* fenced code blocks are examples, not declarations,
|
|
55
|
+
* fenced code blocks or inline code spans are examples, not declarations,
|
|
56
|
+
* and are skipped. */
|
|
33
57
|
export function parseDocAnchors(text) {
|
|
34
58
|
const out = [];
|
|
35
|
-
const
|
|
59
|
+
const skip = [...fencedRanges(text), ...inlineSpanRanges(text)];
|
|
36
60
|
MARKER.lastIndex = 0;
|
|
37
61
|
let m;
|
|
38
62
|
while ((m = MARKER.exec(text))) {
|
|
39
63
|
const at = m.index;
|
|
40
|
-
if (
|
|
64
|
+
if (skip.some(([s, e]) => at >= s && at <= e))
|
|
41
65
|
continue;
|
|
42
66
|
out.push({ topic: m[1], pin: m[2] ?? null, line: text.slice(0, at).split("\n").length });
|
|
43
67
|
}
|
package/dist/core/docscan.js
CHANGED
|
@@ -20,7 +20,11 @@ import { join, extname } from "node:path";
|
|
|
20
20
|
import { parseDocAnchors } from "./docanchors.js";
|
|
21
21
|
import { currentForTopic } from "./topics.js";
|
|
22
22
|
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
23
|
-
|
|
23
|
+
// Self-referential status declarations only. A bare "proposed" is too loose:
|
|
24
|
+
// the auto-generated grounding block in AGENTS.md legitimately DESCRIBES the
|
|
25
|
+
// proposed decision status ("candidate/proposed rules") and was graded stale
|
|
26
|
+
// for it — a false alarm in the machinery that polices false alarms.
|
|
27
|
+
export const STALE_MARKER = /\b(?:status|state)\s*[:\-—]\s*(?:proposed|draft)\b|\bnot yet implemented\b|\bno code yet\b/i;
|
|
24
28
|
export const SRC_REF = /\bsrc\/[A-Za-z0-9_\-/]+\.ts\b/g;
|
|
25
29
|
const SKIP_DIRS = new Set(["node_modules", ".git", ".hunch", ".hunch-private", "dist", "vscode-extension", "site"]);
|
|
26
30
|
/** Bounded walk for repo markdown (root + docs/, depth-limited; heavy/irrelevant trees skipped). */
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery receipts (roadmap dec_925f4bcaad): a machine-local ledger of which
|
|
3
|
+
* memory records were actually DELIVERED to an agent, when, and into what.
|
|
4
|
+
*
|
|
5
|
+
* This is observed telemetry, not derived state: it cannot be reconstructed
|
|
6
|
+
* from the JSON store, so it must NOT live in the reindex-rebuilt SQLite index
|
|
7
|
+
* (con_a87360128b's derived layer is dropped and rebuilt at will). It gets its
|
|
8
|
+
* own database under .hunch-cache/ — gitignored, per-machine, append-only —
|
|
9
|
+
* the same family as hookcache's session state, not the store's.
|
|
10
|
+
*
|
|
11
|
+
* Failure posture inherits the hook's: recording a receipt must never cost a
|
|
12
|
+
* delivery. Every entry point swallows every error; a lost receipt is noise,
|
|
13
|
+
* a blocked edit is a broken product.
|
|
14
|
+
*/
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { mkdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
/** Load node:sqlite while swallowing ONLY its ExperimentalWarning — the same
|
|
19
|
+
* discipline as src/store/db.ts: this module rides the hook into every CLI
|
|
20
|
+
* invocation, and Hunch's stderr reaches humans, hooks, and MCP clients. A
|
|
21
|
+
* bare top-level import printed the warning on every command and failed the
|
|
22
|
+
* release gate's clean-stderr contract. */
|
|
23
|
+
function loadSqlite() {
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const realEmit = process.emitWarning.bind(process);
|
|
26
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
27
|
+
if (String(warning).includes("SQLite is an experimental feature"))
|
|
28
|
+
return;
|
|
29
|
+
realEmit(warning, ...rest);
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
return require("node:sqlite");
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.emitWarning = realEmit;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let sqlite = null;
|
|
39
|
+
function openServedDb(root) {
|
|
40
|
+
sqlite ??= loadSqlite();
|
|
41
|
+
const dir = join(root, ".hunch-cache");
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
const db = new sqlite.DatabaseSync(join(dir, "served.db"));
|
|
44
|
+
db.exec(`CREATE TABLE IF NOT EXISTS served (
|
|
45
|
+
at TEXT NOT NULL,
|
|
46
|
+
session TEXT,
|
|
47
|
+
event TEXT NOT NULL,
|
|
48
|
+
kind TEXT NOT NULL,
|
|
49
|
+
record_id TEXT NOT NULL,
|
|
50
|
+
target TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS served_record ON served (record_id);`);
|
|
53
|
+
return db;
|
|
54
|
+
}
|
|
55
|
+
/** Append delivery receipts. Never throws — a receipt must never cost a delivery. */
|
|
56
|
+
export function recordServed(root, entries) {
|
|
57
|
+
if (!entries.length)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const db = openServedDb(root);
|
|
61
|
+
try {
|
|
62
|
+
const at = new Date().toISOString();
|
|
63
|
+
const insert = db.prepare("INSERT INTO served (at, session, event, kind, record_id, target) VALUES (?, ?, ?, ?, ?, ?)");
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
insert.run(at, entry.session_id ?? null, entry.event, entry.kind, entry.record_id, entry.target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* unwritable cache dir / locked db — the delivery already happened; drop the receipt */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** The ledger, aggregated per record. Never throws; an unreadable ledger reads as empty. */
|
|
77
|
+
export function servedSummary(root) {
|
|
78
|
+
const empty = { total: 0, distinct_records: 0, distinct_sessions: 0, first_at: null, last_at: null, rows: [] };
|
|
79
|
+
try {
|
|
80
|
+
const db = openServedDb(root);
|
|
81
|
+
try {
|
|
82
|
+
const totals = db.prepare("SELECT COUNT(*) AS total, COUNT(DISTINCT record_id) AS records, COUNT(DISTINCT session) AS sessions, MIN(at) AS first_at, MAX(at) AS last_at FROM served").get();
|
|
83
|
+
const rows = db.prepare(`SELECT record_id, kind,
|
|
84
|
+
SUM(CASE WHEN event = 'served' THEN 1 ELSE 0 END) AS serves,
|
|
85
|
+
SUM(CASE WHEN event = 'refreshed' THEN 1 ELSE 0 END) AS refreshes,
|
|
86
|
+
MAX(at) AS last_at
|
|
87
|
+
FROM served GROUP BY record_id, kind ORDER BY serves DESC, refreshes DESC`).all();
|
|
88
|
+
return {
|
|
89
|
+
total: totals?.total ?? 0,
|
|
90
|
+
distinct_records: totals?.records ?? 0,
|
|
91
|
+
distinct_sessions: totals?.sessions ?? 0,
|
|
92
|
+
first_at: totals?.first_at ?? null,
|
|
93
|
+
last_at: totals?.last_at ?? null,
|
|
94
|
+
rows,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
db.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return empty;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=served.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -49,6 +49,13 @@ const flushNote = (flush, home, mode) => flush === "pushed" ? ` (committed + pus
|
|
|
49
49
|
? " (committed to the overlay repo — push deferred: offline, no upstream, or merge conflict; the next capture or `hunch private --sync` retries)"
|
|
50
50
|
: " (auto-committed to .hunch/ — rides your next push)"
|
|
51
51
|
: "";
|
|
52
|
+
/** When an overlay exists, a PUBLIC write deserves one visible line: a record that
|
|
53
|
+
* lands in the committed store publishes on the next push, and an agent writing
|
|
54
|
+
* strategy/competitive content there is a leak nobody notices until it ships
|
|
55
|
+
* (2026-08-09: 15 roadmap records caught pre-push only by a release sweep). */
|
|
56
|
+
const publicHomeNote = (home, hasPrivate) => home === "public" && hasPrivate
|
|
57
|
+
? "\nℹ Landed in the COMMITTED PUBLIC store (publishes with the repo). For sensitive/strategy content, re-record with private:true — the overlay store."
|
|
58
|
+
: "";
|
|
52
59
|
// Read-side token budgets: every tool result is injected into a Claude Code
|
|
53
60
|
// session, so an uncapped list pollutes the context window. Cap each list to its
|
|
54
61
|
// highest-signal head (records are pre-sorted by severity/confidence) and tell the
|
|
@@ -903,7 +910,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
903
910
|
// record commits+pushes its overlay repo; a public one commits .hunch/ in THIS repo
|
|
904
911
|
// (commit only — it rides the user's next push, never auto-pushing their code branch).
|
|
905
912
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!decision.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
|
|
906
|
-
const flushed = flushNote(flush, home, store.mode);
|
|
913
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
|
|
907
914
|
// Capture-session gate (staged deprecation, §9.3): the token was consumed
|
|
908
915
|
// above (it also decides the provenance tier). No token still writes
|
|
909
916
|
// (non-breaking) but lands as agent_recorded with a nudge toward /capture.
|
|
@@ -980,7 +987,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
980
987
|
if (home === "public" && !store.autoCommit)
|
|
981
988
|
refreshExistingGrounding(root, store); // overlay rules never render into committed grounding
|
|
982
989
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!input.private, `hunch: capture ${rec.id}`, startupTeamRoute ?? undefined);
|
|
983
|
-
const flushed = flushNote(flush, home, store.mode);
|
|
990
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
|
|
984
991
|
const enforce = rec.severity === "blocking"
|
|
985
992
|
? "blocks a DIRECT edit to its scope at strict firmness, and fails a PR whose diff touches that scope (CI guard); blast-radius hits and lower firmness stay advisory"
|
|
986
993
|
: "flags violating edits and PRs (advisory)";
|
|
@@ -1056,7 +1063,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
1056
1063
|
store.putCapture("findings", rec, !!finding.private);
|
|
1057
1064
|
store.reindex();
|
|
1058
1065
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!finding.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
|
|
1059
|
-
const flushed = flushNote(flush, home, store.mode);
|
|
1066
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
|
|
1060
1067
|
const where = finding.private
|
|
1061
1068
|
? ` [PRIVATE overlay — not committed to this repo]${flushed}`
|
|
1062
1069
|
: home === "private" ? ` [SHARED store — one source of truth for the whole team]${flushed}` : flushed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"node": ">=22.13.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
+
"version": "node tooling/sync-version-pins.mjs && git add plugin/.mcp.json server.json",
|
|
56
|
+
"sync-version-pins": "node tooling/sync-version-pins.mjs",
|
|
55
57
|
"clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
56
58
|
"build": "npm run clean && tsc -p tsconfig.json",
|
|
57
59
|
"dev": "tsx src/cli/index.ts",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://hunch-pi.vercel.app",
|
|
10
|
-
"version": "1.12.
|
|
10
|
+
"version": "1.12.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.12.
|
|
16
|
+
"version": "1.12.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|