@davesheffer/hunch 1.41.6 → 1.43.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.
Files changed (48) hide show
  1. package/README.md +5 -1
  2. package/dist/cli/index.js +489 -167
  3. package/dist/constitution/experiment.d.ts +3 -3
  4. package/dist/constitution/g3.d.ts +1 -1
  5. package/dist/core/delivery.d.ts +12 -0
  6. package/dist/core/delivery.js +4 -4
  7. package/dist/core/footprint.d.ts +15 -0
  8. package/dist/core/footprint.js +167 -0
  9. package/dist/core/groundingLag.d.ts +15 -0
  10. package/dist/core/groundingLag.js +27 -0
  11. package/dist/core/hookText.d.ts +4 -0
  12. package/dist/core/hookText.js +8 -0
  13. package/dist/core/hookcache.d.ts +22 -0
  14. package/dist/core/hookcache.js +53 -2
  15. package/dist/core/pipeline.d.ts +28 -0
  16. package/dist/core/pipeline.js +50 -0
  17. package/dist/core/publication.js +17 -3
  18. package/dist/core/shellwrites.d.ts +7 -0
  19. package/dist/core/shellwrites.js +131 -0
  20. package/dist/core/siblingfix.d.ts +124 -0
  21. package/dist/core/siblingfix.js +814 -0
  22. package/dist/core/taskReportHook.d.ts +1 -1
  23. package/dist/core/taskReportHook.js +20 -4
  24. package/dist/core/taskSelection.d.ts +67 -0
  25. package/dist/core/taskSelection.js +196 -0
  26. package/dist/extractors/git.d.ts +31 -0
  27. package/dist/extractors/git.js +180 -1
  28. package/dist/extractors/nativeTreeSitter.d.ts +2 -1
  29. package/dist/extractors/nativeTreeSitter.js +128 -30
  30. package/dist/integrations/claudemd.d.ts +20 -2
  31. package/dist/integrations/claudemd.js +79 -53
  32. package/dist/integrations/providers.d.ts +9 -5
  33. package/dist/integrations/providers.js +36 -23
  34. package/dist/integrations/scaffold.js +2 -1
  35. package/dist/integrations/team.d.ts +24 -4
  36. package/dist/integrations/team.js +154 -16
  37. package/dist/integrations/worktree.d.ts +3 -2
  38. package/dist/integrations/worktree.js +7 -4
  39. package/dist/mcp/server.d.ts +489 -0
  40. package/dist/mcp/server.js +208 -62
  41. package/dist/mcp/taskReportTools.js +12 -9
  42. package/dist/mcp/toolset.d.ts +11 -1
  43. package/dist/mcp/toolset.js +28 -8
  44. package/dist/store/hunchStore.d.ts +25 -1
  45. package/dist/store/hunchStore.js +146 -21
  46. package/dist/store/jsonStore.js +25 -4
  47. package/package.json +1 -1
  48. package/server.json +2 -2
