zob-harness 0.5.0 → 0.6.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/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/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
|
+
}
|
|
@@ -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",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zob-context-discovery
|
|
3
|
+
description: Use when running, documenting, or reviewing adaptive active search backend context discovery, including zob_context_search, optional ColGREP setup, and grep/find/read fallback behavior.
|
|
4
|
+
---
|
|
5
|
+
# ZOB Context Discovery Skill
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
Use this skill for:
|
|
10
|
+
- `zob_context_search` and `zob_context_*` discovery workflows.
|
|
11
|
+
- Active search backend guidance in prompts, docs, or registry entries.
|
|
12
|
+
- Optional ColGREP setup/doctor/query UX.
|
|
13
|
+
- Reviews of context discovery safety, bounded search output, and exact evidence refs.
|
|
14
|
+
|
|
15
|
+
## Active backend rules
|
|
16
|
+
|
|
17
|
+
1. Prefer `zob_context_search` for repo-local discovery when the runtime tool is available.
|
|
18
|
+
2. When ColGREP is installed and ready, use it as the preferred broad/semantic discovery backend.
|
|
19
|
+
3. Always use grep/find/read or exact file refs for verification before making claims.
|
|
20
|
+
4. When ColGREP is missing, unavailable, or not indexed, fall back to grep/find/read. Missing ColGREP is not a blocker for normal ZOB work.
|
|
21
|
+
5. Do not auto-install ColGREP, run network/package-manager installer commands, or mutate user tooling without explicit owner approval.
|
|
22
|
+
6. Keep search bounded to repo-local allowed paths and task-relevant globs.
|
|
23
|
+
7. Never read forbidden paths or secret-like files, including `.env`, `**/.env`, `**/*secret*`, `**/*key*`, private keys, `.pi/sessions`, `.pi/agent-sessions`, `node_modules`, `dist`, or `build`.
|
|
24
|
+
8. Persist only safe metadata/artifact refs for context packs. Do not persist raw secret/session bodies.
|
|
25
|
+
|
|
26
|
+
## User setup and scripts
|
|
27
|
+
|
|
28
|
+
- `npm run zob:context:doctor` checks the active backend, reports config/status, and prints install/setup guidance without installing anything.
|
|
29
|
+
- `npm run zob:context:init` may initialize safe ColGREP settings/indexing only when ColGREP is already installed and the owner runs it deliberately.
|
|
30
|
+
- `npm run zob:context:query -- <query>` runs a one-shot context query, preferring ColGREP when ready and using grep fallback otherwise.
|
|
31
|
+
- `npm run smoke:context-discovery` validates deterministic fallback behavior and should pass even when ColGREP is absent.
|
|
32
|
+
|
|
33
|
+
## Prompt injection posture
|
|
34
|
+
|
|
35
|
+
- Active-backend prompt injection is controlled by `.pi/context-discovery.json` under `promptInjection.enabled`.
|
|
36
|
+
- The injected block must stay concise, current-repo scoped, and bounded by the configured include/exclude roots; it is a discovery hint, not a context pack or evidence source.
|
|
37
|
+
- Do not inject stale/global context or raw search results into the prompt. Use `zob_context_search` and then read exact files when details are needed.
|
|
38
|
+
|
|
39
|
+
## Evidence expectations
|
|
40
|
+
|
|
41
|
+
- Cite repo-relative paths and line refs when available.
|
|
42
|
+
- Treat semantic/broad search hits as leads until exact grep/read verification confirms the behavior.
|
|
43
|
+
- Include provider/fallback metadata in readiness claims when relevant.
|
|
44
|
+
- If context discovery cannot search a required path because of scope or forbidden-path policy, report a blocker instead of broadening silently.
|
|
45
|
+
|
|
46
|
+
## Oracle / no-ship criteria
|
|
47
|
+
|
|
48
|
+
No-ship for context discovery if any of these remain true:
|
|
49
|
+
- forbidden sources or secret/session/vendor/build paths are read, indexed intentionally, or returned as results;
|
|
50
|
+
- ColGREP setup requires unapproved network/package-manager/installer commands;
|
|
51
|
+
- missing ColGREP blocks normal ZOB operation instead of using fallback;
|
|
52
|
+
- dynamic prompt injection includes stale/global context, raw search bodies, or unbounded output;
|
|
53
|
+
- implementation claims rely on semantic hits without exact grep/read/file-ref verification;
|
|
54
|
+
- context freshness or citation coverage cannot be shown for files used as evidence.
|
|
@@ -14,7 +14,7 @@ Use this skill for any task involving:
|
|
|
14
14
|
- Software-factory design from repeated manual workflows.
|
|
15
15
|
- Runtime tool/command routing via `.pi/capabilities/zob-public-runtime-capabilities.json`.
|
|
16
16
|
|
|
17
|
-
For routing behavior, load `zob-tool-router` before non-trivial or tool-ambiguous work. For compaction/recovery behavior, load `zob-compaction-policy` before changing compaction hooks or resuming from a compacted long-running goal. For domain behavior, load the domain skill named by the registry instead of inlining details here: `zob-goal-todo-tree`, `zob-coms-v2-live`, `zob-coms-safety`, `zob-mission-control-coms`, `zob-autonomous-runtime`, `zob-factory`, `zob-sandbox`, `zob-oracle`, or `zob-spec` as applicable.
|
|
17
|
+
For routing behavior, load `zob-tool-router` before non-trivial or tool-ambiguous work. For compaction/recovery behavior, load `zob-compaction-policy` before changing compaction hooks or resuming from a compacted long-running goal. For active context discovery/search backend behavior, load `zob-context-discovery` and prefer `zob_context_search` when available, with ColGREP as the preferred ready backend and grep/find/read as exact-verification fallback. For domain behavior, load the domain skill named by the registry instead of inlining details here: `zob-goal-todo-tree`, `zob-coms-v2-live`, `zob-coms-safety`, `zob-mission-control-coms`, `zob-autonomous-runtime`, `zob-factory`, `zob-sandbox`, `zob-oracle`, or `zob-spec` as applicable.
|
|
18
18
|
|
|
19
19
|
## Agent Factory posture
|
|
20
20
|
|
|
@@ -34,7 +34,7 @@ Communication is a core deliverable. Prefer one parent-visible control room by d
|
|
|
34
34
|
1. Classify the task as one of: `explore`, `plan`, `implement`, `oracle`, `factory`, `orchestrator`. When `.pi/routing/intent-classifier.json` enables an optional model classifier, treat it as advisory intent routing only; regex fallback and deterministic safety hard-blocks remain authoritative. `autoSwitchIntents` controls which detected intents switch mode directly; this project enables all ZOB modes by default. Use `/intent-classifier status|regex|model-strict|model-fallback|test` (alias `/intent`) to switch/test routing without editing JSON by hand.
|
|
35
35
|
2. For non-trivial or tool-ambiguous work, apply `zob-tool-router`: classify applicable families, then use/delegate/skip each with a reason.
|
|
36
36
|
3. Use `orchestrator` when the task needs Chief Vision coordination, multi-agent decomposition, Lead/Worker routing, goal/TODO graph governance, or parent-owned dispatch; the root should delegate substantive work rather than do it directly.
|
|
37
|
-
4. Check `.pi/capabilities/zob-public-runtime-capabilities.json` for the relevant tool/command family, mode allowlist, skill refs, and no-ship notes.
|
|
37
|
+
4. Check `.pi/capabilities/zob-public-runtime-capabilities.json` for the relevant tool/command family, mode allowlist, skill refs, and no-ship notes. For context tasks, prefer the active backend via `zob_context_search`; use ColGREP when ready, and grep/find/read for fallback plus exact verification.
|
|
38
38
|
5. If broad or risky, use the `delegate_agent` tool before editing.
|
|
39
39
|
6. For delegated work, use the six-part contract:
|
|
40
40
|
- TASK
|
package/AGENTS.md
CHANGED
|
@@ -49,7 +49,7 @@ Use the registry plus the domain skill instead of copying tool docs into prompts
|
|
|
49
49
|
- Delegation/catalog: `zob_delegation_catalog`, `delegate_agent`, `delegate_task` -> `.pi/skills/zob-delegation-routing/SKILL.md` and `.pi/skills/zob-harness/SKILL.md`; call the catalog before first delegation when agent/contract/tool routing is uncertain, normally omit `delegate_task.output_contract` and `delegate_task.required_tools`, and never invent contract IDs or agent tools.
|
|
50
50
|
- Live coms: `zob_coms_*` -> `.pi/skills/zob-coms-v2-live/SKILL.md` and `.pi/skills/zob-coms-safety/SKILL.md`; required-local delivery must be live, never append-only success.
|
|
51
51
|
- Mission Control: `zob_mission_control_*` / readiness -> `.pi/skills/zob-mission-control-coms/SKILL.md`; command writes are proposals only.
|
|
52
|
-
- Context: `zob_context_*` -> registry no-ship notes plus `.pi/skills/zob-
|
|
52
|
+
- Context: `zob_context_search`, `zob_context_*` -> registry no-ship notes plus `.pi/skills/zob-context-discovery/SKILL.md`, `.pi/skills/zob-harness/SKILL.md`, and `.pi/skills/zob-spec/SKILL.md` as applicable; prefer the active search backend (ColGREP when ready, grep/find/read fallback) and verify exact claims with file refs.
|
|
53
53
|
- Compute profile / effort routing: `zob_compute_*`, `npm run preview:compute-profile:project-dna-smoke`, `npm run validate:compute-profile:project-dna-smoke` -> `.pi/skills/zob-compute-profile/SKILL.md`; preview/resolve/report tools are metadata-only and never bypass safety, budget, oracle, sandbox, or parent-owned dispatch gates.
|
|
54
54
|
- Autonomy: `zob_autonomous_*` -> `.pi/skills/zob-autonomous-runtime/SKILL.md`; dry-run/readonly smoke/validation are supervised evidence only, not global autonomy completion.
|
|
55
55
|
- Factory quarantine/run: `factory_quarantine_*`, `factory_run` -> `.pi/skills/zob-factory/SKILL.md` and `.pi/skills/zob-sandbox/SKILL.md`.
|
package/README.md
CHANGED
|
@@ -330,9 +330,22 @@ Durable records stay hash-only (`bodyStored=false` with TODO refs, receiver refs
|
|
|
330
330
|
|
|
331
331
|
This release adds the handoff runtime/docs and `npm run smoke:goal-todo-handoff` validation. It does not auto-launch teams or auto-complete parent TODOs. Current npm release automation is handled separately by the CI/CD flow below; local agents still must not create tags, publish packages, commit, or push unless explicitly authorized through the governed workflow.
|
|
332
332
|
|
|
333
|
-
### Use
|
|
333
|
+
### Use active context discovery
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
ZOB can adapt context search to the active backend. Use `zob_context_search` inside Pi when available: it prefers ColGREP for broad/semantic repo discovery when ColGREP is installed and indexed, and falls back to grep/find/read when it is missing or unavailable. Treat broad search hits as leads and verify exact claims with file refs before editing or reporting readiness.
|
|
336
|
+
|
|
337
|
+
ColGREP setup is optional and owner-driven; ZOB must not auto-install it or run installer/package-manager commands. Local helpers:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm run zob:context:doctor # read-only backend/config status and setup guidance
|
|
341
|
+
npm run zob:context:init # initialize safe ColGREP settings/index only if ColGREP is already installed
|
|
342
|
+
npm run zob:context:query -- "goal todo routing" # one-shot query with grep fallback
|
|
343
|
+
npm run smoke:context-discovery # deterministic fallback smoke; passes without ColGREP
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Forbidden/secret/session/vendor/build paths remain excluded from discovery. Active-backend prompt injection is bounded and configurable through `.pi/context-discovery.json` (`promptInjection.enabled`); it should never inject stale/global context or raw search results. Oracle/no-ship review for this feature checks context freshness, citation coverage, exact grep/read verification, forbidden-source violations, and no unapproved installer/network behavior.
|
|
347
|
+
|
|
348
|
+
See [`reports/context-discovery/design.md`](reports/context-discovery/design.md), [`.pi/skills/zob-context-discovery/SKILL.md`](.pi/skills/zob-context-discovery/SKILL.md), and [scripts/README.md](scripts/README.md) for the operating rules and script map.
|
|
336
349
|
|
|
337
350
|
### Use governed commits
|
|
338
351
|
|
|
@@ -430,6 +443,7 @@ npm run check -- --pretty false # TypeScript validation baseline
|
|
|
430
443
|
npm run check:ci # CI-style TypeScript check
|
|
431
444
|
npm run validate:script-surface # package script/file surface validation
|
|
432
445
|
npm run smoke:harness # path-policy + child-goal-ref smoke
|
|
446
|
+
npm run smoke:context-discovery # active context backend smoke with grep fallback
|
|
433
447
|
npm run smoke:goal-todo-handoff # Goal TODO ZPeer/ZTeam handoff static smoke
|
|
434
448
|
npm run smoke:intent-classifier # optional model intent-classifier fallback smoke
|
|
435
449
|
npm run smoke:git-ops # governed commit policy smoke
|
|
@@ -437,6 +451,9 @@ npm run smoke:worker-pool # worker-pool static smoke
|
|
|
437
451
|
npm run smoke:zpeer # static + local ZPeer smoke
|
|
438
452
|
npm run validate:project-dna # ProjectDNA scaffold validation
|
|
439
453
|
npm run pack:dry-run # npm package dry-run surface check
|
|
454
|
+
npm run zob:context:doctor # active context backend status/guidance
|
|
455
|
+
npm run zob:context:init # optional ColGREP init when already installed
|
|
456
|
+
npm run zob:context:query -- "..." # one-shot active backend query
|
|
440
457
|
npm run demo:pacman:prepare # prepare Pac-Man factory run artifacts
|
|
441
458
|
npm run demo:pacman:validate # validate Pac-Man factory run artifacts
|
|
442
459
|
npm run demo:pacman # launch the full Pac-Man Agent Factory demo
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
".pi/autonomy-policy.json",
|
|
47
47
|
".pi/budget-policy.json",
|
|
48
48
|
".pi/capabilities",
|
|
49
|
+
".pi/context-discovery.json",
|
|
49
50
|
".pi/chains",
|
|
50
51
|
".pi/compute-profiles",
|
|
51
52
|
".pi/daemon-policy.json",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"scripts/agentic-spec-team",
|
|
78
79
|
"scripts/autonomy",
|
|
79
80
|
"scripts/compute-profile",
|
|
81
|
+
"scripts/context-discovery",
|
|
80
82
|
"scripts/git-ops",
|
|
81
83
|
"scripts/goal-todo",
|
|
82
84
|
"scripts/intent-classifier",
|
|
@@ -114,6 +116,10 @@
|
|
|
114
116
|
"smoke:autonomy-readiness-secret": "node scripts/autonomy/mission-readiness-secret-smoke.mjs",
|
|
115
117
|
"smoke:zpeer": "node scripts/zpeer-static-smoke.mjs && node scripts/zpeer-local-e2e-smoke.mjs",
|
|
116
118
|
"smoke:zagent": "node scripts/zagent-static-smoke.mjs",
|
|
119
|
+
"zob:context:doctor": "node scripts/context-discovery/doctor.mjs",
|
|
120
|
+
"zob:context:init": "node scripts/context-discovery/init-colgrep.mjs",
|
|
121
|
+
"zob:context:query": "node scripts/context-discovery/query.mjs",
|
|
122
|
+
"smoke:context-discovery": "node scripts/context-discovery/smoke.mjs",
|
|
117
123
|
"validate:script-surface": "node scripts/package-surface/validate-script-refs.mjs",
|
|
118
124
|
"pack:dry-run": "npm pack --dry-run --json",
|
|
119
125
|
"release:preview": "node scripts/release/preview.mjs",
|
package/scripts/README.md
CHANGED
|
@@ -8,6 +8,7 @@ These script families are intended to be part of the normal tracked repo workflo
|
|
|
8
8
|
|
|
9
9
|
- `scripts/autonomy/` — static/read-only autonomy readiness smokes.
|
|
10
10
|
- `scripts/compute-profile/` — compute profile policy and regression checks.
|
|
11
|
+
- `scripts/context-discovery/` — adaptive active search backend helpers: `npm run zob:context:doctor`, `npm run zob:context:init`, `npm run zob:context:query`, and `npm run smoke:context-discovery`; prefers ColGREP when installed/ready and validates grep fallback when it is absent.
|
|
11
12
|
- `scripts/git-ops/` — governed `/zcommit` policy smokes.
|
|
12
13
|
- `scripts/goal-todo/` — Goal/TODO tree compatibility smokes, including `scripts/goal-todo/handoff-static-smoke.mjs` coverage for the Goal TODO ZPeer/ZTeam handoff script (`npm run smoke:goal-todo-handoff`).
|
|
13
14
|
- `scripts/harness-intake/` — natural-language harness setup/session analyzer that produces quarantined ZOB team/factory proposals plus tmux launch support.
|
|
@@ -53,6 +54,7 @@ Common safe checks:
|
|
|
53
54
|
```bash
|
|
54
55
|
npm run validate:script-surface
|
|
55
56
|
npm run validate:capability-refs
|
|
57
|
+
npm run smoke:context-discovery
|
|
56
58
|
npm run check -- --pretty false
|
|
57
59
|
npm run smoke:harness
|
|
58
60
|
```
|
|
@@ -63,4 +65,6 @@ Run `npm run release:preview` before an authorized `/zcommit push` when you want
|
|
|
63
65
|
|
|
64
66
|
Run `npm run validate:capability-refs` after changing `.pi/capabilities/zob-public-runtime-capabilities.json`, skills, docs, or runtime capability references. It verifies registry doc/skill refs resolve to repo-local files.
|
|
65
67
|
|
|
68
|
+
Run `npm run smoke:context-discovery` after changing context-discovery docs, skills, config, or helpers. It is expected to pass without ColGREP by proving the grep fallback path.
|
|
69
|
+
|
|
66
70
|
Run additional domain-specific smokes only when their source files are present and the task requires them.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { detectColgrep, loadConfig, printJson } from "./shared.mjs";
|
|
3
|
+
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
const colgrep = detectColgrep();
|
|
6
|
+
const report = {
|
|
7
|
+
ok: true,
|
|
8
|
+
mode: colgrep.ready ? "colgrep-ready" : "grep-fallback",
|
|
9
|
+
colgrep,
|
|
10
|
+
config: {
|
|
11
|
+
loadedFrom: config.loadedFrom,
|
|
12
|
+
includePaths: config.includePaths,
|
|
13
|
+
excludePaths: config.excludePaths,
|
|
14
|
+
limits: config.limits,
|
|
15
|
+
promptInjection: config.promptInjection,
|
|
16
|
+
},
|
|
17
|
+
guidance: colgrep.guidance,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (process.argv.includes("--json")) {
|
|
21
|
+
printJson(report);
|
|
22
|
+
} else {
|
|
23
|
+
console.log("ZOB context discovery doctor");
|
|
24
|
+
console.log(`mode: ${report.mode}`);
|
|
25
|
+
console.log(`config: ${config.loadedFrom}`);
|
|
26
|
+
console.log(`includePaths: ${config.includePaths.join(", ")}`);
|
|
27
|
+
console.log(`excludePaths: ${config.excludePaths.join(", ")}`);
|
|
28
|
+
console.log(`promptInjection: enabled=${String(config.promptInjection.enabled)} includeInstallHint=${String(config.promptInjection.includeInstallHint)}`);
|
|
29
|
+
console.log(`guidance: ${report.guidance}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
process.exit(0);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { detectColgrep, loadConfig, printJson } from "./shared.mjs";
|
|
4
|
+
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const colgrep = detectColgrep();
|
|
7
|
+
|
|
8
|
+
if (!colgrep.installed) {
|
|
9
|
+
const report = {
|
|
10
|
+
ok: false,
|
|
11
|
+
action: "init-colgrep",
|
|
12
|
+
skipped: true,
|
|
13
|
+
reason: "colgrep-missing",
|
|
14
|
+
guidance: "ColGREP is not installed or not on PATH. This script will not auto-install it. Install ColGREP manually, then rerun npm run zob:context:init.",
|
|
15
|
+
};
|
|
16
|
+
if (process.argv.includes("--json")) {
|
|
17
|
+
printJson(report);
|
|
18
|
+
} else {
|
|
19
|
+
console.log(report.guidance);
|
|
20
|
+
}
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const settingsArgs = [
|
|
25
|
+
"settings",
|
|
26
|
+
"--relative-paths",
|
|
27
|
+
"--force-include",
|
|
28
|
+
".pi",
|
|
29
|
+
...config.excludePaths.flatMap((pattern) => ["--ignore", pattern]),
|
|
30
|
+
];
|
|
31
|
+
const settings = spawnSync("colgrep", settingsArgs, {
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const init = spawnSync("colgrep", ["init", "-y"], {
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ok = settings.status === 0 && init.status === 0;
|
|
44
|
+
const report = {
|
|
45
|
+
ok,
|
|
46
|
+
action: "init-colgrep",
|
|
47
|
+
skipped: false,
|
|
48
|
+
settingsStatus: settings.status,
|
|
49
|
+
initStatus: init.status,
|
|
50
|
+
stdout: [settings.stdout, init.stdout].filter(Boolean).join("\n").trim(),
|
|
51
|
+
stderr: [settings.stderr, init.stderr].filter(Boolean).join("\n").trim(),
|
|
52
|
+
guidance: ok ? "ColGREP settings/init completed." : "ColGREP exists but settings/init failed; inspect stdout/stderr and run colgrep help/status manually.",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (process.argv.includes("--json")) {
|
|
56
|
+
printJson(report);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(report.guidance);
|
|
59
|
+
if (report.stderr) {
|
|
60
|
+
console.log(report.stderr);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { detectColgrep, fallbackSearch, loadConfig, parseArgs, printHumanSearch, printJson } from "./shared.mjs";
|
|
4
|
+
|
|
5
|
+
const args = parseArgs(process.argv.slice(2));
|
|
6
|
+
const query = args.query ?? args.q ?? args._.join(" ");
|
|
7
|
+
if (!query) {
|
|
8
|
+
console.error("usage: npm run zob:context:query -- --query <text> [--max-results 20] [--max-context-lines 2] [--json]");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
const maxResults = Math.max(1, Math.min(Number(args["max-results"] ?? config.limits.maxResults ?? 20), 100));
|
|
14
|
+
const maxContextLines = Math.max(0, Math.min(Number(args["max-context-lines"] ?? config.limits.maxContextLines ?? 2), 5));
|
|
15
|
+
const colgrep = detectColgrep();
|
|
16
|
+
|
|
17
|
+
function runFallback(reason) {
|
|
18
|
+
return {
|
|
19
|
+
...fallbackSearch({ query, config, maxResults, maxContextLines }),
|
|
20
|
+
reason,
|
|
21
|
+
colgrep,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let result;
|
|
26
|
+
if (colgrep.ready) {
|
|
27
|
+
const colgrepArgs = ["--json", "-k", String(maxResults), "-n", String(maxContextLines), String(query), ...config.includePaths];
|
|
28
|
+
const colgrepResult = spawnSync("colgrep", colgrepArgs, {
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (colgrepResult.status === 0) {
|
|
35
|
+
result = {
|
|
36
|
+
provider: "colgrep",
|
|
37
|
+
fallback: false,
|
|
38
|
+
query,
|
|
39
|
+
resultCount: undefined,
|
|
40
|
+
raw: colgrepResult.stdout.trim(),
|
|
41
|
+
stderr: colgrepResult.stderr.trim(),
|
|
42
|
+
recommendedVerification: ["Use grep/read on returned repo-relative refs for exact proof."],
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
result = runFallback("colgrep-query-failed");
|
|
46
|
+
result.colgrepQueryStatus = colgrepResult.status;
|
|
47
|
+
result.colgrepQueryStderr = colgrepResult.stderr.trim();
|
|
48
|
+
result.colgrepArgs = colgrepArgs;
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
result = runFallback(colgrep.installed ? "colgrep-not-ready" : "colgrep-missing");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (args.json) {
|
|
55
|
+
printJson(result);
|
|
56
|
+
} else if (result.provider === "colgrep") {
|
|
57
|
+
console.log("provider: colgrep");
|
|
58
|
+
console.log(result.raw);
|
|
59
|
+
} else {
|
|
60
|
+
printHumanSearch(result);
|
|
61
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const repoRoot = process.cwd();
|
|
7
|
+
export const configPath = ".pi/context-discovery.json";
|
|
8
|
+
|
|
9
|
+
export const defaultConfig = {
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
preferredProvider: "colgrep",
|
|
12
|
+
fallbackProvider: "grep",
|
|
13
|
+
includePaths: [
|
|
14
|
+
".pi/extensions",
|
|
15
|
+
".pi/skills",
|
|
16
|
+
".pi/capabilities",
|
|
17
|
+
"scripts",
|
|
18
|
+
"docs",
|
|
19
|
+
"README.md",
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
],
|
|
22
|
+
excludePaths: [
|
|
23
|
+
".env",
|
|
24
|
+
"**/.env",
|
|
25
|
+
".env.*",
|
|
26
|
+
"**/*secret*",
|
|
27
|
+
"**/*key*",
|
|
28
|
+
"*.pem",
|
|
29
|
+
".pi/sessions",
|
|
30
|
+
".pi/agent-sessions",
|
|
31
|
+
"node_modules",
|
|
32
|
+
"dist",
|
|
33
|
+
"build",
|
|
34
|
+
],
|
|
35
|
+
limits: {
|
|
36
|
+
maxResults: 20,
|
|
37
|
+
maxContextLines: 2,
|
|
38
|
+
maxFileBytes: 1024 * 1024,
|
|
39
|
+
},
|
|
40
|
+
promptInjection: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
includeInstallHint: true,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function loadConfig() {
|
|
47
|
+
if (!existsSync(join(repoRoot, configPath))) {
|
|
48
|
+
return { ...defaultConfig, loadedFrom: "defaults" };
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(readFileSync(join(repoRoot, configPath), "utf8"));
|
|
51
|
+
return {
|
|
52
|
+
...defaultConfig,
|
|
53
|
+
...parsed,
|
|
54
|
+
includePaths: Array.isArray(parsed.includePaths) ? parsed.includePaths : defaultConfig.includePaths,
|
|
55
|
+
excludePaths: Array.isArray(parsed.excludePaths) ? parsed.excludePaths : defaultConfig.excludePaths,
|
|
56
|
+
limits: { ...defaultConfig.limits, ...(parsed.limits ?? {}) },
|
|
57
|
+
promptInjection: { ...defaultConfig.promptInjection, ...(parsed.promptInjection ?? {}) },
|
|
58
|
+
loadedFrom: configPath,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shellQuote(value) {
|
|
63
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function commandExists(command) {
|
|
67
|
+
if (process.env.ZOB_CONTEXT_FORCE_FALLBACK === "1") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const result = spawnSync("sh", ["-c", `command -v ${shellQuote(command)}`], {
|
|
71
|
+
cwd: repoRoot,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function detectColgrep() {
|
|
79
|
+
const installed = commandExists("colgrep");
|
|
80
|
+
if (!installed) {
|
|
81
|
+
return {
|
|
82
|
+
provider: "grep-fallback",
|
|
83
|
+
installed: false,
|
|
84
|
+
ready: false,
|
|
85
|
+
guidance: "ColGREP is not on PATH. Install/setup it manually if desired, then run npm run zob:context:init. Fallback search remains active.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const status = spawnSync("colgrep", ["status"], {
|
|
90
|
+
cwd: repoRoot,
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
provider: status.status === 0 ? "colgrep" : "grep-fallback",
|
|
97
|
+
installed: true,
|
|
98
|
+
ready: status.status === 0,
|
|
99
|
+
statusCode: status.status,
|
|
100
|
+
stdout: status.stdout?.trim() ?? "",
|
|
101
|
+
stderr: status.stderr?.trim() ?? "",
|
|
102
|
+
guidance: status.status === 0
|
|
103
|
+
? "ColGREP detected and status check passed."
|
|
104
|
+
: "ColGREP is installed but not ready/indexed. Run npm run zob:context:init or inspect colgrep status output.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseArgs(argv) {
|
|
109
|
+
const args = { _: [] };
|
|
110
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
111
|
+
const token = argv[index];
|
|
112
|
+
if (!token.startsWith("--")) {
|
|
113
|
+
args._.push(token);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const key = token.slice(2);
|
|
117
|
+
const next = argv[index + 1];
|
|
118
|
+
if (!next || next.startsWith("--")) {
|
|
119
|
+
args[key] = true;
|
|
120
|
+
} else {
|
|
121
|
+
args[key] = next;
|
|
122
|
+
index += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return args;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeRepoPath(raw) {
|
|
129
|
+
const normalized = normalize(String(raw).replace(/^\.\//u, ""));
|
|
130
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith(`..${sep}`)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return normalized.split(sep).join("/");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function globToRegExp(pattern) {
|
|
137
|
+
const escaped = pattern
|
|
138
|
+
.split("*")
|
|
139
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&"))
|
|
140
|
+
.join(".*");
|
|
141
|
+
return new RegExp(`^${escaped}$`, "iu");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isExcluded(relPath, excludePaths) {
|
|
145
|
+
const normalized = normalizeRepoPath(relPath);
|
|
146
|
+
if (!normalized) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return excludePaths.some((pattern) => {
|
|
150
|
+
const clean = normalizeRepoPath(pattern) ?? pattern;
|
|
151
|
+
if (clean.includes("*")) {
|
|
152
|
+
return globToRegExp(clean).test(normalized) || globToRegExp(clean.replace(/^\*\*\//u, "")).test(basename(normalized));
|
|
153
|
+
}
|
|
154
|
+
return normalized === clean || normalized.startsWith(`${clean}/`) || basename(normalized) === clean;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function looksTextFile(relPath) {
|
|
159
|
+
const textExts = new Set(["", ".cjs", ".css", ".js", ".json", ".md", ".mjs", ".sh", ".ts", ".tsx", ".txt", ".yaml", ".yml"]);
|
|
160
|
+
return textExts.has(extname(relPath).toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectFiles(startRel, config, out) {
|
|
164
|
+
const safeRel = normalizeRepoPath(startRel);
|
|
165
|
+
if (!safeRel || isExcluded(safeRel, config.excludePaths)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const absolute = join(repoRoot, safeRel);
|
|
169
|
+
if (!existsSync(absolute)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const stat = statSync(absolute);
|
|
173
|
+
if (stat.isFile()) {
|
|
174
|
+
if (stat.size <= config.limits.maxFileBytes && looksTextFile(safeRel)) {
|
|
175
|
+
out.push(safeRel);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!stat.isDirectory()) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const entry of readdirSync(absolute, { withFileTypes: true })) {
|
|
183
|
+
collectFiles(join(safeRel, entry.name), config, out);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function fallbackSearch({ query, config, maxResults, maxContextLines }) {
|
|
188
|
+
const files = [];
|
|
189
|
+
for (const includePath of config.includePaths) {
|
|
190
|
+
collectFiles(includePath, config, files);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const wanted = String(query ?? "").toLowerCase();
|
|
194
|
+
const results = [];
|
|
195
|
+
for (const relPath of [...new Set(files)].sort()) {
|
|
196
|
+
if (results.length >= maxResults) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const content = readFileSync(join(repoRoot, relPath), "utf8");
|
|
200
|
+
const lines = content.split(/\r?\n/u);
|
|
201
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
202
|
+
if (!lines[lineIndex].toLowerCase().includes(wanted)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const start = Math.max(0, lineIndex - maxContextLines);
|
|
206
|
+
const end = Math.min(lines.length, lineIndex + maxContextLines + 1);
|
|
207
|
+
results.push({
|
|
208
|
+
path: relPath,
|
|
209
|
+
line: lineIndex + 1,
|
|
210
|
+
ref: `${relPath}:${lineIndex + 1}`,
|
|
211
|
+
preview: lines[lineIndex].trim().slice(0, 240),
|
|
212
|
+
context: lines.slice(start, end).map((text, offset) => ({ line: start + offset + 1, text: text.slice(0, 240) })),
|
|
213
|
+
});
|
|
214
|
+
if (results.length >= maxResults) {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
provider: "grep-fallback",
|
|
222
|
+
fallback: true,
|
|
223
|
+
query,
|
|
224
|
+
maxResults,
|
|
225
|
+
maxContextLines,
|
|
226
|
+
resultCount: results.length,
|
|
227
|
+
results,
|
|
228
|
+
recommendedVerification: results.length
|
|
229
|
+
? [`grep -n ${shellQuote(query)} ${shellQuote(results[0].path)}`, `read ${results[0].path}`]
|
|
230
|
+
: [`grep -R -n ${shellQuote(query)} ${config.includePaths.map(shellQuote).join(" ")}`],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function printJson(value) {
|
|
235
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function printHumanSearch(result) {
|
|
239
|
+
console.log(`provider: ${result.provider}`);
|
|
240
|
+
console.log(`fallback: ${result.fallback ? "yes" : "no"}`);
|
|
241
|
+
console.log(`results: ${result.resultCount}`);
|
|
242
|
+
for (const item of result.results ?? []) {
|
|
243
|
+
console.log(`- ${item.ref}: ${item.preview}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function repoRelative(path) {
|
|
248
|
+
return relative(repoRoot, join(repoRoot, path)).split(sep).join("/");
|
|
249
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const env = { ...process.env, ZOB_CONTEXT_FORCE_FALLBACK: "1" };
|
|
5
|
+
const result = spawnSync(process.execPath, ["scripts/context-discovery/query.mjs", "--query", "ZOB Harness", "--json", "--max-results", "5"], {
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
env,
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (result.status !== 0) {
|
|
13
|
+
console.error("context-discovery smoke FAIL: query exited non-zero");
|
|
14
|
+
console.error(result.stderr || result.stdout);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(result.stdout);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error("context-discovery smoke FAIL: query did not emit JSON");
|
|
23
|
+
console.error(result.stdout);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hasAllowedRef = Array.isArray(parsed.results) && parsed.results.some((entry) => typeof entry.path === "string" && !entry.path.includes(".pi/sessions") && !entry.path.includes(".pi/agent-sessions") && !entry.path.includes("node_modules"));
|
|
28
|
+
if (parsed.provider !== "grep-fallback" || parsed.fallback !== true || parsed.reason !== "colgrep-missing" || !hasAllowedRef) {
|
|
29
|
+
console.error("context-discovery smoke FAIL: fallback result did not match expectations");
|
|
30
|
+
console.error(JSON.stringify({ provider: parsed.provider, fallback: parsed.fallback, reason: parsed.reason, results: parsed.results }, null, 2));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("context-discovery smoke PASS");
|
|
35
|
+
console.log(`provider=${parsed.provider} reason=${parsed.reason} results=${parsed.resultCount}`);
|
|
36
|
+
console.log(`evidence=${parsed.results.find((entry) => typeof entry.path === "string")?.ref}`);
|