@davesheffer/hunch 1.40.0 → 1.41.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/dist/cli/index.js +60 -6
- package/dist/core/paths.d.ts +9 -1
- package/dist/core/paths.js +9 -1
- package/dist/core/taskQuery.d.ts +2 -0
- package/dist/core/taskQuery.js +2 -0
- package/dist/core/taskReportEvidence.js +3 -1
- package/dist/core/taskReportRender.d.ts +3 -0
- package/dist/core/taskReportRender.js +31 -13
- package/dist/extractors/git.d.ts +37 -0
- package/dist/extractors/git.js +56 -1
- package/dist/extractors/indexer.d.ts +5 -0
- package/dist/extractors/indexer.js +17 -2
- package/dist/extractors/parse.d.ts +3 -1
- package/dist/extractors/parse.js +6 -2
- package/dist/integrations/hooks.js +60 -7
- package/dist/integrations/team.js +11 -1
- package/dist/mcp/server.d.ts +125 -0
- package/dist/mcp/server.js +455 -3
- package/dist/store/hunchStore.d.ts +8 -0
- package/dist/store/hunchStore.js +14 -1
- package/dist/store/stateBinding.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -19,15 +19,25 @@ import { installMergeDriver } from "./mergeDriver.js";
|
|
|
19
19
|
import { ensureGitignore } from "./gitignore.js";
|
|
20
20
|
import { resolveInvocation } from "../cli/invocation.js";
|
|
21
21
|
export const DEFAULT_TEAM_REF = "refs/heads/main";
|
|
22
|
+
// check-ref-format without --branch checks syntax only, independent of the
|
|
23
|
+
// repository, ref existence, or remote state. Retain just its last successful
|
|
24
|
+
// input: one shared sync can otherwise spawn Git dozens of times for the same
|
|
25
|
+
// name. Route/remote validation still runs afresh, and failed invocations retry.
|
|
26
|
+
let lastValidTeamRef;
|
|
22
27
|
export function safeTeamRef(value) {
|
|
23
28
|
const ref = value.trim();
|
|
24
29
|
if (!ref.startsWith("refs/heads/") || ref === "refs/heads/")
|
|
25
30
|
return null;
|
|
31
|
+
if (ref === lastValidTeamRef)
|
|
32
|
+
return ref;
|
|
26
33
|
const checked = spawnSync("git", ["check-ref-format", ref], {
|
|
27
34
|
stdio: "ignore",
|
|
28
35
|
env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1" },
|
|
29
36
|
});
|
|
30
|
-
|
|
37
|
+
if (checked.status !== 0)
|
|
38
|
+
return null;
|
|
39
|
+
lastValidTeamRef = ref;
|
|
40
|
+
return ref;
|
|
31
41
|
}
|
|
32
42
|
export function teamSharedRef(team) {
|
|
33
43
|
return team.shared_ref ?? DEFAULT_TEAM_REF;
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -8,8 +8,133 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { HunchStore } from "../store/hunchStore.js";
|
|
11
|
+
import { type StateFacet } from "../core/stateContract.js";
|
|
11
12
|
import type { Symbol } from "../core/types.js";
|
|
12
13
|
export declare const publicationVocabulary: (hunchDir: string) => RegExp[];
|
|
14
|
+
/** The cwd-hint fallback (see `cwdHintField` above) only helps when the CALLER
|
|
15
|
+
* remembers to pass it — a subagent spawned straight into its own linked
|
|
16
|
+
* worktree has no reason to think of the session's "starting directory" as
|
|
17
|
+
* different from its own, so the hint is silently omitted and an auto-committing
|
|
18
|
+
* write lands wherever `root` last was (often the primary checkout, on its
|
|
19
|
+
* default branch). This is a backstop, NOT a complete fix: it only catches a
|
|
20
|
+
* related file that's new/untracked at the resolved root but already exists in
|
|
21
|
+
* a sibling worktree — the common "edited an existing tracked file" case is
|
|
22
|
+
* invisible to a pure existence check (the file exists at every worktree, just
|
|
23
|
+
* with different content).
|
|
24
|
+
*
|
|
25
|
+
* Used by `misrouteGuard` below to cover every auto-committing write tool that
|
|
26
|
+
* names files: hunch_record_decision's related_files, hunch_record_correction's
|
|
27
|
+
* scope_hint_file, hunch_record_finding's affected_files, and nuryel_write's
|
|
28
|
+
* decisions/findings/bugs/constraints facets via `fileEvidenceFor`. Every
|
|
29
|
+
* matching sibling is named as a candidate `cwd` and the write is refused
|
|
30
|
+
* rather than risked; it returns every match rather than the first, since
|
|
31
|
+
* confidently naming just one would let a caller that blindly retries as
|
|
32
|
+
* instructed land in the WRONG worktree — the same failure mode one level
|
|
33
|
+
* removed.
|
|
34
|
+
*
|
|
35
|
+
* Two false-positive guards, both required, not optional hardening:
|
|
36
|
+
* - Empty related_files, or no sibling worktree doing any better, is silent — a
|
|
37
|
+
* decision that legitimately precedes its code (no files yet) is unaffected.
|
|
38
|
+
* - A related_files entry ABSENT at the root is not automatically suspicious: an
|
|
39
|
+
* ordinary delete/rename recorded correctly at the resolved root produces the
|
|
40
|
+
* exact same shape (file gone here, still present in whichever sibling
|
|
41
|
+
* worktree branched before the change) as a genuine misroute.
|
|
42
|
+
* `pathKnownToHistory` distinguishes them — a path this checkout's own HEAD
|
|
43
|
+
* has ever tracked explains its absence without invoking a worktree guess.
|
|
44
|
+
*
|
|
45
|
+
* Coverage boundary: only checks the file evidence passed in THIS call (related_files
|
|
46
|
+
* / scope_hint_file / affected_files), not values inherited from an existing record
|
|
47
|
+
* on re-record/supersede — a call that omits its file field to rely on inheritance
|
|
48
|
+
* won't trip this guard even if it's happening in the wrong worktree. That's a
|
|
49
|
+
* deliberate tradeoff against false positives on stale evidence, not full coverage
|
|
50
|
+
* of every misrouted write.
|
|
51
|
+
*
|
|
52
|
+
* Absolute paths: agents naturally send them (edit-tool payloads and MCP roots
|
|
53
|
+
* are absolute), and a NAIVE `join(dir, "/abs/path")` produces a nonsense
|
|
54
|
+
* concatenated path that exists nowhere — the guard would find nothing to
|
|
55
|
+
* compare against ANY directory and stay silent, reproducing the exact bug the
|
|
56
|
+
* guard exists to catch. `existsUnder` checks an absolute entry AS ITSELF,
|
|
57
|
+
* scoped to whichever directory is under test (root, or each candidate worktree
|
|
58
|
+
* in turn) via `deepestContainer`, not `join()` — so an absolute path naming a
|
|
59
|
+
* file that exists only under a worktree's own tree is direct positive evidence
|
|
60
|
+
* for that worktree specifically, no existence heuristic required. Relativizing
|
|
61
|
+
* it against `root` alone can't represent a SIBLING worktree's absolute path at
|
|
62
|
+
* all: it's never under root's tree, so it relativizes to "../…" and gets
|
|
63
|
+
* dropped — silently reintroducing the bypass one level removed. A NESTED
|
|
64
|
+
* worktree (`root/.worktrees/x`) is the opposite trap: it IS lexically under
|
|
65
|
+
* root's own tree, so a plain "is this path under root" check wrongly
|
|
66
|
+
* attributes it to root — `deepestContainer` picks the MOST SPECIFIC
|
|
67
|
+
* (longest-path) containing worktree, not just any containing ancestor, so root
|
|
68
|
+
* never swallows a worktree nested inside it. An absolute path outside every
|
|
69
|
+
* known worktree matches none and correctly contributes nothing: not "absent
|
|
70
|
+
* here, check the siblings", just nothing to compare. Canonicalized
|
|
71
|
+
* (`canonicalRootPath`, the same symlink/case resolution `resolveActiveRoot`
|
|
72
|
+
* uses above) on both sides before comparing, so a worktree reached through a
|
|
73
|
+
* symlink or a case-different spelling isn't silently missed, and compared by
|
|
74
|
+
* exact path SEGMENT, not string prefix, so a real sibling `foo-other` doesn't
|
|
75
|
+
* false-match `foo` and a file genuinely named `..odd.ts` isn't mistaken for a
|
|
76
|
+
* `..` traversal. A DIRECTORY entry (or the empty-string/"." an agent might
|
|
77
|
+
* send meaning "the repo itself") contributes nothing either: `existsUnder`
|
|
78
|
+
* checks `isFile`, never mere existence, since a directory match would silently
|
|
79
|
+
* disable the guard for every OTHER entry in the same call — and
|
|
80
|
+
* `pathKnownToHistory` (below) confirms a git pathspec matched the EXACT entry,
|
|
81
|
+
* not merely something under/matching it, for the identical reason.
|
|
82
|
+
*
|
|
83
|
+
* A RELATIVE entry containing a ".." segment that escapes root's own tree when
|
|
84
|
+
* resolved against root is the same misroute as a worktree-rooted absolute
|
|
85
|
+
* path, merely spelled relatively — resolved against ROOT specifically (the
|
|
86
|
+
* only base the server actually has; never re-resolved per candidate in the
|
|
87
|
+
* loop below, which would answer a different, meaningless question) and then
|
|
88
|
+
* run through the identical absolute-path containment logic. A relative entry
|
|
89
|
+
* that does NOT escape root's tree keeps the original, intentional
|
|
90
|
+
* multi-location check instead: the SAME relative suffix tried against every
|
|
91
|
+
* candidate directory in turn — that's how a plain "this file's name" evidence
|
|
92
|
+
* has always found a sibling worktree holding a file by that name, and it must
|
|
93
|
+
* keep doing so for a NESTED worktree reached by a non-escaping relative path,
|
|
94
|
+
* whose containing worktree this does not (yet) distinguish from root itself —
|
|
95
|
+
* that residual gap fails OPEN (silently uncaught), never toward a false
|
|
96
|
+
* positive.
|
|
97
|
+
*
|
|
98
|
+
* Exported for direct unit testing — the candidate logic is otherwise reachable
|
|
99
|
+
* only through a full MCP client/server integration test. */
|
|
100
|
+
export declare const misroutedWorktreeCandidates: (root: string, relatedFiles: readonly string[]) => string[];
|
|
101
|
+
/** Normalizes file evidence for the misroute guard specifically. A literal
|
|
102
|
+
* backslash byte is a legal POSIX filename character, not a path separator,
|
|
103
|
+
* so blindly running every entry through `toPosixTarget` before
|
|
104
|
+
* `misroutedWorktreeCandidates` sees it can rewrite a real file's name into a
|
|
105
|
+
* fake extra path segment. The string alone can't disambiguate "Windows
|
|
106
|
+
* separator" from "literal POSIX byte", so this asks the filesystem AND git
|
|
107
|
+
* history instead (`namesRealEvidence`): when the RAW, un-normalized entry
|
|
108
|
+
* already names a real file, or is a relative path git knows was tracked at
|
|
109
|
+
* `root` (possibly since deleted/renamed), that settles it — the byte was
|
|
110
|
+
* literal — and the raw string is kept as-is, bypassing `toPosixTarget`
|
|
111
|
+
* entirely. Otherwise it normalizes through `toPosixTarget` exactly as
|
|
112
|
+
* before, preserving the legitimate Windows-separator case (a Windows-style
|
|
113
|
+
* path can never collide with a real or once-tracked POSIX file, since `\`
|
|
114
|
+
* cannot appear in a Windows filename to begin with). Scoped to the
|
|
115
|
+
* misroute-guard call sites only — the stored related_files/affected_files
|
|
116
|
+
* fields are still normalized separately, unaffected by this.
|
|
117
|
+
*
|
|
118
|
+
* Known accepted gap: a contrived collision — a file legitimately deleted
|
|
119
|
+
* from `root`'s history AND a genuine Windows-style path meaning something
|
|
120
|
+
* else present in a sibling worktree — resolves in favor of history and
|
|
121
|
+
* fails OPEN (no misroute reported), the same fail-open tradeoff
|
|
122
|
+
* `misroutedWorktreeCandidates` itself already documents elsewhere. */
|
|
123
|
+
export declare function guardEvidence(root: string, files: readonly string[]): string[];
|
|
124
|
+
/** The file-evidence field, if any, each nuryel_write facet carries: related_files
|
|
125
|
+
* for decisions, affected_files for findings/bugs, and scope (the glob list a
|
|
126
|
+
* constraint applies to — the same role hunch_record_correction's
|
|
127
|
+
* scope_hint_file plays) for constraints. A TOTAL map over StateFacet, not an
|
|
128
|
+
* open-ended ternary chain: adding a facet to STATE_FACETS without adding it
|
|
129
|
+
* here is a compile error, not a silent gap. null means the facet carries no
|
|
130
|
+
* comparable field and is left unguarded rather than inventing one. Exported so
|
|
131
|
+
* tests can assert the mapping directly instead of only probing it through live
|
|
132
|
+
* misroute behavior. */
|
|
133
|
+
export declare const FILE_EVIDENCE_FIELD: Record<StateFacet, string | null>;
|
|
134
|
+
/** `record` is caller-supplied z.record(string, unknown) — defensive by
|
|
135
|
+
* construction, so anything other than an array of strings reads as no
|
|
136
|
+
* evidence instead of throwing. */
|
|
137
|
+
export declare function fileEvidenceFor(facet: StateFacet, record: Record<string, unknown>): string[];
|
|
13
138
|
/** Resolve a free-form target (symbol id / name / file path) to symbol records.
|
|
14
139
|
* Tiered exact-id > exact-name > exact-file > segment-anchored-suffix matching,
|
|
15
140
|
* shared with `HunchStore.why()` via `matchSymbolsTiered` so the two don't drift
|