@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.
- package/README.md +5 -1
- package/dist/cli/index.js +489 -167
- package/dist/constitution/experiment.d.ts +3 -3
- package/dist/constitution/g3.d.ts +1 -1
- package/dist/core/delivery.d.ts +12 -0
- package/dist/core/delivery.js +4 -4
- package/dist/core/footprint.d.ts +15 -0
- package/dist/core/footprint.js +167 -0
- package/dist/core/groundingLag.d.ts +15 -0
- package/dist/core/groundingLag.js +27 -0
- package/dist/core/hookText.d.ts +4 -0
- package/dist/core/hookText.js +8 -0
- package/dist/core/hookcache.d.ts +22 -0
- package/dist/core/hookcache.js +53 -2
- package/dist/core/pipeline.d.ts +28 -0
- package/dist/core/pipeline.js +50 -0
- package/dist/core/publication.js +17 -3
- package/dist/core/shellwrites.d.ts +7 -0
- package/dist/core/shellwrites.js +131 -0
- package/dist/core/siblingfix.d.ts +124 -0
- package/dist/core/siblingfix.js +814 -0
- package/dist/core/taskReportHook.d.ts +1 -1
- package/dist/core/taskReportHook.js +20 -4
- package/dist/core/taskSelection.d.ts +67 -0
- package/dist/core/taskSelection.js +196 -0
- package/dist/extractors/git.d.ts +31 -0
- package/dist/extractors/git.js +180 -1
- package/dist/extractors/nativeTreeSitter.d.ts +2 -1
- package/dist/extractors/nativeTreeSitter.js +128 -30
- package/dist/integrations/claudemd.d.ts +20 -2
- package/dist/integrations/claudemd.js +79 -53
- package/dist/integrations/providers.d.ts +9 -5
- package/dist/integrations/providers.js +36 -23
- package/dist/integrations/scaffold.js +2 -1
- package/dist/integrations/team.d.ts +24 -4
- package/dist/integrations/team.js +154 -16
- package/dist/integrations/worktree.d.ts +3 -2
- package/dist/integrations/worktree.js +7 -4
- package/dist/mcp/server.d.ts +489 -0
- package/dist/mcp/server.js +208 -62
- package/dist/mcp/taskReportTools.js +12 -9
- package/dist/mcp/toolset.d.ts +11 -1
- package/dist/mcp/toolset.js +28 -8
- package/dist/store/hunchStore.d.ts +25 -1
- package/dist/store/hunchStore.js +146 -21
- package/dist/store/jsonStore.js +25 -4
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, utimesSync } from "node:fs";
|
|
4
|
+
import { tmpdir, userInfo } from "node:os";
|
|
5
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
5
6
|
const runtimeRequire = createRequire(import.meta.url);
|
|
7
|
+
/** Legacy per-process copy dirs (`hunch-tree-sitter-<pid>-*`), pruned when dead. */
|
|
6
8
|
const COPY_PREFIX = "hunch-tree-sitter-";
|
|
9
|
+
/** Per-user cache of content-addressed copies (`hunch-tree-sitter-cache-<user>`). */
|
|
10
|
+
const CACHE_PREFIX = "hunch-tree-sitter-cache-";
|
|
11
|
+
/** A hash dir no process has used for this long is pruned. */
|
|
12
|
+
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
13
|
const NATIVE_PACKAGES = [
|
|
8
14
|
"tree-sitter",
|
|
9
15
|
"tree-sitter-typescript",
|
|
@@ -84,21 +90,116 @@ function removeStaleCopies() {
|
|
|
84
90
|
function environmentKey(packageName) {
|
|
85
91
|
return `${packageName.toUpperCase().replaceAll("-", "_")}_PREBUILD`;
|
|
86
92
|
}
|
|
87
|
-
|
|
93
|
+
/** The per-user copy cache. A binary copied to a fresh path costs a first-load
|
|
94
|
+
* assessment (macOS checks every never-seen dylib: ~2s per addon, six addons)
|
|
95
|
+
* on every process; the same bytes at a path already loaded once cost ~0ms. So
|
|
96
|
+
* copies are content-addressed and reused across processes instead of made per
|
|
97
|
+
* process. The directory is private to the user: an addon dlopen'd from a
|
|
98
|
+
* location another account can write would be code execution as this user. */
|
|
99
|
+
function cacheRoot() {
|
|
100
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
101
|
+
let user = String(uid ?? "");
|
|
102
|
+
if (!user) {
|
|
103
|
+
try {
|
|
104
|
+
user = userInfo().username;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
user = "user";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const root = join(tmpdir(), `${CACHE_PREFIX}${user.replace(/[^A-Za-z0-9_.-]/g, "_")}`);
|
|
111
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
112
|
+
const st = lstatSync(root);
|
|
113
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
114
|
+
throw new Error(`tree-sitter copy cache is not a directory: ${root}`);
|
|
115
|
+
// POSIX: refuse a cache another account owns or can write into. (Windows has
|
|
116
|
+
// no uid and a per-user TEMP; its ACLs are not mode bits.)
|
|
117
|
+
if (uid !== null && (st.uid !== uid || (st.mode & 0o022) !== 0)) {
|
|
118
|
+
throw new Error(`tree-sitter copy cache is not private to this user: ${root}`);
|
|
119
|
+
}
|
|
120
|
+
return root;
|
|
121
|
+
}
|
|
122
|
+
function sha256(bytes) {
|
|
123
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
function copyNativeBinding(packageName, root, nodeGypBuild) {
|
|
88
126
|
const packageRoot = dirname(runtimeRequire.resolve(`${packageName}/package.json`));
|
|
89
127
|
const source = nodeGypBuild.path(packageRoot);
|
|
90
|
-
const
|
|
128
|
+
const bytes = readFileSync(source);
|
|
129
|
+
const digest = sha256(bytes);
|
|
130
|
+
const hashDir = join(root, digest.slice(0, 32));
|
|
131
|
+
const packageCopy = join(hashDir, packageName);
|
|
91
132
|
const normalized = source.replaceAll("\\", "/");
|
|
92
133
|
const prebuild = /\/prebuilds\/([^/]+)\/[^/]+$/.exec(normalized);
|
|
93
134
|
const destination = prebuild
|
|
94
135
|
? join(packageCopy, "prebuilds", prebuild[1], basename(source))
|
|
95
136
|
: join(packageCopy, "build", "Release", basename(source));
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
// Reuse only bytes that still hash to the installed binary: a truncated or
|
|
138
|
+
// replaced copy is rewritten, never loaded.
|
|
139
|
+
const intact = () => {
|
|
140
|
+
try {
|
|
141
|
+
return sha256(readFileSync(destination)) === digest;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
// Mark the hash dir in use BEFORE verifying it, so a concurrent prune
|
|
148
|
+
// cannot remove it between the check and the dlopen.
|
|
149
|
+
try {
|
|
150
|
+
const now = new Date();
|
|
151
|
+
utimesSync(hashDir, now, now);
|
|
152
|
+
}
|
|
153
|
+
catch { /* not created yet */ }
|
|
154
|
+
if (!intact()) {
|
|
155
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
156
|
+
// Atomic: a concurrent process sees the old file or the complete new one.
|
|
157
|
+
const partial = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
158
|
+
try {
|
|
159
|
+
copyFileSync(source, partial);
|
|
160
|
+
renameSync(partial, destination);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
try {
|
|
164
|
+
rmSync(partial, { force: true });
|
|
165
|
+
}
|
|
166
|
+
catch { /* best effort */ }
|
|
167
|
+
// Windows refuses to replace a copy another process has loaded; that copy
|
|
168
|
+
// is then the same bytes, which is all this needs.
|
|
169
|
+
if (!intact())
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
if (!intact())
|
|
173
|
+
throw new Error(`tree-sitter copy does not match its source: ${destination}`);
|
|
174
|
+
}
|
|
98
175
|
return packageCopy;
|
|
99
176
|
}
|
|
177
|
+
/** Hash dirs unused for CACHE_TTL_MS belong to replaced installs. Deleting one
|
|
178
|
+
* that is still loaded fails on Windows (retried by a later process) and is
|
|
179
|
+
* harmless on POSIX (the mapping outlives the directory entry). */
|
|
180
|
+
function pruneCache(root, keep) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const cutoff = Date.now() - CACHE_TTL_MS;
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (!entry.isDirectory() || keep.has(entry.name))
|
|
191
|
+
continue;
|
|
192
|
+
const dir = join(root, entry.name);
|
|
193
|
+
try {
|
|
194
|
+
if (statSync(dir).mtimeMs < cutoff)
|
|
195
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 2 });
|
|
196
|
+
}
|
|
197
|
+
catch { /* in use or already gone */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
100
200
|
/** Load all native tree-sitter addons (the parser runtime + every grammar) from
|
|
101
|
-
*
|
|
201
|
+
* content-addressed copies in a per-user temp cache — never from the installed
|
|
202
|
+
* package. Windows keeps loaded `.node` files locked for the
|
|
102
203
|
* process lifetime; redirecting the upstream loaders means npm can replace the
|
|
103
204
|
* installed package during an active MCP session without killing that session
|
|
104
205
|
* or falling back to a stale binary. */
|
|
@@ -126,25 +227,33 @@ function loadRuntime() {
|
|
|
126
227
|
// already-loaded source-built addon slip past this guard and defeat the
|
|
127
228
|
// file-lock isolation entirely (issue #52).
|
|
128
229
|
const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python|-go|-php|-yaml)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
|
|
129
|
-
&& !new RegExp(`(?:^|[\\\\/])
|
|
230
|
+
&& !new RegExp(`(?:^|[\\\\/])(?:${COPY_PREFIX}\\d+-|${CACHE_PREFIX})`).test(path));
|
|
130
231
|
if (preloaded.length) {
|
|
131
232
|
throw new Error(`tree-sitter native addon was loaded before Hunch could isolate it: ${preloaded.join(", ")}`);
|
|
132
233
|
}
|
|
133
234
|
removeStaleCopies();
|
|
134
|
-
|
|
235
|
+
// A refused cache (another account's or a group-writable dir squatting the
|
|
236
|
+
// predictable name) must not kill parsing: fall back to a private
|
|
237
|
+
// per-process copy dir, which removeStaleCopies prunes once the pid is gone.
|
|
238
|
+
let root;
|
|
239
|
+
let shared = true;
|
|
240
|
+
try {
|
|
241
|
+
root = cacheRoot();
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
root = mkdtempSync(join(tmpdir(), `${COPY_PREFIX}${process.pid}-`));
|
|
245
|
+
shared = false;
|
|
246
|
+
}
|
|
135
247
|
const previous = new Map();
|
|
248
|
+
const used = new Set();
|
|
136
249
|
try {
|
|
137
|
-
// Resolved INSIDE the try: if node-gyp-build cannot be resolved (a partial
|
|
138
|
-
// install, npm mid-swap) the catch below still removes the copy dir we just
|
|
139
|
-
// created. Outside it, every retry — and failure is deliberately not
|
|
140
|
-
// memoized, so a long-lived MCP server retries forever — leaked one empty
|
|
141
|
-
// hunch-tree-sitter-<pid>-* dir that removeStaleCopies can never prune,
|
|
142
|
-
// because it only prunes dirs whose pid is dead.
|
|
143
250
|
const nodeGypBuild = runtimeRequire("node-gyp-build");
|
|
144
251
|
for (const packageName of NATIVE_PACKAGES) {
|
|
145
252
|
const key = environmentKey(packageName);
|
|
146
253
|
previous.set(key, process.env[key]);
|
|
147
|
-
|
|
254
|
+
const packageCopy = copyNativeBinding(packageName, root, nodeGypBuild);
|
|
255
|
+
used.add(relative(root, packageCopy).split(sep)[0]);
|
|
256
|
+
process.env[key] = packageCopy;
|
|
148
257
|
}
|
|
149
258
|
const Parser = runtimeRequire("tree-sitter");
|
|
150
259
|
const languages = runtimeRequire("tree-sitter-typescript");
|
|
@@ -154,13 +263,6 @@ function loadRuntime() {
|
|
|
154
263
|
const yaml = runtimeRequire("@tree-sitter-grammars/tree-sitter-yaml");
|
|
155
264
|
runtime = { Parser, typescript: languages.typescript, tsx: languages.tsx, python, go, php: php.php, yaml };
|
|
156
265
|
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
try {
|
|
159
|
-
rmSync(copyRoot, { recursive: true, force: true });
|
|
160
|
-
}
|
|
161
|
-
catch { /* best effort */ }
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
266
|
finally {
|
|
165
267
|
for (const [key, value] of previous) {
|
|
166
268
|
if (value === undefined)
|
|
@@ -169,12 +271,8 @@ function loadRuntime() {
|
|
|
169
271
|
process.env[key] = value;
|
|
170
272
|
}
|
|
171
273
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
rmSync(copyRoot, { recursive: true, force: true });
|
|
175
|
-
}
|
|
176
|
-
catch { /* next process prunes it */ }
|
|
177
|
-
});
|
|
274
|
+
if (shared)
|
|
275
|
+
pruneCache(root, used);
|
|
178
276
|
return runtime;
|
|
179
277
|
}
|
|
180
278
|
//# sourceMappingURL=nativeTreeSitter.js.map
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { HunchStore } from "../store/hunchStore.js";
|
|
2
|
+
import { groundingTemplate } from "../core/groundingLag.js";
|
|
3
|
+
/** Version of the block's PROSE. Bump it whenever renderHunchSection's wording
|
|
4
|
+
* changes. A block without the stamp is template 1. */
|
|
5
|
+
export declare const GROUNDING_TEMPLATE = 2;
|
|
6
|
+
export { groundingTemplate };
|
|
7
|
+
/** Never downgrade a block's prose (fnd_f6875f475b). The post-commit capture runs
|
|
8
|
+
* the PINNED hunch, which can be older than the renderer that wrote the committed
|
|
9
|
+
* block (a branch that develops the renderer, or a repo whose pin lags its docs).
|
|
10
|
+
* When the existing block carries a newer template than `section`, keep its prose
|
|
11
|
+
* and move only the record counts, so the counts stay true without reverting text
|
|
12
|
+
* this version did not author. The preserved block keeps its wiki line and those
|
|
13
|
+
* Top-invariants lines whose constraint this version still renders.
|
|
14
|
+
* Returns the section to write. */
|
|
15
|
+
export declare function preserveNewerTemplate(existing: string, section: string): string;
|
|
2
16
|
/** Remove the managed HUNCH section (markers inclusive), leaving only the
|
|
3
17
|
* user-authored surroundings. Lets a caller decide whether two versions of a
|
|
4
18
|
* doc differ ONLY in generated content (the stranded-grounding heal,
|
|
@@ -8,6 +22,10 @@ export declare function renderHunchSection(store: HunchStore, root?: string): st
|
|
|
8
22
|
/** Insert/replace the marker-delimited HUNCH section in a markdown doc, preserving
|
|
9
23
|
* all user-authored content outside the markers. Shared by CLAUDE.md, AGENTS.md,
|
|
10
24
|
* and .github/copilot-instructions.md so every assistant gets the same grounding. */
|
|
11
|
-
export declare function upsertSection(file: string, section: string, fallbackTitle: string
|
|
25
|
+
export declare function upsertSection(file: string, section: string, fallbackTitle: string, opts?: {
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}): string;
|
|
12
28
|
/** Insert/replace the HUNCH section in CLAUDE.md, preserving everything else. */
|
|
13
|
-
export declare function updateClaudeMd(root: string, store: HunchStore
|
|
29
|
+
export declare function updateClaudeMd(root: string, store: HunchStore, opts?: {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}): string;
|
|
@@ -8,9 +8,56 @@ import { writeFileAtomic } from "../core/io.js";
|
|
|
8
8
|
import { basename, join, dirname } from "node:path";
|
|
9
9
|
import { wikiSummary } from "../wiki/wiki.js";
|
|
10
10
|
import { PolicyRepository } from "../constitution/repository.js";
|
|
11
|
-
import { renderCountsMatch } from "../core/groundingLag.js";
|
|
11
|
+
import { renderCountsMatch, parseGroundingCounts, groundingTemplate } from "../core/groundingLag.js";
|
|
12
12
|
const START = "<!-- HUNCH:START — auto-generated, do not edit by hand -->";
|
|
13
13
|
const END = "<!-- HUNCH:END -->";
|
|
14
|
+
/** Version of the block's PROSE. Bump it whenever renderHunchSection's wording
|
|
15
|
+
* changes. A block without the stamp is template 1. */
|
|
16
|
+
export const GROUNDING_TEMPLATE = 2;
|
|
17
|
+
/** The pre-edit hook injects each in-scope invariant in full; the always-loaded
|
|
18
|
+
* list only has to name it (fnd_a65f71f38f, #369). Blocking statements are never
|
|
19
|
+
* clipped; others are cut at a word boundary within this many chars. */
|
|
20
|
+
const INVARIANT_CHARS = 200;
|
|
21
|
+
export { groundingTemplate };
|
|
22
|
+
/** Never downgrade a block's prose (fnd_f6875f475b). The post-commit capture runs
|
|
23
|
+
* the PINNED hunch, which can be older than the renderer that wrote the committed
|
|
24
|
+
* block (a branch that develops the renderer, or a repo whose pin lags its docs).
|
|
25
|
+
* When the existing block carries a newer template than `section`, keep its prose
|
|
26
|
+
* and move only the record counts, so the counts stay true without reverting text
|
|
27
|
+
* this version did not author. The preserved block keeps its wiki line and those
|
|
28
|
+
* Top-invariants lines whose constraint this version still renders.
|
|
29
|
+
* Returns the section to write. */
|
|
30
|
+
export function preserveNewerTemplate(existing, section) {
|
|
31
|
+
const iStart = existing.indexOf(START);
|
|
32
|
+
const iEnd = existing.indexOf(END);
|
|
33
|
+
if (iStart < 0 || iEnd <= iStart)
|
|
34
|
+
return section;
|
|
35
|
+
const current = existing.slice(iStart, iEnd + END.length);
|
|
36
|
+
if (groundingTemplate(current) <= groundingTemplate(section))
|
|
37
|
+
return section;
|
|
38
|
+
// Never republish an invariant this version no longer renders: a constraint that
|
|
39
|
+
// was retired, deleted, or moved to the private overlay drops out of the kept list.
|
|
40
|
+
const rendered = new Set(section.split(/\r?\n/).map((line) => INVARIANT_LINE_RE.exec(line)?.[1]).filter(Boolean));
|
|
41
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
42
|
+
const lines = current.split(/\r?\n/).filter((line) => {
|
|
43
|
+
const id = INVARIANT_LINE_RE.exec(line)?.[1];
|
|
44
|
+
return !id || rendered.has(id);
|
|
45
|
+
});
|
|
46
|
+
// A heading left with no invariants under it goes too.
|
|
47
|
+
const h = lines.findIndex((line) => line.startsWith(INVARIANTS_HEADING));
|
|
48
|
+
if (h >= 0 && !lines.slice(h + 1).some((line) => INVARIANT_LINE_RE.test(line))) {
|
|
49
|
+
lines.splice(lines[h - 1] === "" ? h - 1 : h, lines[h - 1] === "" ? 2 : 1);
|
|
50
|
+
}
|
|
51
|
+
const kept = lines.join(eol);
|
|
52
|
+
const have = parseGroundingCounts(kept);
|
|
53
|
+
const next = parseGroundingCounts(section);
|
|
54
|
+
// Unreadable counts leave the counts sentence as written rather than downgrading
|
|
55
|
+
// the block (two versions would flip-flop); `hunch grounding` reports countsReadable: false.
|
|
56
|
+
return have && next ? kept.replace(have.match, next.match) : kept;
|
|
57
|
+
}
|
|
58
|
+
const INVARIANTS_HEADING = "### ⛔ Top invariants";
|
|
59
|
+
/** A Top-invariants line; group 1 is its constraint id. */
|
|
60
|
+
const INVARIANT_LINE_RE = /^- \*\*\[[a-z]+\]\*\* .*; (con_[A-Za-z0-9_]+)\)_\r?$/;
|
|
14
61
|
/** Remove the managed HUNCH section (markers inclusive), leaving only the
|
|
15
62
|
* user-authored surroundings. Lets a caller decide whether two versions of a
|
|
16
63
|
* doc differ ONLY in generated content (the stranded-grounding heal,
|
|
@@ -36,58 +83,27 @@ export function renderHunchSection(store, root) {
|
|
|
36
83
|
policies: root ? new PolicyRepository(root, store).listPolicies({ publicOnly: true }).length : 0,
|
|
37
84
|
findings: store.json.loadAll("findings").filter((f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled").length,
|
|
38
85
|
};
|
|
86
|
+
// Name the policy tools only where the MCP server registers them by default
|
|
87
|
+
// (src/mcp/toolset.ts: on once the repo holds a policy). Only committed evidence counts:
|
|
88
|
+
// env and the gitignored .hunch/config.json would make the committed block differ by
|
|
89
|
+
// machine. No root (a bare render) keeps the full list.
|
|
90
|
+
const policyTools = !root || counts.policies > 0;
|
|
39
91
|
const lines = [];
|
|
40
92
|
lines.push(START);
|
|
93
|
+
lines.push(`<!-- hunch:template ${GROUNDING_TEMPLATE} -->`);
|
|
41
94
|
lines.push("## 🧠 Hunch (Engineering Memory)");
|
|
42
95
|
lines.push("");
|
|
43
|
-
lines.push("This repo has **Hunch
|
|
44
|
-
|
|
45
|
-
`${renderCountsMatch(counts)}.`);
|
|
46
|
-
lines.push("");
|
|
47
|
-
lines.push("**Consult Hunch via the `hunch_*` MCP tools — pick by MOMENT, not from memory:**");
|
|
48
|
-
lines.push("");
|
|
49
|
-
lines.push("**Orient (session/task start):**");
|
|
50
|
-
lines.push("- If the host's prompt hook already opened the task and printed a task ID plus a `task verify` command, reuse that exact ID and command — do NOT call `hunch_task(action: \"start\")` for it. Otherwise start the task yourself: call `hunch_task(action: \"start\", title: <short task title>)` once and take `verification_argv` from its result. Each new prompt has its own ID; reuse the ID for follow-up work on the same task and never borrow another task's ID. This is task bookkeeping; `hunch_context` remains the first memory lookup. If reporting fails, continue the work and disclose the gap.");
|
|
51
|
-
lines.push("- When the user asks to **update Hunch**, run `hunch update` from this repository root. It updates to the latest release and repairs all configured harness pins. Use `hunch update --global` to also update a global CLI alongside a repository dependency; reconnect active MCP sessions afterward.");
|
|
52
|
-
lines.push("- `hunch_context(target, task_id)` — the minimal relevant slice for what you're about to do; a task phrase falls back to the closest graph matches. **Call FIRST** for memory. Include the current task ID on each context call so its contribution is inspectable.");
|
|
53
|
-
lines.push("- `hunch_structure(target?)` — the indexed shape of the repo/dir/file/symbol — orient from the graph, not grep rounds.");
|
|
54
|
-
lines.push("- `hunch_workspaces(view?)` — which worktrees and branches are open on which machine, what is merged and deletable (read-only; this machine live, others from memory). Call it instead of `git branch` / `git worktree list`; never delete on its say-so.");
|
|
55
|
-
lines.push("- `hunch_runbook(task)` — the proven steps for a recurring task, before re-deriving them.");
|
|
56
|
-
lines.push("- `hunch_escalations()` — the decisions only the HUMAN can make (including one exact imported ADR at a time, topic conflicts, and policy calls). Normally empty; when it isn't, ASK the user inline — an entry is a question, silence is never approval. Apply an ADR answer only through `hunch_review_imported_adr` with its printed source and review hashes.");
|
|
57
|
-
lines.push("- `hunch now` (CLI) — recent decisions + the live roadmap; `hunch log` — the memory-move timeline (every capture/adopt/supersede/prune/repair, each revertable).");
|
|
58
|
-
lines.push("");
|
|
59
|
-
lines.push("**Before designing / choosing an approach:**");
|
|
60
|
-
lines.push("- `hunch_why(target)` — why a file/symbol is shaped this way (decisions, bugs, constraints) — including what was already REJECTED.");
|
|
61
|
-
lines.push("- `hunch_current_decision(topic)` — the one live answer for a topic (history + rejected included).");
|
|
62
|
-
lines.push("- `hunch_bug_lineage(symptom_or_symbol)` — has this failed before? what was the root cause?");
|
|
63
|
-
lines.push("- `hunch_compare(candidates)` — rank candidate branches/commits by fewest invariant hits.");
|
|
64
|
-
lines.push("- `hunch_query(query)` — free-text search when nothing above fits.");
|
|
96
|
+
lines.push("This repo has **Hunch**, a graph of *why* the code is the way it is. It holds " +
|
|
97
|
+
`${renderCountsMatch(counts)}. Use the \`hunch_*\` MCP tools by moment:`);
|
|
65
98
|
lines.push("");
|
|
66
|
-
lines.push("**
|
|
67
|
-
lines.push("- `
|
|
68
|
-
lines.push("- `
|
|
69
|
-
lines.push(""
|
|
70
|
-
|
|
71
|
-
lines.push("- `
|
|
72
|
-
lines.push("-
|
|
73
|
-
lines.push("-
|
|
74
|
-
lines.push("");
|
|
75
|
-
lines.push("**Before the final response — make Hunch's contribution visible:**");
|
|
76
|
-
lines.push("- When running a relevant check, use the exact launcher the prompt hook printed — or, on a host without one, the `verification_argv` returned by hunch_task start — followed by the check command and its arguments, from this worktree. It runs `hunch task verify <task_id> -- <command> [arguments]` using the same installation as MCP, avoiding stale global binaries. This retains the actual exit result and source snapshot; raw output is not stored. Do not rerun an expensive check solely for reporting; missing evidence stays unverified.");
|
|
77
|
-
lines.push("- Include the current task_id when calling hunch_record_decision, hunch_record_correction, or hunch_record_finding. The save path records its actual memory home and verifies exact Git revisions when committing or pushing; never infer publication from a successful capture alone.");
|
|
78
|
-
lines.push("- Before claiming an application, call `hunch_report(task_id)` and copy the exact occurrence_id, record_id and content_hash from application_references, adding an action you actually took. Never derive an occurrence ID by replacing a receipt prefix or use the task's scope hash as a record hash. If you did not apply a lesson, omit applications.");
|
|
79
|
-
lines.push("- When this task actually used Hunch (a `hunch_*` call carrying the task_id, a verified check, Hunch hook context you acted on, or an application to claim), call `hunch_task(action: \"finish\", task_id, applications?)` and include the returned contribution_card in your final response without the user asking. Skip the finish call only when it used none of those AND the host's own stop hook closes the task and shows the evidence for you (its prompt-hook instruction says so); where no host hook closes the task, and for a task you started yourself with `hunch_task(action: \"start\")`, always finish it yourself. Copy the card verbatim, including its Evidence line (the command that renders the local report on demand) and the agent-reported label; the structured result contains the card even when the host hides text blocks. Do not replace it with a generic claim that Hunch helped. If presentation_enabled is false, omit the card. A delivered lesson or passing command alone does not prove causal impact.");
|
|
80
|
-
lines.push("- If interrupted, finish with `outcome: \"interrupted\"` when possible. `hunch_report(task_id, html: true)` opens the evidence trail by generating a local file; it may contain private memory and is not a public export. If report tools are unavailable after an update, say so and reconnect the host rather than inventing a report.");
|
|
81
|
-
lines.push("");
|
|
82
|
-
lines.push("**Build the Constitution review queue:**");
|
|
83
|
-
lines.push("- `hunch constitution bootstrap --since 90d --max-candidates 3` (CLI) — normalize recent structured human evidence into at most three non-active policy candidates; add `--history` for exact, human-identifier-grounded fix/revert deltas or explicit dependency retirements. Coincidence/ambiguity stays uncompilable; neither path grants authority.");
|
|
84
|
-
lines.push("- `hunch constitution ingest --since 90d [--instructions] [--from export.json]` (CLI) — normalize corrections/failures plus bounded committed instructions/ADRs and strict local review/conversation/PR exports into Git-native evidence; raw prose is hash-only, unsupported intent remains uncompilable, and no policy is minted.");
|
|
85
|
-
lines.push("");
|
|
86
|
-
lines.push("**After deciding / when corrected:**");
|
|
87
|
-
lines.push("- `hunch_capture_decision(topic?)` → `hunch_record_decision(...)` — interview first, then write; status `proposed` = roadmap intent (shows in `hunch now`).");
|
|
88
|
-
lines.push("- `hunch_record_correction(...)` — a human correction becomes an ENFORCED rule (Never Twice), not a one-session memory.");
|
|
89
|
-
lines.push("- `hunch_record_finding(...)` — an OBSERVATION with no code change (an audit that found a gap, a measured number, an incident) becomes durable memory anchored to a date + evidence; `/audit` runs the ritual.");
|
|
90
|
-
lines.push("- `hunch_timeline(target)` — decision history when investigating how something evolved.");
|
|
99
|
+
lines.push("- **Start:** reuse the task ID and `task verify` command the prompt hook printed; with none, call `hunch_task(action: \"start\", title)` once. Then `hunch_context(target, task_id)` first. Orient with `hunch_structure`, `hunch_workspaces`, `hunch_runbook(task)`. Ask the user about each `hunch_escalations()` entry; silence is never approval.");
|
|
100
|
+
lines.push("- **Design:** `hunch_why(target)` (includes what was rejected), `hunch_current_decision(topic)`, `hunch_bug_lineage(symptom_or_symbol)`, `hunch_compare(candidates)`, `hunch_query(query)`.");
|
|
101
|
+
lines.push("- **Edit:** `hunch_check_constraints(scope)`, `hunch_get_dependents(symbol)` / `hunch_blast_radius(target)`, `hunch_findings(scope?)`.");
|
|
102
|
+
lines.push("- **Merge:** `hunch_conformance()`, `hunch_pr_impact(base?)`, `hunch_merge_verdict`." +
|
|
103
|
+
(policyTools ? " Policy review: `hunch_policy_evaluate`, `hunch_policy_plan(policy_id)`, `hunch_policy_card(policy_id)`, `hunch_policy_proof`; only a human activates a policy." : ""));
|
|
104
|
+
lines.push("- **Record:** `hunch_capture_decision` → `hunch_record_decision`; `hunch_record_correction` turns a human correction into an enforced rule; `hunch_record_finding` keeps an observation with evidence. Pass the task_id.");
|
|
105
|
+
lines.push("- **Finish:** run checks through the `task verify` launcher. If the task used Hunch, you started it, or no host stop hook closes it, call `hunch_task(action: \"finish\", task_id)` and show its card verbatim. Its `applications` schema carries the claim rules.");
|
|
106
|
+
lines.push("- To update Hunch, run `hunch update` from the repo root.");
|
|
91
107
|
const wiki = root ? wikiSummary(root) : null;
|
|
92
108
|
if (wiki) {
|
|
93
109
|
lines.push("");
|
|
@@ -97,19 +113,21 @@ export function renderHunchSection(store, root) {
|
|
|
97
113
|
lines.push("");
|
|
98
114
|
lines.push("### ⛔ Top invariants (do not break)");
|
|
99
115
|
for (const c of constraints) {
|
|
100
|
-
lines.push(`- **[${c.severity}]** ${c.statement} _(scope: ${c.scope.join(", ") || "repo"}; ${c.id})_`);
|
|
116
|
+
lines.push(`- **[${c.severity}]** ${c.severity === "blocking" ? c.statement : clip(c.statement, INVARIANT_CHARS)} _(scope: ${c.scope.join(", ") || "repo"}; ${c.id})_`);
|
|
101
117
|
}
|
|
102
118
|
}
|
|
103
119
|
lines.push("");
|
|
104
|
-
lines.push("
|
|
120
|
+
lines.push("_Records carry provenance and confidence; treat low-confidence items as advisory._");
|
|
105
121
|
lines.push(END);
|
|
106
122
|
return lines.join("\n");
|
|
107
123
|
}
|
|
108
124
|
/** Insert/replace the marker-delimited HUNCH section in a markdown doc, preserving
|
|
109
125
|
* all user-authored content outside the markers. Shared by CLAUDE.md, AGENTS.md,
|
|
110
126
|
* and .github/copilot-instructions.md so every assistant gets the same grounding. */
|
|
111
|
-
export function upsertSection(file, section, fallbackTitle) {
|
|
127
|
+
export function upsertSection(file, section, fallbackTitle, opts) {
|
|
112
128
|
let content = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
129
|
+
if (!opts?.force)
|
|
130
|
+
section = preserveNewerTemplate(content, section);
|
|
113
131
|
const iStart = content.indexOf(START);
|
|
114
132
|
const iEnd = content.indexOf(END);
|
|
115
133
|
if (iStart >= 0 && iEnd > iStart) {
|
|
@@ -133,8 +151,16 @@ export function upsertSection(file, section, fallbackTitle) {
|
|
|
133
151
|
return file;
|
|
134
152
|
}
|
|
135
153
|
/** Insert/replace the HUNCH section in CLAUDE.md, preserving everything else. */
|
|
136
|
-
export function updateClaudeMd(root, store) {
|
|
137
|
-
return upsertSection(join(root, "CLAUDE.md"), renderHunchSection(store, root), `# ${basename(root)}
|
|
154
|
+
export function updateClaudeMd(root, store, opts) {
|
|
155
|
+
return upsertSection(join(root, "CLAUDE.md"), renderHunchSection(store, root), `# ${basename(root)}`, opts);
|
|
156
|
+
}
|
|
157
|
+
/** Cut at the last whitespace at or before `max - 1` chars, so a word is never split. */
|
|
158
|
+
function clip(text, max) {
|
|
159
|
+
if (text.length <= max)
|
|
160
|
+
return text;
|
|
161
|
+
const head = text.slice(0, max);
|
|
162
|
+
const cut = head.search(/\s\S*$/);
|
|
163
|
+
return `${(cut > 0 ? text.slice(0, cut) : text.slice(0, max - 1)).trimEnd()}…`;
|
|
138
164
|
}
|
|
139
165
|
function sev(s) {
|
|
140
166
|
return { blocking: 3, warning: 2, advisory: 1 }[s] ?? 0;
|
|
@@ -26,12 +26,16 @@ export declare function writeAntigravityWorkspaceMcp(root: string, inv: Invocati
|
|
|
26
26
|
export declare function writeCodexConfig(root: string, inv: Invocation): string;
|
|
27
27
|
/** AGENTS.md — the cross-tool ambient-instruction standard (Codex and a growing
|
|
28
28
|
* set of assistants read it). Marker-delimited so user prose is preserved. */
|
|
29
|
-
export declare function writeAgentsMd(root: string, store: HunchStore): string;
|
|
29
|
+
export declare function writeAgentsMd(root: string, store: HunchStore, opts?: GroundingWriteOptions): string;
|
|
30
30
|
/** GitHub Copilot custom instructions (VS Code / github.com). Same grounding. */
|
|
31
|
-
export declare function writeCopilotInstructions(root: string, store: HunchStore): string;
|
|
31
|
+
export declare function writeCopilotInstructions(root: string, store: HunchStore, opts?: GroundingWriteOptions): string;
|
|
32
|
+
/** `force` re-renders even a block written by a newer Hunch template. */
|
|
33
|
+
export interface GroundingWriteOptions {
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}
|
|
32
36
|
/** Cursor project rule (.mdc = frontmatter + body). `alwaysApply` keeps the Hunch
|
|
33
37
|
* grounding in context for every request. Fully managed by Hunch (overwritten). */
|
|
34
|
-
export declare function writeCursorRule(root: string, store: HunchStore): string;
|
|
38
|
+
export declare function writeCursorRule(root: string, store: HunchStore, opts?: GroundingWriteOptions): string;
|
|
35
39
|
/** Windsurf (Cascade): .windsurf/mcp_config.json — same `mcpServers` shape as
|
|
36
40
|
* Cursor. Repo-local (committed, shared via git) to match Hunch's other configs,
|
|
37
41
|
* rather than the global ~/.codeium/windsurf path. Merges; refuses to clobber. */
|
|
@@ -43,7 +47,7 @@ export declare function windsurfMcpFile(home?: string): string | null;
|
|
|
43
47
|
export declare function writeWindsurfGlobalMcp(inv: Invocation, home?: string): string | null;
|
|
44
48
|
/** Windsurf project rule (.windsurf/rules/hunch.md). `trigger: always_on` keeps the
|
|
45
49
|
* Hunch grounding in Cascade's context for every request. Fully managed (overwritten). */
|
|
46
|
-
export declare function writeWindsurfRule(root: string, store: HunchStore): string;
|
|
50
|
+
export declare function writeWindsurfRule(root: string, store: HunchStore, opts?: GroundingWriteOptions): string;
|
|
47
51
|
/** Cursor's hook API is beta, but its project-level config accepts this standard
|
|
48
52
|
* event map. Context delivery is opportunistic; the always-on rule and MCP
|
|
49
53
|
* registration remain the durable grounding path if a Cursor build suppresses
|
|
@@ -82,7 +86,7 @@ export declare const GROUNDING_DOC_PATHS: readonly string[];
|
|
|
82
86
|
* assistant). Run by `hunch index` and non-hook `hunch sync` so a project silently
|
|
83
87
|
* picks up generator fixes (e.g. corrected MCP tool param names) and fresh record
|
|
84
88
|
* counts on the next refresh — no manual `hunch init`. */
|
|
85
|
-
export declare function refreshExistingGrounding(root: string, store: HunchStore): string[];
|
|
89
|
+
export declare function refreshExistingGrounding(root: string, store: HunchStore, opts?: GroundingWriteOptions): string[];
|
|
86
90
|
/** Capture-commit refresh: rewrite grounding docs that are git-clean OR whose only
|
|
87
91
|
* divergence from HEAD is generated content, and return the absolute paths to fold
|
|
88
92
|
* into the memory commit (commitAndPushHunch alsoStage). This keeps committed record
|
|
@@ -22,7 +22,7 @@ import { writeFileAtomic } from "../core/io.js";
|
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
24
|
import { isHunchHookCommand } from "./hookmatch.js";
|
|
25
|
-
import { renderHunchSection, stripManagedSection, upsertSection, updateClaudeMd } from "./claudemd.js";
|
|
25
|
+
import { renderHunchSection, stripManagedSection, upsertSection, updateClaudeMd, preserveNewerTemplate } from "./claudemd.js";
|
|
26
26
|
import { headFileContent, isGitCleanPath } from "../extractors/git.js";
|
|
27
27
|
import { parseJsonc } from "../core/jsonc.js";
|
|
28
28
|
import { parse as parseToml } from "smol-toml";
|
|
@@ -271,18 +271,26 @@ export function writeCodexConfig(root, inv) {
|
|
|
271
271
|
}
|
|
272
272
|
/** AGENTS.md — the cross-tool ambient-instruction standard (Codex and a growing
|
|
273
273
|
* set of assistants read it). Marker-delimited so user prose is preserved. */
|
|
274
|
-
export function writeAgentsMd(root, store) {
|
|
275
|
-
return upsertSection(join(root, "AGENTS.md"), renderHunchSection(store, root), "# AGENTS.md");
|
|
274
|
+
export function writeAgentsMd(root, store, opts) {
|
|
275
|
+
return upsertSection(join(root, "AGENTS.md"), renderHunchSection(store, root), "# AGENTS.md", opts);
|
|
276
276
|
}
|
|
277
277
|
/** GitHub Copilot custom instructions (VS Code / github.com). Same grounding. */
|
|
278
|
-
export function writeCopilotInstructions(root, store) {
|
|
279
|
-
return upsertSection(join(root, ".github", "copilot-instructions.md"), renderHunchSection(store, root), "# Copilot instructions");
|
|
278
|
+
export function writeCopilotInstructions(root, store, opts) {
|
|
279
|
+
return upsertSection(join(root, ".github", "copilot-instructions.md"), renderHunchSection(store, root), "# Copilot instructions", opts);
|
|
280
|
+
}
|
|
281
|
+
/** The block for a wholly-owned rule file, never downgrading newer prose (unless forced). */
|
|
282
|
+
function ownedSection(file, store, root, opts) {
|
|
283
|
+
const section = renderHunchSection(store, root);
|
|
284
|
+
if (opts?.force)
|
|
285
|
+
return section;
|
|
286
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
287
|
+
return preserveNewerTemplate(existing, section);
|
|
280
288
|
}
|
|
281
289
|
/** Cursor project rule (.mdc = frontmatter + body). `alwaysApply` keeps the Hunch
|
|
282
290
|
* grounding in context for every request. Fully managed by Hunch (overwritten). */
|
|
283
|
-
export function writeCursorRule(root, store) {
|
|
291
|
+
export function writeCursorRule(root, store, opts) {
|
|
284
292
|
const file = join(root, ".cursor", "rules", "hunch.mdc");
|
|
285
|
-
const body = `---\ndescription: Hunch engineering memory — consult the hunch_* MCP tools before editing\nalwaysApply: true\n---\n\n${
|
|
293
|
+
const body = `---\ndescription: Hunch engineering memory — consult the hunch_* MCP tools before editing\nalwaysApply: true\n---\n\n${ownedSection(file, store, root, opts)}\n`;
|
|
286
294
|
mkdirSync(dirname(file), { recursive: true });
|
|
287
295
|
writeFileAtomic(file, body);
|
|
288
296
|
return file;
|
|
@@ -315,9 +323,9 @@ export function writeWindsurfGlobalMcp(inv, home = homedir()) {
|
|
|
315
323
|
}
|
|
316
324
|
/** Windsurf project rule (.windsurf/rules/hunch.md). `trigger: always_on` keeps the
|
|
317
325
|
* Hunch grounding in Cascade's context for every request. Fully managed (overwritten). */
|
|
318
|
-
export function writeWindsurfRule(root, store) {
|
|
326
|
+
export function writeWindsurfRule(root, store, opts) {
|
|
319
327
|
const file = join(root, ".windsurf", "rules", "hunch.md");
|
|
320
|
-
const body = `---\ntrigger: always_on\ndescription: Hunch engineering memory — consult the hunch_* MCP tools before editing\n---\n\n${
|
|
328
|
+
const body = `---\ntrigger: always_on\ndescription: Hunch engineering memory — consult the hunch_* MCP tools before editing\n---\n\n${ownedSection(file, store, root, opts)}\n`;
|
|
321
329
|
mkdirSync(dirname(file), { recursive: true });
|
|
322
330
|
writeFileAtomic(file, body);
|
|
323
331
|
return file;
|
|
@@ -356,7 +364,8 @@ export function writeCodexHooks(root, inv) {
|
|
|
356
364
|
return writeHookConfig(file, {
|
|
357
365
|
SessionStart: [entry()],
|
|
358
366
|
UserPromptSubmit: [entry()],
|
|
359
|
-
|
|
367
|
+
// The shell entries take the baseline a shell write is measured from.
|
|
368
|
+
PreToolUse: [entry("apply_patch|Bash|PowerShell|shell|local_shell")],
|
|
360
369
|
// Codex's native command tool arrives as `Bash` (or `PowerShell` on
|
|
361
370
|
// Windows), while older hosts may expose shell/local_shell names.
|
|
362
371
|
PostToolUse: [entry("apply_patch|Bash|PowerShell|shell|local_shell")],
|
|
@@ -435,12 +444,16 @@ export function writeAntigravityHooks(root, inv) {
|
|
|
435
444
|
* docs reflect that no engineering memory is published here (renderHunchSection
|
|
436
445
|
* reads the public store only, so private records never leak into them). */
|
|
437
446
|
export function regenerateGrounding(root, store) {
|
|
447
|
+
// FORCED: a block from a newer template would otherwise keep its prose — including
|
|
448
|
+
// its Top-invariants list — so a constraint that just moved to the overlay would stay
|
|
449
|
+
// printed in the committed public doc.
|
|
450
|
+
const force = { force: true };
|
|
438
451
|
return [
|
|
439
|
-
updateClaudeMd(root, store),
|
|
440
|
-
writeAgentsMd(root, store),
|
|
441
|
-
writeCopilotInstructions(root, store),
|
|
442
|
-
writeCursorRule(root, store),
|
|
443
|
-
writeWindsurfRule(root, store),
|
|
452
|
+
updateClaudeMd(root, store, force),
|
|
453
|
+
writeAgentsMd(root, store, force),
|
|
454
|
+
writeCopilotInstructions(root, store, force),
|
|
455
|
+
writeCursorRule(root, store, force),
|
|
456
|
+
writeWindsurfRule(root, store, force),
|
|
444
457
|
];
|
|
445
458
|
}
|
|
446
459
|
/** The five grounding docs, repo-relative (POSIX separators, as git prints them). */
|
|
@@ -451,13 +464,13 @@ export const GROUNDING_DOC_PATHS = Object.freeze([
|
|
|
451
464
|
".cursor/rules/hunch.mdc",
|
|
452
465
|
".windsurf/rules/hunch.md",
|
|
453
466
|
]);
|
|
454
|
-
function groundingTargets(root, store) {
|
|
467
|
+
function groundingTargets(root, store, opts) {
|
|
455
468
|
return [
|
|
456
|
-
["CLAUDE.md", () => updateClaudeMd(root, store)],
|
|
457
|
-
["AGENTS.md", () => writeAgentsMd(root, store)],
|
|
458
|
-
[join(".github", "copilot-instructions.md"), () => writeCopilotInstructions(root, store)],
|
|
459
|
-
[join(".cursor", "rules", "hunch.mdc"), () => writeCursorRule(root, store)],
|
|
460
|
-
[join(".windsurf", "rules", "hunch.md"), () => writeWindsurfRule(root, store)],
|
|
469
|
+
["CLAUDE.md", () => updateClaudeMd(root, store, opts)],
|
|
470
|
+
["AGENTS.md", () => writeAgentsMd(root, store, opts)],
|
|
471
|
+
[join(".github", "copilot-instructions.md"), () => writeCopilotInstructions(root, store, opts)],
|
|
472
|
+
[join(".cursor", "rules", "hunch.mdc"), () => writeCursorRule(root, store, opts)],
|
|
473
|
+
[join(".windsurf", "rules", "hunch.md"), () => writeWindsurfRule(root, store, opts)],
|
|
461
474
|
];
|
|
462
475
|
}
|
|
463
476
|
/** Self-heal: refresh the Hunch section in each grounding doc that ALREADY exists,
|
|
@@ -466,9 +479,9 @@ function groundingTargets(root, store) {
|
|
|
466
479
|
* assistant). Run by `hunch index` and non-hook `hunch sync` so a project silently
|
|
467
480
|
* picks up generator fixes (e.g. corrected MCP tool param names) and fresh record
|
|
468
481
|
* counts on the next refresh — no manual `hunch init`. */
|
|
469
|
-
export function refreshExistingGrounding(root, store) {
|
|
482
|
+
export function refreshExistingGrounding(root, store, opts) {
|
|
470
483
|
const changed = [];
|
|
471
|
-
for (const [rel, write] of groundingTargets(root, store)) {
|
|
484
|
+
for (const [rel, write] of groundingTargets(root, store, opts)) {
|
|
472
485
|
const file = join(root, rel);
|
|
473
486
|
if (!existsSync(file))
|
|
474
487
|
continue; // refresh-only: never scaffold a doc the project doesn't have
|
|
@@ -180,9 +180,10 @@ export function installClaudeHooks(root, hookCmd) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
const keep = (arr) => (Array.isArray(arr) ? arr.map((entry) => withoutHunchCommands(entry, hookCmd)).filter((e) => e !== null) : []);
|
|
183
|
+
// Shell tools too: their PreToolUse takes the baseline a shell write is measured from.
|
|
183
184
|
json.hooks.PreToolUse = [
|
|
184
185
|
...keep(json.hooks.PreToolUse),
|
|
185
|
-
{ matcher: "Edit|Write|MultiEdit", hooks: [{ type: "command", command: hookCmd }] },
|
|
186
|
+
{ matcher: "Edit|Write|MultiEdit|Bash|PowerShell", hooks: [{ type: "command", command: hookCmd }] },
|
|
186
187
|
];
|
|
187
188
|
json.hooks.UserPromptSubmit = [
|
|
188
189
|
...keep(json.hooks.UserPromptSubmit),
|