@davesheffer/hunch 0.24.0 → 0.25.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 +18 -0
- package/dist/cli/index.js +52 -1
- package/dist/core/ids.js +5 -0
- package/dist/core/types.js +20 -2
- package/dist/extractors/git.js +6 -0
- package/dist/store/hunchStore.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,24 @@ came before. Local-first, no documentation toil, no SaaS.
|
|
|
66
66
|
[provenance](https://hunch-pi.vercel.app/docs#provenance) ·
|
|
67
67
|
[time-travel](https://hunch-pi.vercel.app/docs#time-travel)
|
|
68
68
|
|
|
69
|
+
## Why Hunch is different
|
|
70
|
+
|
|
71
|
+
"Memory for coding agents" is getting crowded, but most of it is a *server-side, ephemeral,
|
|
72
|
+
single-vendor* RAG cache over your current code. Hunch is the opposite on every axis — and that
|
|
73
|
+
combination is the moat:
|
|
74
|
+
|
|
75
|
+
| | Typical agent memory | **Hunch** |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| **Storage** | server-side / a vendor's cloud | **git-tracked JSON in your repo** — diff it, review it in PRs, sync it over `git push` |
|
|
78
|
+
| **Lifetime** | the session; often auto-expiring | the **lifetime of the codebase** — non-destructive supersede/veto keeps the *why-it-changed* trail |
|
|
79
|
+
| **Clients** | one vendor's agent | **client-agnostic** — one `.hunch/` graph serves Claude Code, Cursor, Copilot & Windsurf via MCP |
|
|
80
|
+
| **What's stored** | opaque extracted "facts" | **structured ADRs** — decisions with rejected-alternatives, bug lineage, and invariants |
|
|
81
|
+
| **Enforcement** | advisory / just-in-time hints | **fail-closed deterministic guards** — no model in the block path; a commit fails on a human-vouched, set-intersection match |
|
|
82
|
+
| **Trust** | take it on faith | **provenance on every record** (source + confidence + evidence) and a measurable retrieval signal (`hunch eval`) |
|
|
83
|
+
|
|
84
|
+
The short version: **git tracks *what* changed; Hunch tracks *why*** — locally, durably, and under
|
|
85
|
+
your control, with guards that actually hold the line instead of just suggesting.
|
|
86
|
+
|
|
69
87
|
## Getting started
|
|
70
88
|
|
|
71
89
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -28,7 +28,8 @@ import { indexRepo } from "../extractors/indexer.js";
|
|
|
28
28
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
29
29
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
30
30
|
import { selectProvider } from "../synthesis/provider.js";
|
|
31
|
-
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists, commitAndPushHunch, gitUntrackCached } from "../extractors/git.js";
|
|
31
|
+
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, gitUntrackCached } from "../extractors/git.js";
|
|
32
|
+
import { runbookId } from "../core/ids.js";
|
|
32
33
|
import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
|
|
33
34
|
import { partitionReview, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
34
35
|
import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
|
|
@@ -454,6 +455,56 @@ program
|
|
|
454
455
|
}
|
|
455
456
|
store.close();
|
|
456
457
|
});
|
|
458
|
+
// ---- runbook (distill reusable "how" from a commit range; roadmap #5) ------
|
|
459
|
+
program
|
|
460
|
+
.command("runbook")
|
|
461
|
+
.description("Distill a reusable runbook (the 'how' of a recurring task) from a commit range. Advisory; refine before relying on it.")
|
|
462
|
+
.argument("<range>", "commit range: <base>..<head>, or <base> (→ <base>..HEAD)")
|
|
463
|
+
.requiredOption("--task <task>", "the recurring task this runbook answers")
|
|
464
|
+
.option("--private", "write into the private overlay (HUNCH_PRIVATE_DIR), not the committed repo")
|
|
465
|
+
.action((range, opts) => {
|
|
466
|
+
const { store, root } = storeFor();
|
|
467
|
+
if (opts.private && !store.hasPrivate) {
|
|
468
|
+
store.close();
|
|
469
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
470
|
+
}
|
|
471
|
+
const [base, head = "HEAD"] = range.split("..");
|
|
472
|
+
if (!base || !revExists(base, root)) {
|
|
473
|
+
store.close();
|
|
474
|
+
return fail(`base ref "${base ?? ""}" not found (range: ${range})`);
|
|
475
|
+
}
|
|
476
|
+
const steps = rangeSubjects(base, root, head);
|
|
477
|
+
const files = rangeFiles(base, root, head);
|
|
478
|
+
if (!steps.length && !files.length) {
|
|
479
|
+
store.close();
|
|
480
|
+
return fail(`no commits or changes in range ${range}`);
|
|
481
|
+
}
|
|
482
|
+
const now = new Date().toISOString();
|
|
483
|
+
const rec = {
|
|
484
|
+
id: runbookId(opts.task),
|
|
485
|
+
task: opts.task,
|
|
486
|
+
trigger: [opts.task],
|
|
487
|
+
steps, // already oldest-first (chronological procedure)
|
|
488
|
+
files,
|
|
489
|
+
gotchas: [],
|
|
490
|
+
outcome: "",
|
|
491
|
+
source_range: range,
|
|
492
|
+
valid_from: now,
|
|
493
|
+
valid_to: null,
|
|
494
|
+
// Deterministic draft (commit subjects + changed files); advisory, low-confidence.
|
|
495
|
+
// Refine steps/gotchas by hand. LLM enrichment is a later tier.
|
|
496
|
+
provenance: { source: "extracted", confidence: 0.5, evidence: [range] },
|
|
497
|
+
date: now,
|
|
498
|
+
};
|
|
499
|
+
if (opts.private)
|
|
500
|
+
store.putPrivate("runbooks", rec);
|
|
501
|
+
else
|
|
502
|
+
store.json.put("runbooks", rec);
|
|
503
|
+
store.reindex();
|
|
504
|
+
console.log(`✓ runbook ${rec.id} — "${rec.task}" (${rec.steps.length} steps, ${rec.files.length} files)${opts.private ? " [private overlay]" : ""}`);
|
|
505
|
+
console.log(dim(" advisory, deterministic draft — refine the steps/gotchas; surfaced via `hunch query` and MCP."));
|
|
506
|
+
store.close();
|
|
507
|
+
});
|
|
457
508
|
// ---- embed (opt-in semantic search) ---------------------------------------
|
|
458
509
|
program
|
|
459
510
|
.command("embed")
|
package/dist/core/ids.js
CHANGED
|
@@ -32,6 +32,11 @@ export function decisionId(seed) {
|
|
|
32
32
|
export function bugId(seed) {
|
|
33
33
|
return "bug_" + shortHash(seed);
|
|
34
34
|
}
|
|
35
|
+
/** Runbook id seeded by its task; trim + lowercase so re-capturing the same task
|
|
36
|
+
* is idempotent rather than minting a duplicate. */
|
|
37
|
+
export function runbookId(seed) {
|
|
38
|
+
return "rb_" + shortHash(seed.trim().toLowerCase());
|
|
39
|
+
}
|
|
35
40
|
/** Constraint id seeded by its statement. Trim + lowercase so trivial
|
|
36
41
|
* whitespace/case variants of the same rule collapse to one id (idempotent
|
|
37
42
|
* re-capture), instead of minting a duplicate constraint. */
|
package/dist/core/types.js
CHANGED
|
@@ -163,8 +163,25 @@ export const ConstraintSchema = z.object({
|
|
|
163
163
|
valid_to: z.string().nullable().default(null).describe("ISO instant it was retired (null = active)"),
|
|
164
164
|
provenance: ProvenanceSchema,
|
|
165
165
|
});
|
|
166
|
-
/**
|
|
167
|
-
|
|
166
|
+
/** A reusable "how" for a recurring task — trajectory/runbook memory (roadmap #5).
|
|
167
|
+
* ADVISORY retrieval context only; never enters any block path. Distilled from a
|
|
168
|
+
* commit range, surfaced through the same FTS+graph retrieval as every record. */
|
|
169
|
+
export const RunbookSchema = z.object({
|
|
170
|
+
id: z.string().describe("rb_*"),
|
|
171
|
+
task: z.string().describe("the recurring task this answers"),
|
|
172
|
+
trigger: z.array(z.string()).default([]).describe("phrases/intents that should surface it"),
|
|
173
|
+
steps: z.array(z.string()).default([]).describe("ordered procedure"),
|
|
174
|
+
files: z.array(z.string()).default([]).describe("canonical files the task touches (drift-checkable)"),
|
|
175
|
+
gotchas: z.array(z.string()).default([]),
|
|
176
|
+
outcome: z.string().default("").describe("what 'done' looks like"),
|
|
177
|
+
source_range: z.string().nullable().default(null).describe("the commit range it was distilled from"),
|
|
178
|
+
valid_from: z.string().optional(),
|
|
179
|
+
valid_to: z.string().nullable().default(null),
|
|
180
|
+
provenance: ProvenanceSchema,
|
|
181
|
+
date: z.string(),
|
|
182
|
+
});
|
|
183
|
+
/** The entity collections, keyed by their on-disk directory name. */
|
|
184
|
+
export const ENTITY_KINDS = ["components", "edges", "symbols", "decisions", "bugs", "constraints", "runbooks"];
|
|
168
185
|
export const SCHEMAS = {
|
|
169
186
|
components: ComponentSchema,
|
|
170
187
|
edges: EdgeSchema,
|
|
@@ -172,6 +189,7 @@ export const SCHEMAS = {
|
|
|
172
189
|
decisions: DecisionSchema,
|
|
173
190
|
bugs: BugSchema,
|
|
174
191
|
constraints: ConstraintSchema,
|
|
192
|
+
runbooks: RunbookSchema,
|
|
175
193
|
};
|
|
176
194
|
/** Default provenance helper for deterministic (extracted) records. */
|
|
177
195
|
export function extracted(confidence, evidence = []) {
|
package/dist/extractors/git.js
CHANGED
|
@@ -194,6 +194,12 @@ export function rangeFiles(base, cwd, head = "HEAD") {
|
|
|
194
194
|
const out = gitSafe(["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`], cwd);
|
|
195
195
|
return out ? out.split("\n").filter(Boolean) : [];
|
|
196
196
|
}
|
|
197
|
+
/** Commit subjects on `head` since `base` (2-dot: commits added by the task),
|
|
198
|
+
* oldest-first, for distilling a runbook's ordered steps (roadmap #5). */
|
|
199
|
+
export function rangeSubjects(base, cwd, head = "HEAD", max = 50) {
|
|
200
|
+
const out = gitSafe(["log", "--reverse", `-n${max}`, "--format=%s", `${base}..${head}`], cwd);
|
|
201
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
202
|
+
}
|
|
197
203
|
/** The PR's unified diff vs `base` (3-dot), for the Regression Guard's structural
|
|
198
204
|
* analysis. Same noise-exclusion + truncation budget as commit/staged diffs. */
|
|
199
205
|
export function rangeDiff(base, cwd, head = "HEAD", maxBytes = 60_000) {
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -179,6 +179,13 @@ export class HunchStore {
|
|
|
179
179
|
fts(c.id, "constraints", c.statement, `${c.rationale} ${c.scope.join(" ")}`);
|
|
180
180
|
}
|
|
181
181
|
counts.constraints = cons.length;
|
|
182
|
+
// Runbooks (roadmap #5): advisory "how" records — no dedicated SQL table, they
|
|
183
|
+
// ride the unified `search` FTS so they retrieve through the same FTS+graph path.
|
|
184
|
+
const runbooks = this.recs("runbooks");
|
|
185
|
+
for (const r of runbooks) {
|
|
186
|
+
fts(r.id, "runbooks", r.task, `${r.trigger.join(" ")} ${r.steps.join(" ")} ${r.gotchas.join(" ")} ${r.outcome} ${r.files.join(" ")}`);
|
|
187
|
+
}
|
|
188
|
+
counts.runbooks = runbooks.length;
|
|
182
189
|
void j;
|
|
183
190
|
});
|
|
184
191
|
tx();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.",
|