@davesheffer/hunch 1.41.6 → 1.42.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 +1 -1
- package/dist/cli/index.js +402 -154
- package/dist/constitution/experiment.d.ts +3 -3
- package/dist/constitution/g3.d.ts +1 -1
- 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/pipeline.d.ts +28 -0
- package/dist/core/pipeline.js +50 -0
- 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 +19 -3
- 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 +34 -22
- 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
|
@@ -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;
|
|
@@ -435,12 +443,16 @@ export function writeAntigravityHooks(root, inv) {
|
|
|
435
443
|
* docs reflect that no engineering memory is published here (renderHunchSection
|
|
436
444
|
* reads the public store only, so private records never leak into them). */
|
|
437
445
|
export function regenerateGrounding(root, store) {
|
|
446
|
+
// FORCED: a block from a newer template would otherwise keep its prose — including
|
|
447
|
+
// its Top-invariants list — so a constraint that just moved to the overlay would stay
|
|
448
|
+
// printed in the committed public doc.
|
|
449
|
+
const force = { force: true };
|
|
438
450
|
return [
|
|
439
|
-
updateClaudeMd(root, store),
|
|
440
|
-
writeAgentsMd(root, store),
|
|
441
|
-
writeCopilotInstructions(root, store),
|
|
442
|
-
writeCursorRule(root, store),
|
|
443
|
-
writeWindsurfRule(root, store),
|
|
451
|
+
updateClaudeMd(root, store, force),
|
|
452
|
+
writeAgentsMd(root, store, force),
|
|
453
|
+
writeCopilotInstructions(root, store, force),
|
|
454
|
+
writeCursorRule(root, store, force),
|
|
455
|
+
writeWindsurfRule(root, store, force),
|
|
444
456
|
];
|
|
445
457
|
}
|
|
446
458
|
/** The five grounding docs, repo-relative (POSIX separators, as git prints them). */
|
|
@@ -451,13 +463,13 @@ export const GROUNDING_DOC_PATHS = Object.freeze([
|
|
|
451
463
|
".cursor/rules/hunch.mdc",
|
|
452
464
|
".windsurf/rules/hunch.md",
|
|
453
465
|
]);
|
|
454
|
-
function groundingTargets(root, store) {
|
|
466
|
+
function groundingTargets(root, store, opts) {
|
|
455
467
|
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)],
|
|
468
|
+
["CLAUDE.md", () => updateClaudeMd(root, store, opts)],
|
|
469
|
+
["AGENTS.md", () => writeAgentsMd(root, store, opts)],
|
|
470
|
+
[join(".github", "copilot-instructions.md"), () => writeCopilotInstructions(root, store, opts)],
|
|
471
|
+
[join(".cursor", "rules", "hunch.mdc"), () => writeCursorRule(root, store, opts)],
|
|
472
|
+
[join(".windsurf", "rules", "hunch.md"), () => writeWindsurfRule(root, store, opts)],
|
|
461
473
|
];
|
|
462
474
|
}
|
|
463
475
|
/** Self-heal: refresh the Hunch section in each grounding doc that ALREADY exists,
|
|
@@ -466,9 +478,9 @@ function groundingTargets(root, store) {
|
|
|
466
478
|
* assistant). Run by `hunch index` and non-hook `hunch sync` so a project silently
|
|
467
479
|
* picks up generator fixes (e.g. corrected MCP tool param names) and fresh record
|
|
468
480
|
* counts on the next refresh — no manual `hunch init`. */
|
|
469
|
-
export function refreshExistingGrounding(root, store) {
|
|
481
|
+
export function refreshExistingGrounding(root, store, opts) {
|
|
470
482
|
const changed = [];
|
|
471
|
-
for (const [rel, write] of groundingTargets(root, store)) {
|
|
483
|
+
for (const [rel, write] of groundingTargets(root, store, opts)) {
|
|
472
484
|
const file = join(root, rel);
|
|
473
485
|
if (!existsSync(file))
|
|
474
486
|
continue; // refresh-only: never scaffold a doc the project doesn't have
|
|
@@ -9,8 +9,8 @@ export declare const DEFAULT_TEAM_REF = "refs/heads/main";
|
|
|
9
9
|
export declare function safeTeamRef(value: string): string | null;
|
|
10
10
|
export declare function teamSharedRef(team: TeamConfig): string;
|
|
11
11
|
/** SECURITY GATE for team.json's URL. team.json is COMMITTED — in a freshly cloned
|
|
12
|
-
* (possibly untrusted) repo it is attacker-controlled, and
|
|
13
|
-
* it
|
|
12
|
+
* (possibly untrusted) repo it is attacker-controlled, and every consumer parses and
|
|
13
|
+
* compares it before the user's trust is checked. Without this gate a value like `--upload-pack=…` (argument
|
|
14
14
|
* smuggling) or `ext::sh -c …` (git's ext transport) is remote code execution from
|
|
15
15
|
* merely opening a repo. Allow only credential-free https://, ssh://, git://,
|
|
16
16
|
* scp-style git@host:path, and never anything that could parse as a Git flag. */
|
|
@@ -52,6 +52,10 @@ export type ValidatedTeamClone = {
|
|
|
52
52
|
sharedRef: string;
|
|
53
53
|
empty: boolean;
|
|
54
54
|
};
|
|
55
|
+
/** HUNCH_TEAM_CLONE_TIMEOUT_MS: a per-process override for every team-store clone
|
|
56
|
+
* and materialization timeout (default: the caller's own bound). For slow disks,
|
|
57
|
+
* networks, or heavily parallel CI; ignored unless a positive integer. */
|
|
58
|
+
export declare function teamCloneTimeoutOverride(): number | undefined;
|
|
55
59
|
/** Clone a shared memory repository without checking out attacker-controlled
|
|
56
60
|
* paths, validate its exact route/OID/tree/attributes, and only then publish the
|
|
57
61
|
* fully materialized clone at `destination`. Failure removes both quarantine and
|
|
@@ -60,11 +64,27 @@ export declare function cloneValidatedTeamOverlay(sharedRepo: string, sharedRepo
|
|
|
60
64
|
sharedRef?: string;
|
|
61
65
|
timeoutMs?: number;
|
|
62
66
|
}): ValidatedTeamClone | null;
|
|
67
|
+
export declare function teamTrustFile(): string;
|
|
68
|
+
export declare function isTeamStoreTrusted(root: string, team: TeamConfig): boolean;
|
|
69
|
+
/** Record this user's explicit consent to wire `root` to `team.shared_repo`. Returns an
|
|
70
|
+
* undo that removes only THIS checkout's key (restoring its prior entry if one existed),
|
|
71
|
+
* never the whole file byte-for-byte: another checkout's concurrent trust write must
|
|
72
|
+
* survive the undo. The file is deleted only if it ends up empty and did not exist
|
|
73
|
+
* before, so a caller can make consent conditional on a later step. */
|
|
74
|
+
export declare function trustTeamStore(root: string, team: TeamConfig): () => void;
|
|
75
|
+
/** Whether this user consented to `root` using `privateDir` as the advertised team
|
|
76
|
+
* store. The per-worktree `.hunch/local.json` alone proves nothing: a repository can
|
|
77
|
+
* ship one that points at another checkout's store. Consent is an explicit trust
|
|
78
|
+
* entry, or the git-common-dir pointer that only this machine's own setup writes
|
|
79
|
+
* (a git clone never delivers the source's `.git`; an archive that ships its own `.git`
|
|
80
|
+
* already ships hooks that run on the next git command), naming the same store. */
|
|
81
|
+
export declare function teamWiringConsented(root: string, team: TeamConfig, privateDir: string): boolean;
|
|
82
|
+
export declare function untrustedTeamStoreMessage(team: TeamConfig): string;
|
|
63
83
|
/** Auto-wire this checkout to the team's shared store advertised in `.hunch/team.json`:
|
|
64
84
|
* clone it to the worktree-stable anchor, and register the gitignored local pointer +
|
|
65
85
|
* the git-common-dir pointer (mode "shared", auto-commit on) so every consumer — CLI,
|
|
66
86
|
* MCP server, hooks, all worktrees — resolves the same single source of truth.
|
|
67
|
-
* No-op (null) when an overlay is already configured, there's no team.json,
|
|
68
|
-
* clone fails (best-effort: never throws, never blocks startup). Returns the overlay
|
|
87
|
+
* No-op (null) when an overlay is already configured, there's no team.json, this user
|
|
88
|
+
* has not trusted its URL (see trustTeamStore), or the clone fails (best-effort: never throws, never blocks startup). Returns the overlay
|
|
69
89
|
* hunch dir when wired. */
|
|
70
90
|
export declare function ensureTeamOverlay(root: string): string | null;
|