zob-harness 0.5.0 → 0.7.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/.pi/capabilities/zob-public-runtime-capabilities.json +22 -0
- package/.pi/context-discovery.json +36 -0
- package/.pi/extensions/zob-harness/src/domains/context/context-discovery.ts +317 -0
- package/.pi/extensions/zob-harness/src/runtime/commands.ts +3 -0
- package/.pi/extensions/zob-harness/src/runtime/events.ts +4 -2
- package/.pi/extensions/zob-harness/src/runtime/schemas.ts +10 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-context.ts +20 -0
- package/.pi/extensions/zob-harness/src/runtime/zob-intro.ts +662 -0
- package/.pi/skills/zob-context-discovery/SKILL.md +54 -0
- package/.pi/skills/zob-harness/SKILL.md +2 -2
- package/AGENTS.md +1 -1
- package/README.md +19 -2
- package/package.json +7 -1
- package/scripts/README.md +4 -0
- package/scripts/context-discovery/doctor.mjs +32 -0
- package/scripts/context-discovery/init-colgrep.mjs +64 -0
- package/scripts/context-discovery/query.mjs +61 -0
- package/scripts/context-discovery/shared.mjs +249 -0
- package/scripts/context-discovery/smoke.mjs +36 -0
|
@@ -1010,6 +1010,28 @@
|
|
|
1010
1010
|
],
|
|
1011
1011
|
"noShipNotes": "Proposal only; no direct command execution."
|
|
1012
1012
|
},
|
|
1013
|
+
{
|
|
1014
|
+
"name": "zob_context_search",
|
|
1015
|
+
"family": "context",
|
|
1016
|
+
"modes": [
|
|
1017
|
+
"explore",
|
|
1018
|
+
"plan",
|
|
1019
|
+
"implement",
|
|
1020
|
+
"oracle",
|
|
1021
|
+
"factory",
|
|
1022
|
+
"orchestrator"
|
|
1023
|
+
],
|
|
1024
|
+
"skillRefs": [
|
|
1025
|
+
".pi/skills/zob-context-discovery/SKILL.md",
|
|
1026
|
+
".pi/skills/zob-harness/SKILL.md"
|
|
1027
|
+
],
|
|
1028
|
+
"docRefs": [
|
|
1029
|
+
"reports/context-discovery/design.md",
|
|
1030
|
+
"README.md",
|
|
1031
|
+
"scripts/README.md"
|
|
1032
|
+
],
|
|
1033
|
+
"noShipNotes": "Bounded repo-local context discovery only; prefer ColGREP when installed/ready, otherwise use grep/find/read fallback. Never auto-install ColGREP, run installer/network/package-manager commands, read forbidden/secret/session/vendor/build paths, persist raw secret/session bodies, inject stale/global/unbounded prompt context, or treat broad search hits as exact proof without grep/read/file-ref verification. Oracle/no-ship review must check freshness, citation coverage, and forbidden-source violations."
|
|
1034
|
+
},
|
|
1013
1035
|
{
|
|
1014
1036
|
"name": "zob_context_readiness",
|
|
1015
1037
|
"family": "context",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"preferredProvider": "colgrep",
|
|
4
|
+
"fallbackProvider": "grep",
|
|
5
|
+
"includePaths": [
|
|
6
|
+
".pi/extensions",
|
|
7
|
+
".pi/skills",
|
|
8
|
+
".pi/capabilities",
|
|
9
|
+
"scripts",
|
|
10
|
+
"docs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"AGENTS.md"
|
|
13
|
+
],
|
|
14
|
+
"excludePaths": [
|
|
15
|
+
".env",
|
|
16
|
+
"**/.env",
|
|
17
|
+
".env.*",
|
|
18
|
+
"**/*secret*",
|
|
19
|
+
"**/*key*",
|
|
20
|
+
"*.pem",
|
|
21
|
+
".pi/sessions",
|
|
22
|
+
".pi/agent-sessions",
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"build"
|
|
26
|
+
],
|
|
27
|
+
"limits": {
|
|
28
|
+
"maxResults": 20,
|
|
29
|
+
"maxContextLines": 2,
|
|
30
|
+
"maxFileBytes": 1048576
|
|
31
|
+
},
|
|
32
|
+
"promptInjection": {
|
|
33
|
+
"enabled": true,
|
|
34
|
+
"includeInstallHint": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, extname, isAbsolute, join, normalize, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type ContextSearchMode = "auto" | "semantic" | "hybrid" | "regex" | "files";
|
|
6
|
+
|
|
7
|
+
export interface ContextDiscoveryConfig {
|
|
8
|
+
schemaVersion: number;
|
|
9
|
+
preferredProvider: string;
|
|
10
|
+
fallbackProvider: string;
|
|
11
|
+
includePaths: string[];
|
|
12
|
+
excludePaths: string[];
|
|
13
|
+
limits: {
|
|
14
|
+
maxResults: number;
|
|
15
|
+
maxContextLines: number;
|
|
16
|
+
maxFileBytes: number;
|
|
17
|
+
};
|
|
18
|
+
promptInjection: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
includeInstallHint: boolean;
|
|
21
|
+
};
|
|
22
|
+
loadedFrom: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ContextSearchParams {
|
|
26
|
+
query: string;
|
|
27
|
+
mode?: ContextSearchMode;
|
|
28
|
+
pattern?: string;
|
|
29
|
+
paths?: string[];
|
|
30
|
+
max_results?: number;
|
|
31
|
+
max_context_lines?: number;
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NormalizedContextResult {
|
|
36
|
+
path: string;
|
|
37
|
+
line?: number;
|
|
38
|
+
ref: string;
|
|
39
|
+
preview: string;
|
|
40
|
+
context?: Array<{ line: number; text: string }>;
|
|
41
|
+
score?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CONFIG_PATH = ".pi/context-discovery.json";
|
|
45
|
+
const DEFAULT_CONFIG: ContextDiscoveryConfig = {
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
preferredProvider: "colgrep",
|
|
48
|
+
fallbackProvider: "grep",
|
|
49
|
+
includePaths: [".pi/extensions", ".pi/skills", ".pi/capabilities", "scripts", "docs", "README.md", "AGENTS.md"],
|
|
50
|
+
excludePaths: [".env", "**/.env", ".env.*", "**/*secret*", "**/*key*", "*.pem", ".pi/sessions", ".pi/agent-sessions", "node_modules", "dist", "build"],
|
|
51
|
+
limits: { maxResults: 20, maxContextLines: 2, maxFileBytes: 1024 * 1024 },
|
|
52
|
+
promptInjection: { enabled: true, includeInstallHint: true },
|
|
53
|
+
loadedFrom: "defaults",
|
|
54
|
+
};
|
|
55
|
+
const TEXT_EXTENSIONS = new Set(["", ".cjs", ".css", ".js", ".json", ".md", ".mjs", ".sh", ".ts", ".tsx", ".txt", ".yaml", ".yml"]);
|
|
56
|
+
|
|
57
|
+
function clampInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
58
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
59
|
+
if (!Number.isFinite(numberValue)) return fallback;
|
|
60
|
+
return Math.max(min, Math.min(max, Math.floor(numberValue)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shellQuote(value: string): string {
|
|
64
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function globToRegExp(pattern: string): RegExp {
|
|
68
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
|
|
69
|
+
return new RegExp(`^${escaped}$`, "iu");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeRepoPath(raw: unknown): string | undefined {
|
|
73
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return undefined;
|
|
74
|
+
const trimmed = raw.trim().replace(/^\.\//u, "");
|
|
75
|
+
if (trimmed.includes("\0") || trimmed.includes("\\") || isAbsolute(trimmed)) return undefined;
|
|
76
|
+
const normalized = normalize(trimmed);
|
|
77
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith(`..${sep}`)) return undefined;
|
|
78
|
+
return normalized.split(sep).join("/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pathIsExcluded(relPath: string, excludePaths: string[]): boolean {
|
|
82
|
+
const normalized = normalizeRepoPath(relPath);
|
|
83
|
+
if (!normalized) return true;
|
|
84
|
+
return excludePaths.some((pattern) => {
|
|
85
|
+
const clean = normalizeRepoPath(pattern) ?? pattern;
|
|
86
|
+
if (clean.includes("*")) {
|
|
87
|
+
return globToRegExp(clean).test(normalized) || globToRegExp(clean.replace(/^\*\*\//u, "")).test(basename(normalized));
|
|
88
|
+
}
|
|
89
|
+
return normalized === clean || normalized.startsWith(`${clean}/`) || basename(normalized) === clean;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function loadContextDiscoveryConfig(repoRoot: string): ContextDiscoveryConfig {
|
|
94
|
+
const path = join(repoRoot, CONFIG_PATH);
|
|
95
|
+
if (!existsSync(path)) return { ...DEFAULT_CONFIG };
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<ContextDiscoveryConfig>;
|
|
98
|
+
return {
|
|
99
|
+
...DEFAULT_CONFIG,
|
|
100
|
+
...parsed,
|
|
101
|
+
includePaths: Array.isArray(parsed.includePaths) ? parsed.includePaths.filter((item): item is string => typeof item === "string") : DEFAULT_CONFIG.includePaths,
|
|
102
|
+
excludePaths: Array.isArray(parsed.excludePaths) ? parsed.excludePaths.filter((item): item is string => typeof item === "string") : DEFAULT_CONFIG.excludePaths,
|
|
103
|
+
limits: { ...DEFAULT_CONFIG.limits, ...(parsed.limits ?? {}) },
|
|
104
|
+
promptInjection: { ...DEFAULT_CONFIG.promptInjection, ...(parsed.promptInjection ?? {}) },
|
|
105
|
+
loadedFrom: CONFIG_PATH,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return { ...DEFAULT_CONFIG, loadedFrom: `${CONFIG_PATH}:unreadable-fallback-defaults` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function commandExists(repoRoot: string, command: string): boolean {
|
|
113
|
+
if (process.env.ZOB_CONTEXT_FORCE_FALLBACK === "1") return false;
|
|
114
|
+
const result = spawnSync("sh", ["-c", `command -v ${shellQuote(command)}`], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 2_000 });
|
|
115
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function detectColgrep(repoRoot: string): { provider: "colgrep" | "grep-fallback"; installed: boolean; ready: boolean; statusCode?: number | null; guidance: string } {
|
|
119
|
+
if (!commandExists(repoRoot, "colgrep")) {
|
|
120
|
+
return { provider: "grep-fallback", installed: false, ready: false, guidance: "ColGREP is not on PATH. Optional setup: install ColGREP manually, then run npm run zob:context:init. Grep/find fallback is active." };
|
|
121
|
+
}
|
|
122
|
+
const status = spawnSync("colgrep", ["status"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5_000 });
|
|
123
|
+
const ready = status.status === 0;
|
|
124
|
+
return {
|
|
125
|
+
provider: ready ? "colgrep" : "grep-fallback",
|
|
126
|
+
installed: true,
|
|
127
|
+
ready,
|
|
128
|
+
statusCode: status.status,
|
|
129
|
+
guidance: ready ? "ColGREP detected and ready." : "ColGREP is installed but not ready/indexed. Run npm run zob:context:init or inspect colgrep status. Grep/find fallback is active.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildActiveSearchBackendPromptSnippet(repoRoot: string): string {
|
|
134
|
+
const config = loadContextDiscoveryConfig(repoRoot);
|
|
135
|
+
if (!config.promptInjection.enabled) return "";
|
|
136
|
+
const detection = detectColgrep(repoRoot);
|
|
137
|
+
const scope = `${config.loadedFrom}; roots=${config.includePaths.slice(0, 6).join(",")}; excludes=${config.excludePaths.length}`;
|
|
138
|
+
if (detection.ready) {
|
|
139
|
+
return `\n\nZOB ACTIVE SEARCH BACKEND\n- active search backend: colgrep\n- prompt injection: enabled by ${scope}; bounded per turn from current repo config, not a global/stale context pack.\n- Prefer zob_context_search for codebase discovery and broad/semantic search; use grep/read for exact proof and final citations.\n- Search output must stay bounded and avoid forbidden paths/secrets.`;
|
|
140
|
+
}
|
|
141
|
+
const installHint = config.promptInjection.includeInstallHint ? `\n- Optional ColGREP setup hint: ${detection.guidance}` : "";
|
|
142
|
+
return `\n\nZOB ACTIVE SEARCH BACKEND\n- active search backend: grep fallback\n- prompt injection: enabled by ${scope}; bounded per turn from current repo config, not a global/stale context pack.\n- Prefer zob_context_search, grep, find, and read for bounded repo-local discovery and exact proof.${installHint}\n- Missing ColGREP is not a blocker; do not auto-install it.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function safeSearchRoots(repoRoot: string, config: ContextDiscoveryConfig, requestedPaths?: string[]): { roots: string[]; rejected: string[] } {
|
|
146
|
+
const source = requestedPaths && requestedPaths.length > 0 ? requestedPaths : config.includePaths;
|
|
147
|
+
const roots: string[] = [];
|
|
148
|
+
const rejected: string[] = [];
|
|
149
|
+
for (const raw of source) {
|
|
150
|
+
const relPath = normalizeRepoPath(raw);
|
|
151
|
+
if (!relPath || pathIsExcluded(relPath, config.excludePaths)) {
|
|
152
|
+
rejected.push(String(raw));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!existsSync(join(repoRoot, relPath))) {
|
|
156
|
+
rejected.push(String(raw));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
roots.push(relPath);
|
|
160
|
+
}
|
|
161
|
+
return { roots: [...new Set(roots)].sort(), rejected };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function looksTextFile(relPath: string): boolean {
|
|
165
|
+
return TEXT_EXTENSIONS.has(extname(relPath).toLowerCase());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectFiles(repoRoot: string, relPath: string, config: ContextDiscoveryConfig, out: string[]): void {
|
|
169
|
+
const safeRel = normalizeRepoPath(relPath);
|
|
170
|
+
if (!safeRel || pathIsExcluded(safeRel, config.excludePaths)) return;
|
|
171
|
+
const absolute = join(repoRoot, safeRel);
|
|
172
|
+
if (!existsSync(absolute)) return;
|
|
173
|
+
const stat = statSync(absolute);
|
|
174
|
+
if (stat.isFile()) {
|
|
175
|
+
if (stat.size <= config.limits.maxFileBytes && looksTextFile(safeRel)) out.push(safeRel);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!stat.isDirectory()) return;
|
|
179
|
+
for (const entry of readdirSync(absolute, { withFileTypes: true })) collectFiles(repoRoot, join(safeRel, entry.name), config, out);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeLineResult(repoRoot: string, item: Record<string, unknown>, config: ContextDiscoveryConfig): NormalizedContextResult | undefined {
|
|
183
|
+
const rawPath = item.path ?? item.file ?? item.filename ?? item.source_path;
|
|
184
|
+
const path = normalizeRepoPath(rawPath);
|
|
185
|
+
if (!path || pathIsExcluded(path, config.excludePaths) || !existsSync(join(repoRoot, path))) return undefined;
|
|
186
|
+
const lineValue = item.line ?? item.lineNumber ?? item.line_number ?? item.start_line;
|
|
187
|
+
const line = typeof lineValue === "number" ? Math.max(1, Math.floor(lineValue)) : undefined;
|
|
188
|
+
const rawPreview = item.preview ?? item.text ?? item.lineText ?? item.content ?? item.match ?? "";
|
|
189
|
+
const preview = String(rawPreview).replace(/\s+/gu, " ").trim().slice(0, 240);
|
|
190
|
+
const score = typeof item.score === "number" ? item.score : undefined;
|
|
191
|
+
return { path, line, ref: line ? `${path}:${line}` : path, preview, score };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractJsonResults(repoRoot: string, stdout: string, config: ContextDiscoveryConfig, maxResults: number): NormalizedContextResult[] {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
197
|
+
const container = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null ? parsed as Record<string, unknown> : {};
|
|
198
|
+
const candidates = Array.isArray(container) ? container : [container.results, container.matches, container.items].find(Array.isArray) ?? [];
|
|
199
|
+
return (candidates as unknown[]).filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null).map((item) => normalizeLineResult(repoRoot, item, config)).filter((item): item is NormalizedContextResult => Boolean(item)).slice(0, maxResults);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runColgrep(repoRoot: string, query: string, roots: string[], config: ContextDiscoveryConfig, maxResults: number, maxContextLines: number): { ok: boolean; results: NormalizedContextResult[]; error?: string } {
|
|
206
|
+
const args = ["--json", "-k", String(maxResults), "-n", String(maxContextLines), query, ...roots];
|
|
207
|
+
const result = spawnSync("colgrep", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10_000, maxBuffer: 1024 * 1024 });
|
|
208
|
+
if (result.status !== 0) return { ok: false, results: [], error: result.stderr.trim().slice(0, 240) || `colgrep exited ${String(result.status)}` };
|
|
209
|
+
const results = extractJsonResults(repoRoot, result.stdout, config, maxResults);
|
|
210
|
+
return { ok: true, results };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function fallbackSearch(repoRoot: string, params: Required<Pick<ContextSearchParams, "query" | "mode">> & Pick<ContextSearchParams, "pattern" | "paths">, config: ContextDiscoveryConfig, maxResults: number, maxContextLines: number): { results: NormalizedContextResult[]; rejectedPaths: string[]; roots: string[] } {
|
|
214
|
+
const { roots, rejected } = safeSearchRoots(repoRoot, config, params.paths);
|
|
215
|
+
const files: string[] = [];
|
|
216
|
+
for (const root of roots) collectFiles(repoRoot, root, config, files);
|
|
217
|
+
const needle = params.mode === "regex" ? params.pattern ?? params.query : params.query;
|
|
218
|
+
let regex: RegExp | undefined;
|
|
219
|
+
if (params.mode === "regex") {
|
|
220
|
+
try { regex = new RegExp(needle, "iu"); } catch { regex = undefined; }
|
|
221
|
+
}
|
|
222
|
+
const results: NormalizedContextResult[] = [];
|
|
223
|
+
for (const path of [...new Set(files)].sort()) {
|
|
224
|
+
if (results.length >= maxResults) break;
|
|
225
|
+
if (params.mode === "files") {
|
|
226
|
+
if (!path.toLowerCase().includes(params.query.toLowerCase())) continue;
|
|
227
|
+
results.push({ path, ref: path, preview: path });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const lines = readFileSync(join(repoRoot, path), "utf8").split(/\r?\n/u);
|
|
231
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
232
|
+
const matched = regex ? regex.test(lines[index]) : lines[index].toLowerCase().includes(params.query.toLowerCase());
|
|
233
|
+
if (!matched) continue;
|
|
234
|
+
const start = Math.max(0, index - maxContextLines);
|
|
235
|
+
const end = Math.min(lines.length, index + maxContextLines + 1);
|
|
236
|
+
results.push({
|
|
237
|
+
path,
|
|
238
|
+
line: index + 1,
|
|
239
|
+
ref: `${path}:${index + 1}`,
|
|
240
|
+
preview: lines[index].trim().slice(0, 240),
|
|
241
|
+
context: lines.slice(start, end).map((text, offset) => ({ line: start + offset + 1, text: text.slice(0, 240) })),
|
|
242
|
+
});
|
|
243
|
+
if (results.length >= maxResults) break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { results, rejectedPaths: rejected, roots };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function runContextSearch(repoRoot: string, input: ContextSearchParams): Record<string, unknown> {
|
|
250
|
+
const config = loadContextDiscoveryConfig(repoRoot);
|
|
251
|
+
const query = String(input.query ?? "").trim();
|
|
252
|
+
const mode = input.mode ?? "auto";
|
|
253
|
+
const maxResults = clampInteger(input.max_results, config.limits.maxResults, 1, 50);
|
|
254
|
+
const maxContextLines = clampInteger(input.max_context_lines, config.limits.maxContextLines, 0, 5);
|
|
255
|
+
const detection = detectColgrep(repoRoot);
|
|
256
|
+
const { roots, rejected } = safeSearchRoots(repoRoot, config, input.paths);
|
|
257
|
+
let provider = detection.ready ? "colgrep" : "grep-fallback";
|
|
258
|
+
let fallback = !detection.ready;
|
|
259
|
+
let fallbackReason = detection.ready ? undefined : detection.guidance;
|
|
260
|
+
let results: NormalizedContextResult[] = [];
|
|
261
|
+
if (detection.ready && mode !== "regex" && mode !== "files") {
|
|
262
|
+
const colgrep = runColgrep(repoRoot, query, roots, config, maxResults, maxContextLines);
|
|
263
|
+
if (colgrep.ok) results = colgrep.results;
|
|
264
|
+
else {
|
|
265
|
+
provider = "grep-fallback";
|
|
266
|
+
fallback = true;
|
|
267
|
+
fallbackReason = colgrep.error ?? "ColGREP query failed; grep fallback used.";
|
|
268
|
+
}
|
|
269
|
+
} else if (detection.ready) {
|
|
270
|
+
provider = "grep-fallback";
|
|
271
|
+
fallback = true;
|
|
272
|
+
fallbackReason = `${mode} mode uses exact grep/find fallback for deterministic results.`;
|
|
273
|
+
}
|
|
274
|
+
if (fallback || results.length === 0) {
|
|
275
|
+
const fallbackResult = fallbackSearch(repoRoot, { query, mode, pattern: input.pattern, paths: input.paths }, config, maxResults, maxContextLines);
|
|
276
|
+
results = fallbackResult.results;
|
|
277
|
+
}
|
|
278
|
+
const refs = results.map((item) => item.ref);
|
|
279
|
+
return {
|
|
280
|
+
schema: "zob.context-search-result.v1",
|
|
281
|
+
provider,
|
|
282
|
+
preferredProvider: config.preferredProvider,
|
|
283
|
+
fallback,
|
|
284
|
+
fallbackReason,
|
|
285
|
+
colgrepInstalled: detection.installed,
|
|
286
|
+
colgrepReady: detection.ready,
|
|
287
|
+
mode,
|
|
288
|
+
resultCount: results.length,
|
|
289
|
+
refs,
|
|
290
|
+
results,
|
|
291
|
+
searchedRoots: roots,
|
|
292
|
+
rejectedPaths: rejected,
|
|
293
|
+
limits: { maxResults, maxContextLines },
|
|
294
|
+
recommendedVerification: refs.length > 0
|
|
295
|
+
? [`grep -n ${shellQuote(query)} ${shellQuote(results[0]?.path ?? roots[0] ?? ".")}`, `read ${results[0]?.path ?? roots[0] ?? "."}`]
|
|
296
|
+
: [`grep -R -n ${shellQuote(query)} ${roots.map(shellQuote).join(" ") || "."}`, "find relevant safe paths, then read exact files"],
|
|
297
|
+
safety: { repoRelativeOnly: true, forbiddenPathsExcluded: config.excludePaths, rawPromptOrConversationPersisted: false, autoInstall: false },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function formatContextSearchResult(result: Record<string, unknown>): string {
|
|
302
|
+
const provider = String(result.provider ?? "unknown");
|
|
303
|
+
const fallback = result.fallback === true ? "yes" : "no";
|
|
304
|
+
const count = typeof result.resultCount === "number" ? result.resultCount : 0;
|
|
305
|
+
const lines = [`zob_context_search: provider=${provider} fallback=${fallback} results=${count}`];
|
|
306
|
+
const reason = typeof result.fallbackReason === "string" ? result.fallbackReason : undefined;
|
|
307
|
+
if (reason) lines.push(`fallback_status: ${reason}`);
|
|
308
|
+
const results = Array.isArray(result.results) ? result.results.slice(0, 10) : [];
|
|
309
|
+
for (const item of results) {
|
|
310
|
+
if (typeof item !== "object" || item === null) continue;
|
|
311
|
+
const record = item as Record<string, unknown>;
|
|
312
|
+
lines.push(`- ${String(record.ref ?? record.path ?? "result")}: ${String(record.preview ?? "").slice(0, 240)}`);
|
|
313
|
+
}
|
|
314
|
+
const verification = Array.isArray(result.recommendedVerification) ? result.recommendedVerification.slice(0, 2).map(String) : [];
|
|
315
|
+
if (verification.length > 0) lines.push(`verify: ${verification.join(" ; ")}`);
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
@@ -26,6 +26,7 @@ import { showDelegationOverlay } from "./delegation-overlay.js";
|
|
|
26
26
|
import { finishDelegationRun } from "./delegation-monitor.js";
|
|
27
27
|
import { showGoalTodoOverlay } from "./goal-todo-overlay.js";
|
|
28
28
|
import type { HarnessRuntimeState } from "./state.js";
|
|
29
|
+
import { registerZobIntroCommand } from "./zob-intro.js";
|
|
29
30
|
import {
|
|
30
31
|
asInteractiveAutonomyMode,
|
|
31
32
|
formatInteractiveAutonomyStatus,
|
|
@@ -986,6 +987,8 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
|
|
|
986
987
|
refreshIntentClassifierModelCache(ctx);
|
|
987
988
|
});
|
|
988
989
|
|
|
990
|
+
registerZobIntroCommand(pi);
|
|
991
|
+
|
|
989
992
|
// Exact `/new` is handled by Pi before extension input/command hooks. Soft carryover
|
|
990
993
|
// is therefore written from the `session_shutdown` reason="new" hook in events.ts.
|
|
991
994
|
// Keep this registration only for `/new hard`, where users need an explicit clean reset.
|
|
@@ -4,6 +4,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
4
4
|
|
|
5
5
|
import { EXTERNAL_PACKAGE_TOOLS_CONTRACT, MODE_PROMPTS, ZOB_COMPACTION_CONTINUITY_CONTRACT, ZOB_TOOL_ROUTING_CONTRACT } from "../core/constants.js";
|
|
6
6
|
import { buildCurrentZobLivePeerCard } from "../domains/coms/coms-v2/identity.js";
|
|
7
|
+
import { buildActiveSearchBackendPromptSnippet } from "../domains/context/context-discovery.js";
|
|
7
8
|
import { buildZobLiveAckEnvelope, buildZobLiveErrorEnvelope, buildZobLivePongEnvelope } from "../domains/coms/coms-v2/envelope.js";
|
|
8
9
|
import { appendLiveCompletedRef } from "../domains/coms/coms-v2/ledger-bridge.js";
|
|
9
10
|
import { bindZobLocalEndpoint, makeZobLocalEndpoint, sendZobLocalEnvelope } from "../domains/coms/coms-v2/local-transport.js";
|
|
@@ -979,7 +980,7 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
979
980
|
}
|
|
980
981
|
});
|
|
981
982
|
|
|
982
|
-
pi.on("before_agent_start", async (event) => {
|
|
983
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
983
984
|
const goalHint = state.activeGoal
|
|
984
985
|
? `\n\nZOB GOAL GATE\n- ORIGINAL_USER_ASK: ${state.activeGoal.originalUserAsk}\n- ACTIVE_GOAL: ${state.activeGoal.activeGoal}\n- EXPECTED_OUTPUT: ${state.activeGoal.expectedOutput}\n- CONSTRAINTS: ${state.activeGoal.constraints}\n- VALIDATION_EVIDENCE: ${state.activeGoal.validationEvidence}`
|
|
985
986
|
: "\n\nZOB GOAL GATE\n- No active goal set. If the request is broad or multi-step, use /goal_gate first or restate ORIGINAL_USER_ASK / ACTIVE_GOAL explicitly before delegating.";
|
|
@@ -993,10 +994,11 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
993
994
|
const autonomyHint = `\n\n${formatInteractiveAutonomyPromptHint(state.autonomy)}`;
|
|
994
995
|
const zagentHint = formatZagentPromptHint(state);
|
|
995
996
|
const zpeerHint = buildZpeerAwarenessPrompt(state, state.zobLive.inbound?.repoRoot ?? process.cwd());
|
|
997
|
+
const activeSearchBackendHint = buildActiveSearchBackendPromptSnippet(ctx.cwd);
|
|
996
998
|
if (state.activeMode === "vanilla") {
|
|
997
999
|
return { systemPrompt: `${event.systemPrompt}\n\n${MODE_PROMPTS.vanilla}` };
|
|
998
1000
|
}
|
|
999
|
-
const contractHint = `\n\nZOB HARNESS OPERATING CONTRACT\n- Prefer Explore -> Plan -> Implement -> Oracle for non-trivial work.\n- Use the six-part contract for delegated work: TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT.\n- Do not claim completion without concrete evidence.\n- If output may truncate, prioritize verdict, blockers, and next steps over exhaustive listings.\n\n${SAME_AGENT_MODE_INTENT_PROMPT}\n\n${ZOB_TOOL_ROUTING_CONTRACT}\n\n${ZOB_COMPACTION_CONTINUITY_CONTRACT}\n\n${EXTERNAL_PACKAGE_TOOLS_CONTRACT}\n\n${MODE_PROMPTS[state.activeMode]}${goalHint}${runtimeGoalHint}${rulesHint}${autonomyHint}${zagentHint}${zpeerHint}`;
|
|
1001
|
+
const contractHint = `\n\nZOB HARNESS OPERATING CONTRACT\n- Prefer Explore -> Plan -> Implement -> Oracle for non-trivial work.\n- Use the six-part contract for delegated work: TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT.\n- Do not claim completion without concrete evidence.\n- If output may truncate, prioritize verdict, blockers, and next steps over exhaustive listings.\n\n${SAME_AGENT_MODE_INTENT_PROMPT}\n\n${ZOB_TOOL_ROUTING_CONTRACT}\n\n${ZOB_COMPACTION_CONTINUITY_CONTRACT}\n\n${EXTERNAL_PACKAGE_TOOLS_CONTRACT}\n\n${MODE_PROMPTS[state.activeMode]}${goalHint}${runtimeGoalHint}${rulesHint}${autonomyHint}${zagentHint}${zpeerHint}${activeSearchBackendHint}`;
|
|
1000
1002
|
return { systemPrompt: `${event.systemPrompt}${contractHint}` };
|
|
1001
1003
|
});
|
|
1002
1004
|
|
|
@@ -565,6 +565,15 @@ const ContextReadinessParams = Type.Object({
|
|
|
565
565
|
runId: Type.Optional(Type.String({ description: "Optional run id for the metadata-only Context/GBrain P0 readiness audit." })),
|
|
566
566
|
});
|
|
567
567
|
|
|
568
|
+
const ContextSearchParams = Type.Object({
|
|
569
|
+
query: Type.String({ description: "Bounded repo-local context search query. Required for all modes." }),
|
|
570
|
+
mode: Type.Optional(StringEnum(["auto", "semantic", "hybrid", "regex", "files"] as const, { description: "Search mode. auto/semantic/hybrid prefer ColGREP when ready; regex/files use deterministic fallback.", default: "auto" })),
|
|
571
|
+
pattern: Type.Optional(Type.String({ description: "Optional regex pattern used when mode=regex. Defaults to query." })),
|
|
572
|
+
paths: Type.Optional(Type.Array(Type.String(), { description: "Optional repo-relative search roots. Forbidden/session/vendor/build paths are rejected by the helper." })),
|
|
573
|
+
max_results: Type.Optional(Type.Number({ description: "Maximum result count. Runtime clamps to safe bounds." })),
|
|
574
|
+
max_context_lines: Type.Optional(Type.Number({ description: "Context lines around fallback matches. Runtime clamps to safe bounds." })),
|
|
575
|
+
});
|
|
576
|
+
|
|
568
577
|
const ContextScopeValidateParams = Type.Object({
|
|
569
578
|
runId: Type.String({ description: "Run id requiring a context_scope before lookup/context-pack injection." }),
|
|
570
579
|
scopeId: Type.Optional(Type.String({ description: "Optional deterministic context scope id." })),
|
|
@@ -685,6 +694,7 @@ export {
|
|
|
685
694
|
MissionControlProposeCommandParams,
|
|
686
695
|
MissionControlSnapshotParams,
|
|
687
696
|
ContextReadinessParams,
|
|
697
|
+
ContextSearchParams,
|
|
688
698
|
ContextScopeValidateParams,
|
|
689
699
|
ContextWritebackProposalParams,
|
|
690
700
|
ProjectDnaReadinessParams,
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ContextReadinessParams,
|
|
5
|
+
ContextSearchParams,
|
|
5
6
|
ContextScopeValidateParams,
|
|
6
7
|
ContextWritebackProposalParams,
|
|
7
8
|
} from "./schemas.js";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
validateContextScope,
|
|
12
13
|
writeContextWritebackProposal,
|
|
13
14
|
} from "../domains/context/context-gbrain.js";
|
|
15
|
+
import { formatContextSearchResult, runContextSearch } from "../domains/context/context-discovery.js";
|
|
14
16
|
|
|
15
17
|
export function registerContextTools(pi: ExtensionAPI): void {
|
|
16
18
|
pi.registerTool({
|
|
@@ -24,6 +26,24 @@ export function registerContextTools(pi: ExtensionAPI): void {
|
|
|
24
26
|
},
|
|
25
27
|
});
|
|
26
28
|
|
|
29
|
+
pi.registerTool({
|
|
30
|
+
name: "zob_context_search",
|
|
31
|
+
label: "ZOB Context Search",
|
|
32
|
+
description: "Search bounded repo-local context through the active context backend. Prefers ColGREP when installed and ready, falls back to safe grep/find-style search, never auto-installs ColGREP, and excludes forbidden/session/vendor/build paths.",
|
|
33
|
+
promptSnippet: "Use zob_context_search for bounded repo-local discovery before broad grep/read; verify exact proof with grep/read on returned refs.",
|
|
34
|
+
promptGuidelines: [
|
|
35
|
+
"Call zob_context_search for codebase/context discovery when semantic or broad search is useful.",
|
|
36
|
+
"zob_context_search prefers ColGREP when ready and falls back to bounded grep when ColGREP is missing, not ready, or a query fails.",
|
|
37
|
+
"Never install ColGREP from this tool path; missing ColGREP is not a blocker.",
|
|
38
|
+
"Use returned refs as leads and verify final claims with exact read/grep evidence.",
|
|
39
|
+
],
|
|
40
|
+
parameters: ContextSearchParams,
|
|
41
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
42
|
+
const result = runContextSearch(ctx.cwd, params);
|
|
43
|
+
return { content: [{ type: "text", text: formatContextSearchResult(result) }], details: result };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
27
47
|
pi.registerTool({
|
|
28
48
|
name: "zob_context_validate_scope",
|
|
29
49
|
label: "ZOB Context Scope Validate",
|