@@ -0,0 +1,131 @@
1
+ /** Files a shell command wrote.
2
+ *
3
+ * Pre-edit grounding keys on the host's edit tools (Edit/Write/apply_patch).
4
+ * An agent that edits through the shell — a `python` heredoc, `sed -i`, `perl
5
+ * -pi`, a PowerShell `Set-Content` — never passes through them, so the file it
6
+ * changed arrives with no grounding at all. The shell tool's post-execution
7
+ * hook sees every command, but not what the command touched; parsing arbitrary
8
+ * shell for write targets is guesswork.
9
+ *
10
+ * Instead: fingerprint the working tree's dirty files (`git status`, then
11
+ * mtime+size) per session, repository and agent. A prompt and every tool call refresh
12
+ * the fingerprint; after a shell command, a dirty file whose fingerprint moved
13
+ * was written by that command. Deterministic, shell-agnostic, and bounded by the
14
+ * dirty set (never a tree walk). Any failure yields no files — never an error. */
15
+ import { execFileSync } from "node:child_process";
16
+ import { createHash } from "node:crypto";
17
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ /** Dirty paths fingerprinted; beyond it the rest are ignored (a vendored
21
+ * untracked tree must not make every command slow). */
22
+ const MAX_PATHS = 2000;
23
+ /** Hunch's own state: written by the hook itself and by capture tools. */
24
+ const OWN_STATE = /^\.hunch(?:-cache)?\//;
25
+ /** Concurrent subagents share the session but not their commands: each keeps
26
+ * its own baseline, keyed by its agent id (hashed with the rest, never kept
27
+ * raw) like the pre-edit dedupe. No agent id keeps the session's baseline. */
28
+ function snapshotFile(root, sessionId, agentId) {
29
+ const key = createHash("sha256").update(`${sessionId}\u0000${root}${agentId ? `\u0000${agentId}` : ""}`).digest("hex").slice(0, 24);
30
+ // Same directory as the session injection cache: its sweep drops stale files.
31
+ return join(tmpdir(), "hunch-hookcache", `shell-${key}.json`);
32
+ }
33
+ /** Repo-relative dirty paths (tracked changes and untracked files), NUL-safe. */
34
+ function dirtyPaths(root) {
35
+ let raw;
36
+ try {
37
+ raw = execFileSync("git", ["-C", root, "status", "--porcelain=v1", "-z", "--untracked-files=all"], {
38
+ encoding: "utf8",
39
+ timeout: 2_000,
40
+ maxBuffer: 8_000_000,
41
+ stdio: ["ignore", "pipe", "ignore"],
42
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
43
+ });
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ // Porcelain paths are relative to the git toplevel, which can sit above the
49
+ // Hunch root (a package inside a monorepo): re-relativize, drop the outside.
50
+ let prefix = "";
51
+ try {
52
+ prefix = execFileSync("git", ["-C", root, "rev-parse", "--show-prefix"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim();
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ const out = [];
58
+ const fields = raw.split("\u0000");
59
+ for (let i = 0; i < fields.length && out.length < MAX_PATHS; i++) {
60
+ const entry = fields[i];
61
+ if (entry.length < 4)
62
+ continue;
63
+ const status = entry.slice(0, 2);
64
+ const path = entry.slice(3);
65
+ if (path.startsWith(prefix))
66
+ out.push(path.slice(prefix.length));
67
+ // A rename/copy entry is followed by its source path.
68
+ if (status.includes("R") || status.includes("C"))
69
+ i++;
70
+ }
71
+ return out;
72
+ }
73
+ function fingerprint(root) {
74
+ const paths = dirtyPaths(root);
75
+ if (!paths)
76
+ return null;
77
+ const fp = {};
78
+ for (const p of paths) {
79
+ if (OWN_STATE.test(p))
80
+ continue;
81
+ try {
82
+ const st = statSync(join(root, p));
83
+ if (st.isFile())
84
+ fp[p] = `${st.mtimeMs}:${st.size}`;
85
+ }
86
+ catch { /* deleted: nothing to ground */ }
87
+ }
88
+ return fp;
89
+ }
90
+ function load(file) {
91
+ try {
92
+ const raw = JSON.parse(readFileSync(file, "utf8"));
93
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ function save(file, fp) {
100
+ try {
101
+ mkdirSync(join(tmpdir(), "hunch-hookcache"), { recursive: true });
102
+ writeFileSync(file, JSON.stringify(fp));
103
+ }
104
+ catch { /* the next refresh retries */ }
105
+ }
106
+ /** Record the working tree as it stands, so later shell writes are measured
107
+ * from here (a prompt, or any tool call that is not a shell command). */
108
+ export function refreshShellBaseline(root, sessionId, agentId) {
109
+ if (!sessionId)
110
+ return;
111
+ const fp = fingerprint(root);
112
+ if (fp)
113
+ save(snapshotFile(root, sessionId, agentId), fp);
114
+ }
115
+ /** Repo-relative files the shell command that just ran wrote (created or
116
+ * modified), and the baseline moves forward. Empty without a baseline: a
117
+ * session's first observation cannot tell its own writes from earlier ones. */
118
+ export function shellWrittenFiles(root, sessionId, agentId) {
119
+ if (!sessionId)
120
+ return [];
121
+ const file = snapshotFile(root, sessionId, agentId);
122
+ const before = load(file);
123
+ const now = fingerprint(root);
124
+ if (!now)
125
+ return [];
126
+ save(file, now);
127
+ if (!before)
128
+ return [];
129
+ return Object.keys(now).filter((p) => before[p] !== now[p]).sort();
130
+ }
131
+ //# sourceMappingURL=shellwrites.js.map
@@ -0,0 +1,124 @@
1
+ export interface SiblingCommit {
2
+ sha: string;
3
+ subject: string;
4
+ fix: boolean;
5
+ /** The commit's changed lines inside the sibling, in hunk order, `+`/`-` prefixed. */
6
+ change: string[];
7
+ /** Test files the same commit changed: the proof that came with the fix. */
8
+ tests?: string[];
9
+ }
10
+ export interface SiblingLesson {
11
+ symbol: string;
12
+ file: string;
13
+ line: number;
14
+ sibling: string;
15
+ siblingFile: string;
16
+ siblingStart: number;
17
+ siblingEnd: number;
18
+ similarity: number;
19
+ commits: SiblingCommit[];
20
+ }
21
+ export interface SiblingOptions {
22
+ /** Candidate pairs whose history is inspected (best similarity first). */
23
+ maxPairs?: number;
24
+ /** Lessons returned. */
25
+ maxLessons?: number;
26
+ /** Per git call; each call is also capped by what is left of `budgetMs`. */
27
+ timeoutMs?: number;
28
+ /** Wall-clock budget for the whole computation; past it the result is incomplete. */
29
+ budgetMs?: number;
30
+ /** Cap on the no-index inventory scan (tests force truncation with it). */
31
+ inventoryBudgetMs?: number;
32
+ /** Set false to bypass the .hunch-cache read/write (tests). */
33
+ cache?: boolean;
34
+ /** Clock for the incomplete-run retry window (tests). */
35
+ now?: number;
36
+ }
37
+ interface IndexedSymbol {
38
+ file: string;
39
+ name: string;
40
+ kind: string;
41
+ }
42
+ /** `isHunchProviderHook` → {is, hunch, provider, hook}. */
43
+ export declare function nameTokens(name: string): Set<string>;
44
+ /** Identifier-ish tokens of a body's CODE, including those inside regex/string
45
+ * literals. Comment lines are dropped: a fix usually adds an explanation, and
46
+ * its prose would otherwise swamp the shared shape. */
47
+ export declare function codeTokens(text: string): Set<string>;
48
+ /** Overlap coefficient: shared / smaller set. A hardened copy grows, so
49
+ * containment — not Jaccard — is what "same shape" means for bodies. */
50
+ export declare function overlap(a: ReadonlySet<string>, b: ReadonlySet<string>): number;
51
+ export declare function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number;
52
+ export declare function isFixSubject(subject: string): boolean;
53
+ export interface LineLogCommit {
54
+ sha: string;
55
+ subject: string;
56
+ added: string[];
57
+ removed: string[];
58
+ /** Added and removed lines in hunk order, `+`/`-` prefixed. */
59
+ diff: string[];
60
+ /** Largest old/new line span of any hunk — how far git's range reached. */
61
+ span: number;
62
+ /** Every hunk starts from nothing (`@@ -0,0 …`): the function was created here. */
63
+ created: boolean;
64
+ }
65
+ /** Parse `git log -L … --format=%x1eC %H%x1f%s` output. */
66
+ export declare function parseLineLog(output: string): LineLogCommit[];
67
+ /** A commit whose removed and added lines differ only in whitespace/line
68
+ * endings (a reformat, a CRLF→LF sweep, a re-indent) changed nothing. */
69
+ export declare function isSubstantive(commit: LineLogCommit): boolean;
70
+ /** git follows a line range backwards only while it can map it. A whole-file
71
+ * rewrite (a line-ending sweep, a reformat) breaks the mapping: from that
72
+ * commit on, every hunk spans the whole file and nothing in it is attributable
73
+ * to the function. Keep the history before that commit and report where the
74
+ * mapping was lost. */
75
+ export declare function attributable(commits: readonly LineLogCommit[], fnLines: number): {
76
+ kept: LineLogCommit[];
77
+ lostAt: string | null;
78
+ };
79
+ /** Candidate scans run (tests: a cache hit must not run one). */
80
+ export declare const siblingCounters: {
81
+ candidateScans: number;
82
+ lessonComputes: number;
83
+ };
84
+ /** Sibling-fix lessons for one repo-relative file. */
85
+ export declare function siblingLessonsFor(root: string, file: string, symbols: readonly IndexedSymbol[], options?: SiblingOptions): SiblingLesson[];
86
+ /** Stable identity for hook dedupe: which fixes were surfaced, not the wording. */
87
+ export declare function siblingLessonsIdentity(lessons: readonly SiblingLesson[]): string;
88
+ /** A commit's diff inside the function, blank-only lines dropped, capped
89
+ * (a fix commit gets the larger cap). */
90
+ export declare function changeLines(diff: readonly string[], fix?: boolean): string[];
91
+ export declare const SIBLING_HEADING = "## \u26A0 Fix not carried to this function";
92
+ /** Callers of each lesson's function, by lesson symbol: names of functions in
93
+ * the same working-tree file whose body calls it. */
94
+ export type SiblingCallers = ReadonlyMap<string, readonly string[]>;
95
+ /** Written to be acted on, not skimmed: an agent reads "possible, heuristic,
96
+ * elsewhere" as out of scope and moves on (trap-310: three deliveries, zero
97
+ * follow-ups). So the lesson says what this copy lacks, which code here runs
98
+ * through it, and asks for an explicit outcome. It stays advisory: "does not
99
+ * apply, because …" is always an accepted answer. */
100
+ /** A blocking invariant whose scope covers the lesson's file. */
101
+ export interface SiblingInvariant {
102
+ id: string;
103
+ statement: string;
104
+ }
105
+ /** "Pre-existing" and "outside this task" are how an agent that agrees with the
106
+ * lesson still leaves it (trap-310 v4: gap confirmed, reported, not fixed).
107
+ * The change in hand runs through the copy, so only "cannot reach it" or
108
+ * "already handled" count as not applying. */
109
+ export declare const NOT_A_REASON = "\"Pre-existing\" or \"outside this task\" does not count: the change you are making runs through this copy, so leaving it ships the gap again inside your change.";
110
+ export declare function renderSiblingLessons(lessons: readonly SiblingLesson[], callers?: SiblingCallers, invariants?: readonly SiblingInvariant[]): string;
111
+ /** Hash of the function's current body, or null when it cannot be found. A
112
+ * changed hash means the agent touched the function after the lesson. */
113
+ export declare function functionBodyHash(root: string, file: string, symbol: string): string | null;
114
+ export interface SiblingGrounding {
115
+ text: string;
116
+ identity: string;
117
+ lessons: SiblingLesson[];
118
+ callers: SiblingCallers;
119
+ }
120
+ /** Grounding-path entry: the rendered block plus its dedupe identity for one
121
+ * repo-relative file. Never throws — a parser load failure or a git error
122
+ * means no sibling lessons, not a failed edit hook. */
123
+ export declare function siblingGrounding(root: string, file: string, symbols: readonly IndexedSymbol[], invariants?: readonly SiblingInvariant[]): SiblingGrounding;
124
+ export {};