sweet-search 2.6.11 → 2.6.13
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/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +17 -13
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +10 -8
- package/core/search/grep-output-shaping.js +193 -0
- package/core/search/search-pattern.js +17 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +44 -18
- package/package.json +8 -8
- package/scripts/hooks/remind-tools.mjs +3 -2
- package/scripts/inject-agent-instructions.js +9 -8
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
variant: mcp
|
|
3
|
-
derived_from: p7-v1-
|
|
4
|
-
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/
|
|
3
|
+
derived_from: p7-v1-mpppp
|
|
4
|
+
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mpppp.md
|
|
5
5
|
benchmarked: false
|
|
6
6
|
note: >-
|
|
7
|
-
Hand-derived MCP-tool variant of the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
Hand-derived MCP-tool variant of the M++++ champion (p7-v1-mpppp = M++ +
|
|
8
|
+
stop-discipline scoping edits: search-rules-only intro, "stop searching" /
|
|
9
|
+
"Search output" wording; routing unchanged from the frozen, benchmarked
|
|
10
|
+
M++). The STRATEGY core — routing by what-you-hold, trust-the-top-hit,
|
|
11
|
+
sufficiency stops, the two-probe absence rule, the <state_summary> gate, and
|
|
12
|
+
the search-output discipline — is preserved (semantics intact; the rest
|
|
13
|
+
carries only the tool-mechanics
|
|
12
14
|
rename). Only the tool-mechanics layer is remapped: the six ss-* CLI commands collapse onto the sweet-search MCP
|
|
13
15
|
tool surface (a UNIFIED `search` that subsumes ss-search/ss-find/ss-grep, plus
|
|
14
16
|
`trace` / `read-semantic` / `read` / `repo-map`). The anti-raw-scan discipline
|
|
@@ -21,9 +23,11 @@ note: >-
|
|
|
21
23
|
---
|
|
22
24
|
# Sweet-search — code search tool guide
|
|
23
25
|
|
|
26
|
+
These are code-search rules only — they never decide when the task is done; the deliverable is whatever the task/prompt asks for. When the task calls for a change, locating the right code is a means, not the finish line: apply the edit with your normal file-editing tool and never a description of the fix in place of the fix.
|
|
27
|
+
|
|
24
28
|
Sweet-search indexes the working tree (uncommitted edits too) and searches it faster and cheaper than scanning files by hand. Use the sweet-search tools for all code search and navigation, in preference to native Grep/Read or shell `grep`/`find`/`cat`/`ls` — the index covers every file, so a manual scan only re-confirms a sweet-search result at higher cost, never beats it. Reach for a raw read only for an edit too recent to be reconciled (seconds old). Never re-run a sweet-search hit as a native Grep. Any sub-agent you delegate to must use these sweet-search tools, with this system prompt verbatim.
|
|
25
29
|
|
|
26
|
-
## Tools
|
|
30
|
+
## Tools (search tools, via the sweet-search MCP server)
|
|
27
31
|
The sweet-search MCP server exposes these (call each by the name your client lists it under):
|
|
28
32
|
- **search** — hybrid code search; returns ranked, self-contained code blocks. Your primary tool: it subsumes semantic search, exact-literal search, and semantic-reranked regex. Give it a natural-language query for a concept, or pass a `regex` (or lexical mode) for an exact token.
|
|
29
33
|
- **trace** — a symbol's callers, callees and impact in one call.
|
|
@@ -39,13 +43,13 @@ The sweet-search MCP server exposes these (call each by the name your client lis
|
|
|
39
43
|
Trust the top ranked result; confirm with at most one narrow `read`, never a re-run of a matching hit.
|
|
40
44
|
|
|
41
45
|
## Multi-file
|
|
42
|
-
Chain inside the tools: land the entry file, `read-semantic` it for the import or handoff symbol, then `search` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to a native Grep/Read to "just look" — is the main multi-file cost trap.
|
|
46
|
+
Chain inside the tools: land the entry file, `read-semantic` it for the import or handoff symbol, then `search` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop tracing there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to a native Grep/Read to "just look" — is the main multi-file cost trap.
|
|
43
47
|
|
|
44
|
-
## A confirmed absence is a complete answer
|
|
45
|
-
When what you're looking for may not exist, absence is settled once TWO complementary `search` probes come back empty for the same concept: one in natural language and one as a broad `regex` on its likeliest identifier (a short substring/prefix). A search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any native scan or file listing, so state the negative and stop: no third synonym, no native `grep`/`ls`/`cat` enumeration.
|
|
48
|
+
## A confirmed absence is a complete search answer
|
|
49
|
+
When what you're looking for may not exist, absence is settled once TWO complementary `search` probes come back empty for the same concept: one in natural language and one as a broad `regex` on its likeliest identifier (a short substring/prefix). A search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any native scan or file listing, so state the negative and stop searching: no third synonym, no native `grep`/`ls`/`cat` enumeration.
|
|
46
50
|
|
|
47
51
|
## Before the third probe
|
|
48
52
|
Before your third sweet-search probe in the current search iteration — or before your final answer, whichever comes first — output a `<state_summary>` block with exactly: (1) one sentence on what you've established, (2) one sentence on your current blind spot.
|
|
49
53
|
|
|
50
|
-
##
|
|
51
|
-
Stop the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match
|
|
54
|
+
## Search output
|
|
55
|
+
Stop searching the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match` — then finish whatever the task/prompt asks for.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
run_id: p7-v1-
|
|
2
|
+
run_id: p7-v1-mpppp
|
|
3
3
|
score_sonnet: 0.993
|
|
4
4
|
score_gpt5_5: 0.988
|
|
5
5
|
joint_maximin: 0.988
|
|
6
6
|
eas_factor: null
|
|
7
7
|
length_penalty: null
|
|
8
8
|
final_score: null
|
|
9
|
-
token_count:
|
|
9
|
+
token_count: 1175
|
|
10
10
|
scs_cwSCS: 0.931
|
|
11
11
|
scs_min_paraphrase_accuracy: 1
|
|
12
12
|
homp_family_pass: true
|
|
@@ -19,9 +19,11 @@ vault_within_15pct_of_heldout: true
|
|
|
19
19
|
---
|
|
20
20
|
# Sweet-search — code search tool guide
|
|
21
21
|
|
|
22
|
+
These are code-search rules only — they never decide when the task is done; the deliverable is whatever the task/prompt asks for. When the task calls for a change, locating the right code is a means, not the finish line: apply the edit with your normal file-editing tool and never a description of the fix in place of the fix.
|
|
23
|
+
|
|
22
24
|
Sweet-search indexes the working tree (uncommitted edits too) and searches it faster and cheaper than raw shell. Use the `ss-*` tools for all code search and navigation. Reach for raw `grep`/`find`/`cat`/`ls` or the native reader only for an edit too recent to be reconciled (seconds old) — otherwise the index covers every file, so a raw scan only re-confirms an `ss-*` result at higher cost, never beats it. Never re-run an `ss-*` hit as raw grep. Any sub-agent you delegate to must use these `ss-*` tools, with this system prompt verbatim.
|
|
23
25
|
|
|
24
|
-
## Tools (
|
|
26
|
+
## Tools (search commands, invoked via Bash)
|
|
25
27
|
`ss-search`/`ss-find` return ranked code blocks; `ss-grep` is file:line only; `ss-semantic` returns the top ranked spans in one file; `ss-trace` returns a symbol's callers, callees and impact. `ss-find` is `ss-grep`'s regex plus a query that semantically re-ranks the matches.
|
|
26
28
|
- `ss-search "<query>" [-k N]` — semantic; default when you have no exact symbol
|
|
27
29
|
- `ss-find "<query>" --regex "<regex>" [-k N]`
|
|
@@ -38,13 +40,13 @@ Sweet-search indexes the working tree (uncommitted edits too) and searches it fa
|
|
|
38
40
|
Trust the top ranked result; confirm with at most one narrow `ss-read`, never a re-run of a matching hit.
|
|
39
41
|
|
|
40
42
|
## Multi-file
|
|
41
|
-
Chain inside the tools: land the entry file, `ss-semantic` it for the import or handoff symbol, then `ss-search`/`ss-find` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to raw `cat`/`grep` to "just look" — is the main multi-file cost trap.
|
|
43
|
+
Chain inside the tools: land the entry file, `ss-semantic` it for the import or handoff symbol, then `ss-search`/`ss-find` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop tracing there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to raw `cat`/`grep` to "just look" — is the main multi-file cost trap.
|
|
42
44
|
|
|
43
|
-
## A confirmed absence is a complete answer
|
|
44
|
-
When what you're looking for may not exist, absence is settled once TWO complementary index probes come back empty for the same concept: one `ss-search` in natural language and one broad `ss-grep` on its likeliest identifier (a short substring/prefix). A semantic search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any raw scan or file listing, so state the negative and stop: no third synonym, no `find`/`ls`/`cat` enumeration, no native scan.
|
|
45
|
+
## A confirmed absence is a complete search answer
|
|
46
|
+
When what you're looking for may not exist, absence is settled once TWO complementary index probes come back empty for the same concept: one `ss-search` in natural language and one broad `ss-grep` on its likeliest identifier (a short substring/prefix). A semantic search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any raw scan or file listing, so state the negative and stop searching: no third synonym, no `find`/`ls`/`cat` enumeration, no native scan.
|
|
45
47
|
|
|
46
48
|
## Before the third probe
|
|
47
49
|
Before your third sweet-search probe in the current search iteration — or before your final answer, whichever comes first — output a `<state_summary>` block with exactly: (1) one sentence on what you've established, (2) one sentence on your current blind spot.
|
|
48
50
|
|
|
49
|
-
##
|
|
50
|
-
Stop the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match
|
|
51
|
+
## Search output
|
|
52
|
+
Stop searching the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match` — then finish whatever the task/prompt asks for.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-facing grep output shaping: k-budget file-level diversity.
|
|
3
|
+
*
|
|
4
|
+
* Root cause (task bench, rstudio-education__gradethis-161): bareGrep returns
|
|
5
|
+
* matches sorted alphabetically by file and the agent wrapper printed the
|
|
6
|
+
* first k grouped by file — so one flooded early-alphabet file could consume
|
|
7
|
+
* the entire k budget and structurally hide every other matching file, with
|
|
8
|
+
* no signal that elision happened.
|
|
9
|
+
*
|
|
10
|
+
* These helpers are pure and option-gated: only the ss-grep agent wrapper
|
|
11
|
+
* enables them. The human-facing grep product shape and all NL ranking paths
|
|
12
|
+
* are untouched (no scoring — file order stays the engine's deterministic
|
|
13
|
+
* sorted order; raw match count is deliberately never used as a sort key).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True when a match's repo-relative path is the drill-in target. Accepts the
|
|
18
|
+
* exact relative path as printed in grep output, a path with a leading "./",
|
|
19
|
+
* or a suffix (basename or trailing path segments) so agents can paste any
|
|
20
|
+
* reasonable spelling of the file they saw.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} file - repo-relative match path (as emitted by the engine)
|
|
23
|
+
* @param {string} filter - user-supplied --in value
|
|
24
|
+
*/
|
|
25
|
+
export function matchesGrepFileFilter(file, filter) {
|
|
26
|
+
if (!file || !filter) return false;
|
|
27
|
+
const f = String(filter).replace(/^\.\//, '').replace(/\\/g, '/');
|
|
28
|
+
const target = String(file).replace(/\\/g, '/');
|
|
29
|
+
return target === f || target.endsWith(`/${f}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Streaming per-file diversification of a (file,line)-sorted match list.
|
|
34
|
+
* Keeps at most `perFileCap` matches per file and at most `maxFiles` distinct
|
|
35
|
+
* files; everything beyond is COUNTED but never stored, so memory is bounded
|
|
36
|
+
* by perFileCap*maxFiles regardless of total match count.
|
|
37
|
+
*
|
|
38
|
+
* PRECONDITION: matches must be grouped by file (bareGrep sorts by
|
|
39
|
+
* (file, line, column) before calling) — the walk uses a single per-file
|
|
40
|
+
* cursor precisely so no map proportional to distinct-file count is built.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{file: string}>} matches - sorted by (file, line)
|
|
43
|
+
* @param {{perFileCap: number, maxFiles?: number, hiddenSampleSize?: number}} opts
|
|
44
|
+
* @returns {{kept: Array, fileSummary: {
|
|
45
|
+
* files: Array<{file: string, total: number, kept: number}>,
|
|
46
|
+
* hiddenFileCount: number, hiddenMatchCount: number,
|
|
47
|
+
* hiddenSample: Array<{file: string, total: number}>,
|
|
48
|
+
* }}}
|
|
49
|
+
*/
|
|
50
|
+
export function applyGrepFileDiversity(matches, opts = {}) {
|
|
51
|
+
const perFileCap = Math.max(1, opts.perFileCap | 0);
|
|
52
|
+
const maxFiles = opts.maxFiles > 0 ? (opts.maxFiles | 0) : Infinity;
|
|
53
|
+
const hiddenSampleSize = opts.hiddenSampleSize ?? 3;
|
|
54
|
+
|
|
55
|
+
const kept = [];
|
|
56
|
+
const files = []; // [{file, total, kept}] in first-appearance (sorted) order
|
|
57
|
+
const hiddenSample = []; // first few hidden files, name + total
|
|
58
|
+
let hiddenFileCount = 0;
|
|
59
|
+
let hiddenMatchCount = 0;
|
|
60
|
+
|
|
61
|
+
let current = null;
|
|
62
|
+
for (const m of matches) {
|
|
63
|
+
if (!current || current.file !== m.file) {
|
|
64
|
+
if (files.length < maxFiles) {
|
|
65
|
+
current = { file: m.file, total: 0, kept: 0 };
|
|
66
|
+
files.push(current);
|
|
67
|
+
} else {
|
|
68
|
+
current = { file: m.file, total: 0, kept: -1 }; // hidden file: count only
|
|
69
|
+
hiddenFileCount++;
|
|
70
|
+
if (hiddenSample.length < hiddenSampleSize) {
|
|
71
|
+
hiddenSample.push(current);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
current.total++;
|
|
76
|
+
if (current.kept >= 0 && current.kept < perFileCap) {
|
|
77
|
+
kept.push(m);
|
|
78
|
+
current.kept++;
|
|
79
|
+
} else if (current.kept < 0) {
|
|
80
|
+
hiddenMatchCount++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
kept,
|
|
86
|
+
fileSummary: {
|
|
87
|
+
files,
|
|
88
|
+
hiddenFileCount,
|
|
89
|
+
hiddenMatchCount,
|
|
90
|
+
hiddenSample: hiddenSample.map(h => ({ file: h.file, total: h.total })),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Breadth-first budget allocation: round-robin one line per file per round,
|
|
97
|
+
* in the given (deterministic) file order, until `budget` lines are allocated
|
|
98
|
+
* or every file's available matches are exhausted. Flooding is structurally
|
|
99
|
+
* impossible — file A gets its (r+1)-th line only after every other file got
|
|
100
|
+
* its r-th — while few-files queries keep full depth (deep rounds fill just
|
|
101
|
+
* like the old flat output).
|
|
102
|
+
*
|
|
103
|
+
* @param {number[]} counts - available (fetched) matches per file
|
|
104
|
+
* @param {number} budget - total lines to allocate (k)
|
|
105
|
+
* @returns {number[]} allocation per file, same order as counts
|
|
106
|
+
*/
|
|
107
|
+
export function allocateGrepBudget(counts, budget) {
|
|
108
|
+
const alloc = counts.map(() => 0);
|
|
109
|
+
let remaining = Math.max(0, budget | 0);
|
|
110
|
+
let progressed = true;
|
|
111
|
+
while (remaining > 0 && progressed) {
|
|
112
|
+
progressed = false;
|
|
113
|
+
for (let i = 0; i < counts.length && remaining > 0; i++) {
|
|
114
|
+
if (alloc[i] < counts[i]) {
|
|
115
|
+
alloc[i]++;
|
|
116
|
+
remaining--;
|
|
117
|
+
progressed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return alloc;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Render the diversified agent grep body: matches grouped per file (research:
|
|
126
|
+
* contiguous per-file blocks read better than interleaving) with allocation
|
|
127
|
+
* decided breadth-first, and a visible inline `(+N more in this file)` marker
|
|
128
|
+
* on the last shown line of every truncated file — elision is never silent
|
|
129
|
+
* and costs no extra lines.
|
|
130
|
+
*
|
|
131
|
+
* @param {Array<{file: string, line: number, matchText?: string}>} kept -
|
|
132
|
+
* diversified matches in engine order (grouped by file)
|
|
133
|
+
* @param {{files: Array<{file, total, kept}>, hiddenFileCount, hiddenMatchCount,
|
|
134
|
+
* hiddenSample: Array<{file, total}>}} fileSummary
|
|
135
|
+
* @param {number} k - body line budget
|
|
136
|
+
* @returns {{lines: string[], shownMatches: number, matchedFileCount: number,
|
|
137
|
+
* truncatedFileCount: number, hiddenLine: string|null}}
|
|
138
|
+
*/
|
|
139
|
+
export function renderGrepBody(kept, fileSummary, k) {
|
|
140
|
+
const groups = new Map();
|
|
141
|
+
for (const m of kept) {
|
|
142
|
+
if (!groups.has(m.file)) groups.set(m.file, []);
|
|
143
|
+
groups.get(m.file).push(m);
|
|
144
|
+
}
|
|
145
|
+
const totals = new Map(fileSummary.files.map(f => [f.file, f.total]));
|
|
146
|
+
const ordered = [...groups.entries()];
|
|
147
|
+
const alloc = allocateGrepBudget(ordered.map(([, ms]) => ms.length), k);
|
|
148
|
+
|
|
149
|
+
const lines = [];
|
|
150
|
+
let shownMatches = 0;
|
|
151
|
+
let truncatedFileCount = 0;
|
|
152
|
+
const unallocated = []; // fetched files that got zero budget (more files than k)
|
|
153
|
+
ordered.forEach(([file, ms], i) => {
|
|
154
|
+
if (alloc[i] === 0) {
|
|
155
|
+
unallocated.push({ file, total: totals.get(file) ?? ms.length });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const total = totals.get(file) ?? ms.length;
|
|
159
|
+
for (let j = 0; j < alloc[i]; j++) {
|
|
160
|
+
const m = ms[j];
|
|
161
|
+
const text = (m.matchText || '').replace(/\s+/g, ' ').trim().slice(0, 140);
|
|
162
|
+
let line = `${file}:${m.line}: ${text}`;
|
|
163
|
+
if (j === alloc[i] - 1 && total > alloc[i]) {
|
|
164
|
+
line += ` (+${total - alloc[i]} more in this file)`;
|
|
165
|
+
truncatedFileCount++;
|
|
166
|
+
}
|
|
167
|
+
lines.push(line);
|
|
168
|
+
shownMatches++;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Files that matched but got no body line at all (more matching files than
|
|
173
|
+
// budget, or clipped by the engine's maxFiles fetch bound): one honest tail
|
|
174
|
+
// line naming the first few so the agent can jump straight to them.
|
|
175
|
+
const hiddenFiles = unallocated.length + fileSummary.hiddenFileCount;
|
|
176
|
+
const hiddenMatches = unallocated.reduce((a, f) => a + f.total, 0) + fileSummary.hiddenMatchCount;
|
|
177
|
+
let hiddenLine = null;
|
|
178
|
+
if (hiddenFiles > 0) {
|
|
179
|
+
const sample = [...unallocated.map(f => f.file), ...fileSummary.hiddenSample.map(f => f.file)]
|
|
180
|
+
.slice(0, 3);
|
|
181
|
+
hiddenLine = `# +${hiddenFiles} more file(s) with ${hiddenMatches} match(es)` +
|
|
182
|
+
(sample.length ? ` — e.g. ${sample.join(', ')}` : '') +
|
|
183
|
+
`; narrow the regex, raise -k, or drill in with --in <file>`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
lines,
|
|
188
|
+
shownMatches,
|
|
189
|
+
matchedFileCount: fileSummary.files.length + fileSummary.hiddenFileCount,
|
|
190
|
+
truncatedFileCount,
|
|
191
|
+
hiddenLine,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { PROJECT_ROOT } from '../infrastructure/config/index.js';
|
|
17
17
|
import { generateRegexMatches } from './search-pattern-planner.js';
|
|
18
18
|
import { buildBareGrepResults, filterMatchesBySymbolType, resolveSearchSymbolFilter, mapMatchesToChunks, readFileRange } from './search-pattern-chunks.js';
|
|
19
|
+
import { applyGrepFileDiversity, matchesGrepFileFilter } from './grep-output-shaping.js';
|
|
19
20
|
import { isRipgrepAvailable, runRipgrepJson } from './search-pattern-ripgrep.js';
|
|
20
21
|
import { ensureSparseGramIndex } from './search-pattern-prefilter.js';
|
|
21
22
|
import { packageForAgent } from './context-expander.js';
|
|
@@ -152,6 +153,11 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
152
153
|
const candidateResult = await generateRegexMatches(this || {}, regex, searchDir, options);
|
|
153
154
|
let matches = [...candidateResult.indexedMatches, ...candidateResult.overlayMatches];
|
|
154
155
|
matches = filterMatchesBySymbolType(matches, symbolType, this);
|
|
156
|
+
// Agent drill-in scope (--in <file>): applied BEFORE sort/cap so a
|
|
157
|
+
// late-alphabet file's matches can never be pre-clipped by maxMatches.
|
|
158
|
+
if (options.fileFilter) {
|
|
159
|
+
matches = matches.filter(m => matchesGrepFileFilter(m.file, options.fileFilter));
|
|
160
|
+
}
|
|
155
161
|
matches.sort((a, b) =>
|
|
156
162
|
a.file.localeCompare(b.file) ||
|
|
157
163
|
a.line - b.line ||
|
|
@@ -159,6 +165,16 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
159
165
|
);
|
|
160
166
|
|
|
161
167
|
const totalMatches = matches.length;
|
|
168
|
+
// Agent-only k-budget file diversity (option-gated; absent → byte-identical
|
|
169
|
+
// output). Streaming per-file cap: matches beyond the cap are counted, not
|
|
170
|
+
// stored, so memory is bounded by perFileCap*maxFiles, never total matches.
|
|
171
|
+
let fileSummary = null;
|
|
172
|
+
if (options.perFileCap > 0) {
|
|
173
|
+
({ kept: matches, fileSummary } = applyGrepFileDiversity(matches, {
|
|
174
|
+
perFileCap: options.perFileCap,
|
|
175
|
+
maxFiles: options.maxFiles,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
162
178
|
if (maxMatches > 0) {
|
|
163
179
|
matches = matches.slice(0, maxMatches);
|
|
164
180
|
}
|
|
@@ -170,6 +186,7 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
170
186
|
|
|
171
187
|
return {
|
|
172
188
|
results,
|
|
189
|
+
...(fileSummary ? { fileSummary } : {}),
|
|
173
190
|
stats: {
|
|
174
191
|
path: 'grep',
|
|
175
192
|
regex,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildGrepPattern, stripInertFlags, normalizeArgs, extractPositional,
|
|
19
19
|
parseLineRange, looksLikeOption,
|
|
20
20
|
} from './_ss-argparse.mjs';
|
|
21
|
+
import { renderGrepBody } from '../../../core/search/grep-output-shaping.js';
|
|
21
22
|
|
|
22
23
|
// Diagnostic-log isolation (agent-facing tools). The Sweet Search engine emits
|
|
23
24
|
// model/index load banners via console.log → stdout ("LateInteraction: Loaded…",
|
|
@@ -115,13 +116,16 @@ async function ensureWarmServerReady({ timeoutMs = 60000, intervalMs = 500 } = {
|
|
|
115
116
|
|
|
116
117
|
// --- subcommands ----------------------------------------------------------
|
|
117
118
|
|
|
118
|
-
const GREP_USAGE = 'Usage: ss-grep <regex> [-i|--ignore-case] [-w|--word-regexp] [-F|--fixed-strings] [-k N]';
|
|
119
|
+
const GREP_USAGE = 'Usage: ss-grep <regex> [-i|--ignore-case] [-w|--word-regexp] [-F|--fixed-strings] [--in <file>] [-k N]';
|
|
119
120
|
async function cmdGrep(rawArgs) {
|
|
120
121
|
const args = normalizeArgs(rawArgs);
|
|
121
122
|
const ignoreCase = parseBoolFlag(args, ['-i', '--ignore-case']);
|
|
122
123
|
const wordBound = parseBoolFlag(args, ['-w', '--word-regexp']);
|
|
123
124
|
const fixedString = parseBoolFlag(args, ['-F', '--fixed-strings']);
|
|
124
125
|
const k = readPositiveIntFlag(args, ['-k', '--top'], 20, GREP_USAGE);
|
|
126
|
+
// Drill-in scope: show matches from ONE file (the recovery affordance the
|
|
127
|
+
// diversified output advertises when it truncates a flooded file).
|
|
128
|
+
const inFile = readValueFlag(args, '--in', null, GREP_USAGE);
|
|
125
129
|
stripInertFlags(args);
|
|
126
130
|
const regex = buildGrepPattern(resolvePositional(args, GREP_USAGE), { ignoreCase, wordBound, fixedString });
|
|
127
131
|
if (!regex) {
|
|
@@ -129,25 +133,47 @@ async function cmdGrep(rawArgs) {
|
|
|
129
133
|
process.exit(2);
|
|
130
134
|
}
|
|
131
135
|
const s = await getSweetSearch();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const [file, lines] of grouped) {
|
|
142
|
-
for (const r of lines) {
|
|
136
|
+
|
|
137
|
+
if (inFile) {
|
|
138
|
+
// Single-file scope: flat output, depth up to k within that file.
|
|
139
|
+
const result = await s.bareGrep(regex, null, {
|
|
140
|
+
regex, maxMatches: k, contextLines: 0, fileFilter: inFile,
|
|
141
|
+
});
|
|
142
|
+
const total = result.stats?.totalMatches ?? result.results.length;
|
|
143
|
+
process.stdout.write(`# ss-grep: ${total} total match(es) for /${regex}/ (scope: --in ${inFile})\n`);
|
|
144
|
+
result.results.forEach((r, i) => {
|
|
143
145
|
const text = (r.matchText || '').replace(/\s+/g, ' ').trim().slice(0, 140);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
if (
|
|
146
|
+
const marker = (i === result.results.length - 1 && total > result.results.length)
|
|
147
|
+
? ` (+${total - result.results.length} more — raise -k)` : '';
|
|
148
|
+
process.stdout.write(`${r.file}:${r.line}: ${text}${marker}\n`);
|
|
149
|
+
});
|
|
150
|
+
if (result.results.length === 0) process.stdout.write('(no matches)\n');
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// k-budget file diversity: fetch at most min(k,100) matches per file across
|
|
155
|
+
// at most k files (a file can never show more than k lines, and more than k
|
|
156
|
+
// files can never fit), then allocate the k body lines breadth-first so one
|
|
157
|
+
// flooded file can never hide every other matching file (the gradethis-161
|
|
158
|
+
// failure). Rendering stays grouped per file; truncation is marked inline
|
|
159
|
+
// and drillable via --in.
|
|
160
|
+
const result = await s.bareGrep(regex, null, {
|
|
161
|
+
regex, maxMatches: 0, contextLines: 0,
|
|
162
|
+
perFileCap: Math.min(k, 100), maxFiles: k,
|
|
163
|
+
});
|
|
164
|
+
const total = result.stats?.totalMatches ?? result.results.length;
|
|
165
|
+
const fileSummary = result.fileSummary
|
|
166
|
+
|| { files: [], hiddenFileCount: 0, hiddenMatchCount: 0, hiddenSample: [] };
|
|
167
|
+
const body = renderGrepBody(result.results, fileSummary, k);
|
|
168
|
+
|
|
169
|
+
process.stdout.write(`# ss-grep: ${total} total match(es) for /${regex}/\n`);
|
|
170
|
+
if (body.truncatedFileCount > 0 || body.hiddenLine) {
|
|
171
|
+
process.stdout.write(`# ${body.matchedFileCount} file(s) matched; (+N more in this file)=truncated — ` +
|
|
172
|
+
`see the rest: ss-grep "<regex>" --in <file>\n`);
|
|
149
173
|
}
|
|
150
|
-
|
|
174
|
+
for (const line of body.lines) process.stdout.write(line + '\n');
|
|
175
|
+
if (body.hiddenLine) process.stdout.write(body.hiddenLine + '\n');
|
|
176
|
+
if (body.shownMatches === 0) process.stdout.write('(no matches)\n');
|
|
151
177
|
process.exit(0);
|
|
152
178
|
}
|
|
153
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.13",
|
|
4
4
|
"description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"vitest": "^4.0.16"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@sweet-search/native-darwin-arm64": "2.6.
|
|
171
|
-
"@sweet-search/native-darwin-x64": "2.6.
|
|
172
|
-
"@sweet-search/native-linux-arm64-gnu": "2.6.
|
|
173
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.
|
|
174
|
-
"@sweet-search/native-linux-x64-gnu": "2.6.
|
|
175
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.
|
|
176
|
-
"@sweet-search/bg-priority": "2.6.
|
|
170
|
+
"@sweet-search/native-darwin-arm64": "2.6.13",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.6.13",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.6.13",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.13",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.6.13",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.13",
|
|
176
|
+
"@sweet-search/bg-priority": "2.6.13"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* both the file and the settings entry.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
// Tool surface mirrors the shipped M
|
|
21
|
+
// Tool surface mirrors the shipped M++++ policy (the ss-* tools). Kept terse —
|
|
22
22
|
// the full decision tree lives in CLAUDE.md/AGENTS.md; this is just a nudge to
|
|
23
23
|
// stop the agent drifting back to raw grep/read between prompts.
|
|
24
24
|
const REMINDER = [
|
|
@@ -30,8 +30,9 @@ const REMINDER = [
|
|
|
30
30
|
'- Callers/callees/impact of a symbol: ss-trace <symbol>',
|
|
31
31
|
'- Known file, unclear span: ss-semantic <file> "<query>"',
|
|
32
32
|
'- Known file + line range: ss-read <file> <start> <end>',
|
|
33
|
-
'STOP the instant your evidence answers the query — one confirmed file+symbol is enough;',
|
|
33
|
+
'STOP searching the instant your evidence answers the query — one confirmed file+symbol is enough;',
|
|
34
34
|
'a second call costs more than it saves. Multi-file flow questions get one follow-up.',
|
|
35
|
+
'Search rules only — if the task asks for a change, apply the edit.',
|
|
35
36
|
'</sweet-search-reminder>',
|
|
36
37
|
'',
|
|
37
38
|
].join('\n');
|
|
@@ -46,17 +46,18 @@ function escapeRegex(s) {
|
|
|
46
46
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// ─── Canonical policy body = the
|
|
49
|
+
// ─── Canonical policy body = the M++++ champion (integration seam) ───────────
|
|
50
50
|
//
|
|
51
51
|
// Plan §10 / §3.7.1 step 13 / DDD "Integration seam": scripts/init.js is the
|
|
52
52
|
// SOLE consumer of the prompt-optimization ship-file. We read that artifact at
|
|
53
|
-
// load time and strip its YAML front-matter; the remaining body is the
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
53
|
+
// load time and strip its YAML front-matter; the remaining body is the M++++
|
|
54
|
+
// champion (PHASE7 M++ — held-out 0.988 Maximin, OOD 0.952, HOMP/SCS/counter
|
|
55
|
+
// all pass, 5-cell cross-harness validated — plus the stop-discipline scoping
|
|
56
|
+
// edits that fix M++'s over-stopping on FIX tasks; routing unchanged),
|
|
57
|
+
// injected VERBATIM into the harness files. Per-harness shims
|
|
57
58
|
// (Cursor frontmatter, @imports) are applied at write-time, not embedded here.
|
|
58
59
|
//
|
|
59
|
-
// The artifact is generated from
|
|
60
|
+
// The artifact is generated from Mpppp.md by
|
|
60
61
|
// `core/prompt-optimization/sweep/finalize-mpp.mjs` and shipped via the
|
|
61
62
|
// package.json "files" list. If it is missing we fail LOUDLY rather than
|
|
62
63
|
// silently shipping a placeholder/older policy.
|
|
@@ -75,7 +76,7 @@ export function stripFrontMatter(text) {
|
|
|
75
76
|
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M
|
|
79
|
+
function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M++++' } = {}) {
|
|
79
80
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/scripts
|
|
80
81
|
const shipPath = join(here, '..', rel);
|
|
81
82
|
let raw;
|
|
@@ -102,7 +103,7 @@ let _mcpPolicyBody = null;
|
|
|
102
103
|
/** Lazily read + cache the MCP-variant policy body. */
|
|
103
104
|
export function getMcpPolicyBody() {
|
|
104
105
|
if (_mcpPolicyBody == null) {
|
|
105
|
-
_mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M
|
|
106
|
+
_mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M++++ (MCP variant)' });
|
|
106
107
|
}
|
|
107
108
|
return _mcpPolicyBody;
|
|
108
109
|
}
|