@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 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. */
@@ -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
- /** The six entity collections, keyed by their on-disk directory name. */
167
- export const ENTITY_KINDS = ["components", "edges", "symbols", "decisions", "bugs", "constraints"];
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 = []) {
@@ -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) {
@@ -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.24.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.",