@haposoft/cafekit 0.15.1 → 0.16.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +230 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +553 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/lib/skill-deps.js +45 -7
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +41 -7
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/bin/phases/select-platform.js +23 -0
- package/bin/phases/skills-setup.js +7 -3
- package/package.json +15 -5
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
- package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
- package/src/claude/skills/pptx/scripts/package.json +1 -1
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/codex/hooks.json +14 -14
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Haposoft. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode Plugin — spec-gate.ts
|
|
5
|
+
*
|
|
6
|
+
* Advisory port of .claude/hooks/spec-gate.cjs and .codex/hooks/spec-gate.cjs.
|
|
7
|
+
*
|
|
8
|
+
* OpenCode has no Stop-hook equivalent that can hard-block a turn end.
|
|
9
|
+
* This plugin provides a best-effort advisory gate:
|
|
10
|
+
* - On tool.execute.after for writes to specs/<feature>/spec.json or task markdown, re-validate
|
|
11
|
+
* any newly-done task receipts (Verification: PASS, Command, Exit, provenance, completed_at).
|
|
12
|
+
* - On session.idle (≈ Claude Stop), run the same check and surface a banner + stderr warning.
|
|
13
|
+
*
|
|
14
|
+
* It never throws to break the turn (fail-open). Disable via `.opencode/runtime.json`
|
|
15
|
+
* `{ "spec": { "completion_gate": false } }`. Missing key keeps the advisory gate ON.
|
|
16
|
+
* This is tier-2 compared to Claude/Codex hard gates — docs must not claim parity.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
appendFileSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { join, dirname, relative, resolve, sep, isAbsolute } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const SPEC_GATE_BANNER_START = "<!-- CAFEKIT SPEC-GATE START -->";
|
|
33
|
+
const SPEC_GATE_BANNER_END = "<!-- CAFEKIT SPEC-GATE END -->";
|
|
34
|
+
|
|
35
|
+
type TaskEntry = {
|
|
36
|
+
status?: string;
|
|
37
|
+
completed_at?: string;
|
|
38
|
+
receipt?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type SpecJson = {
|
|
42
|
+
status?: string;
|
|
43
|
+
current_phase?: string;
|
|
44
|
+
phase?: string;
|
|
45
|
+
task_registry?: Record<string, TaskEntry>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const EVIDENCE_NAMES = ["Evidence", "Task Test Plan & Verification Evidence", "Verification & Evidence"];
|
|
49
|
+
const PASS_MARKER = /^\s*Verification:\s*PASS\s*$/m;
|
|
50
|
+
const LEGACY_SUCCESS = /^\s*(?:PASS(?:ED)?|✓)(?:\s*:|$)|exit\s+code\s*[:=]?\s*0\b/im;
|
|
51
|
+
const EXPLICIT_FAILURE = /\bFAIL(?:ED|URE|URES|ING)?\b|tests?\s+failed|exit\s+code\s*[:=]?\s*[1-9]\d*|\b0\s+tests?\b/i;
|
|
52
|
+
|
|
53
|
+
function readRuntime(cwd: string): Record<string, any> {
|
|
54
|
+
try {
|
|
55
|
+
const file = join(cwd, ".opencode", "runtime.json");
|
|
56
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function evidenceBody(text: string): string | null {
|
|
63
|
+
const lines = text.split("\n");
|
|
64
|
+
let start = -1;
|
|
65
|
+
let level = 0;
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const m = lines[i].match(/^(#{2,3})\s+(.+?)\s*$/);
|
|
68
|
+
if (m && EVIDENCE_NAMES.includes(m[2])) {
|
|
69
|
+
start = i + 1;
|
|
70
|
+
level = m[1].length;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (start < 0) return null;
|
|
75
|
+
let end = lines.length;
|
|
76
|
+
for (let i = start; i < lines.length; i++) {
|
|
77
|
+
const hm = lines[i].match(/^(#{1,6})\s+/);
|
|
78
|
+
if (hm && hm[1].length <= level) {
|
|
79
|
+
end = i;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.slice(start, end).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateCanonicalReceipt(body: string): string[] {
|
|
87
|
+
const fails: string[] = [];
|
|
88
|
+
if (!/^\s*Verification:\s*PASS\s*$/m.test(body)) fails.push("verification_state");
|
|
89
|
+
if (!/^\s*Command(?:\(s\))?\s*:/m.test(body)) fails.push("command");
|
|
90
|
+
if (!/^\s*Exit\s*:|exit\s+code\s*[:=]|\bResult\s*:\s*PASS\b/im.test(body)) fails.push("exit_result");
|
|
91
|
+
const hasBase = /^\s*Base\s*:[ \t]*\S/im.test(body);
|
|
92
|
+
const hasHead = /^\s*Head\s*:[ \t]*\S/im.test(body);
|
|
93
|
+
const hasBaseSha = /\bbase_sha\s*:[ \t]*\S/im.test(body);
|
|
94
|
+
const hasHeadSha = /\bhead_sha\s*:[ \t]*\S/im.test(body);
|
|
95
|
+
if (!((hasBase && hasHead) || (hasBaseSha && hasHeadSha))) fails.push("provenance");
|
|
96
|
+
if (/\bartifact\b/i.test(body) && !/sha256:/i.test(body)) fails.push("artifact_hash");
|
|
97
|
+
return fails;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkReceipt(featureDir: string, taskPath: string, task: TaskEntry): string[] {
|
|
101
|
+
const fails: string[] = [];
|
|
102
|
+
const abs = resolve(join(featureDir, taskPath));
|
|
103
|
+
const resolvedFeatureDir = resolve(featureDir);
|
|
104
|
+
const rel = relative(resolvedFeatureDir, abs);
|
|
105
|
+
// prevent path traversal: taskPath must stay inside featureDir (cross-platform sibling-prefix safe)
|
|
106
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return ["a"];
|
|
107
|
+
if (!existsSync(abs)) return ["a"];
|
|
108
|
+
let text: string;
|
|
109
|
+
try {
|
|
110
|
+
text = readFileSync(abs, "utf8");
|
|
111
|
+
} catch {
|
|
112
|
+
return ["a"];
|
|
113
|
+
}
|
|
114
|
+
const statusLine = text.split("\n").find((l) => /^\s*(?:\*\*)?Status(?:\*\*)?\s*:/i.test(l));
|
|
115
|
+
if (!statusLine || !/\bdone\b/i.test(statusLine)) fails.push("a");
|
|
116
|
+
const body = evidenceBody(text);
|
|
117
|
+
if (body === null) {
|
|
118
|
+
fails.push("b");
|
|
119
|
+
} else if (/\{\{[^}]+\}\}/.test(body) || EXPLICIT_FAILURE.test(body) || !(PASS_MARKER.test(body) || LEGACY_SUCCESS.test(body))) {
|
|
120
|
+
fails.push("c");
|
|
121
|
+
} else {
|
|
122
|
+
const canonical = validateCanonicalReceipt(body);
|
|
123
|
+
const map: Record<string, string> = { verification_state: "c", command: "e", exit_result: "f", provenance: "g", artifact_hash: "h" };
|
|
124
|
+
for (const f of canonical) {
|
|
125
|
+
if (f === "verification_state") {
|
|
126
|
+
if (!PASS_MARKER.test(body) && !fails.includes("c")) fails.push("c");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const letter = map[f];
|
|
130
|
+
if (letter && !fails.includes(letter)) fails.push(letter);
|
|
131
|
+
}
|
|
132
|
+
if (!PASS_MARKER.test(body) && !fails.includes("c")) fails.push("c");
|
|
133
|
+
}
|
|
134
|
+
const at = task?.completed_at;
|
|
135
|
+
if (typeof at !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(at) || Number.isNaN(Date.parse(at))) fails.push("d");
|
|
136
|
+
return fails;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findActiveSpec(specsPath: string): { featureName: string; spec: SpecJson; specsDir: string; featureDir: string } | null {
|
|
140
|
+
if (!existsSync(specsPath)) return null;
|
|
141
|
+
let entries;
|
|
142
|
+
try {
|
|
143
|
+
entries = readdirSync(specsPath, { withFileTypes: true });
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (!entry.isDirectory()) continue;
|
|
149
|
+
const specFile = join(specsPath, entry.name, "spec.json");
|
|
150
|
+
if (!existsSync(specFile)) continue;
|
|
151
|
+
try {
|
|
152
|
+
const data = JSON.parse(readFileSync(specFile, "utf8")) as SpecJson;
|
|
153
|
+
if (data.status === "in_progress" || data.status === "in-progress") {
|
|
154
|
+
return { featureName: entry.name, spec: data, specsDir: specsPath, featureDir: join(specsPath, entry.name) };
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// skip bad json
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isStaleFlashDone(task: TaskEntry): boolean {
|
|
164
|
+
return task?.status === "done" && task.receipt === "FLASH_UNVERIFIED";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function collectFailures(cwd: string): { featureName: string; failures: { taskPath: string; fails: string[] }[] } | null {
|
|
168
|
+
const runtime = readRuntime(cwd);
|
|
169
|
+
if (runtime.spec && runtime.spec.completion_gate === false) return null;
|
|
170
|
+
const specsPath = join(cwd, runtime.paths?.specs || "specs");
|
|
171
|
+
const active = findActiveSpec(specsPath);
|
|
172
|
+
if (!active) return null;
|
|
173
|
+
const registry = active.spec.task_registry || {};
|
|
174
|
+
const staleFlash = Object.entries(registry).filter(([, t]) => isStaleFlashDone(t as TaskEntry));
|
|
175
|
+
if (staleFlash.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
featureName: active.featureName,
|
|
178
|
+
failures: staleFlash.map(([p]) => ({ taskPath: p, fails: ["flash"] })),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Check every done task (advisory — we have no cache to know newly-done, so check all)
|
|
182
|
+
const failures: { taskPath: string; fails: string[] }[] = [];
|
|
183
|
+
for (const [taskPath, task] of Object.entries(registry)) {
|
|
184
|
+
if ((task as TaskEntry)?.status !== "done") continue;
|
|
185
|
+
const fails = checkReceipt(active.featureDir, taskPath, task as TaskEntry);
|
|
186
|
+
if (fails.length > 0) failures.push({ taskPath, fails });
|
|
187
|
+
}
|
|
188
|
+
if (failures.length === 0) return null;
|
|
189
|
+
return { featureName: active.featureName, failures };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatReason(featureName: string, failures: { taskPath: string; fails: string[] }[]): string {
|
|
193
|
+
const flashOnly = failures.length === 1 && failures[0].fails.includes("flash");
|
|
194
|
+
if (flashOnly) {
|
|
195
|
+
return `Completion gate (advisory): 1 task(s) marked done with FLASH_UNVERIFIED (${failures[0].taskPath}). Run /hapo:test for exact proof, then use explicit sync-finalize.`;
|
|
196
|
+
}
|
|
197
|
+
const lines = [`⚠️ Completion gate (advisory): ${failures.length} done task(s) lack a verification receipt.`];
|
|
198
|
+
for (const { taskPath, fails } of failures) {
|
|
199
|
+
if (fails.includes("flash")) {
|
|
200
|
+
lines.push(`- \`${taskPath}\`: FLASH_UNVERIFIED`);
|
|
201
|
+
} else {
|
|
202
|
+
lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(", ")}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.slice(0, 8).join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function writeBanner(cwd: string, reason: string | null): void {
|
|
209
|
+
try {
|
|
210
|
+
const file = join(cwd, ".opencode", "session-banner.md");
|
|
211
|
+
const dir = dirname(file);
|
|
212
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
213
|
+
let existing = "";
|
|
214
|
+
try {
|
|
215
|
+
existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
216
|
+
} catch {
|
|
217
|
+
existing = "";
|
|
218
|
+
}
|
|
219
|
+
const pattern = new RegExp(`${SPEC_GATE_BANNER_START}[\\s\\S]*?${SPEC_GATE_BANNER_END}\\n?`);
|
|
220
|
+
const cleaned = existing.replace(pattern, "").trimEnd();
|
|
221
|
+
if (!reason) {
|
|
222
|
+
if (cleaned !== existing.trimEnd()) writeFileSync(file, cleaned ? `${cleaned}\n` : "");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const block = [SPEC_GATE_BANNER_START, reason, SPEC_GATE_BANNER_END].join("\n");
|
|
226
|
+
const next = cleaned ? `${cleaned}\n\n${block}\n` : `${block}\n`;
|
|
227
|
+
writeFileSync(file, next);
|
|
228
|
+
} catch {
|
|
229
|
+
// fail-open
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function logCrash(error: unknown): void {
|
|
234
|
+
try {
|
|
235
|
+
const logDir = join(PLUGIN_DIR, ".logs");
|
|
236
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
237
|
+
appendFileSync(
|
|
238
|
+
join(logDir, "hook-log.jsonl"),
|
|
239
|
+
JSON.stringify({ ts: new Date().toISOString(), hook: "spec-gate", status: "crash", error: error instanceof Error ? error.message : String(error) }) + "\n",
|
|
240
|
+
);
|
|
241
|
+
} catch {
|
|
242
|
+
// fail-open
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const RELEVANT_TOOLS = new Set(["edit", "write", "apply_patch", "task", "taskcreate", "taskupdate", "todowrite"]);
|
|
247
|
+
|
|
248
|
+
export const SpecGate: Plugin = async ({ directory }) => ({
|
|
249
|
+
event: async ({ event }) => {
|
|
250
|
+
try {
|
|
251
|
+
if (event.type === "session.idle") {
|
|
252
|
+
const result = collectFailures(directory);
|
|
253
|
+
if (result) {
|
|
254
|
+
const reason = formatReason(result.featureName, result.failures);
|
|
255
|
+
writeBanner(directory, reason);
|
|
256
|
+
console.error(reason);
|
|
257
|
+
} else {
|
|
258
|
+
writeBanner(directory, null);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
logCrash(error);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"tool.execute.after": async (input) => {
|
|
266
|
+
try {
|
|
267
|
+
if (!RELEVANT_TOOLS.has(input.tool)) return;
|
|
268
|
+
const result = collectFailures(directory);
|
|
269
|
+
if (result) {
|
|
270
|
+
const reason = formatReason(result.featureName, result.failures);
|
|
271
|
+
writeBanner(directory, reason);
|
|
272
|
+
console.error(`[spec-gate advisory] ${reason}`);
|
|
273
|
+
} else {
|
|
274
|
+
// clear banner if no failures after a relevant tool
|
|
275
|
+
// (keep banner if failures still exist from other tasks — collectFailures already handles)
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
logCrash(error);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export default SpecGate;
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# External Discovery with Gemini
|
|
2
|
-
|
|
3
|
-
Use external Gemini CLI for faster searches with large context windows (1M+ tokens) when SCALE ≤ 5.
|
|
4
|
-
|
|
5
|
-
## Tool Selection
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
SCALE ≤ 5 → gemini CLI
|
|
9
|
-
SCALE ≥ 6 → Use internal discovery instead
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Configuration
|
|
13
|
-
|
|
14
|
-
Read from `.claude/runtime.json`:
|
|
15
|
-
```json
|
|
16
|
-
{
|
|
17
|
-
"gemini": {
|
|
18
|
-
"model": "gemma-4-31b-it"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Default model: `gemma-4-31b-it`
|
|
24
|
-
|
|
25
|
-
**Note:** This file is automatically installed when you run `npx @haposoft/cafekit`.
|
|
26
|
-
|
|
27
|
-
## When External Mode is Allowed
|
|
28
|
-
|
|
29
|
-
External Gemini inspection requires:
|
|
30
|
-
|
|
31
|
-
**Gemini-Specific Requirements:**
|
|
32
|
-
1. The prompt explicitly includes `ext` argument
|
|
33
|
-
2. SCALE ≤ 5 (calculated from estimated file count)
|
|
34
|
-
|
|
35
|
-
**General Preflight Gate (applies to all modes):**
|
|
36
|
-
3. At least one concrete directory scope is present
|
|
37
|
-
4. The request is not a repo-root or root-wide scan
|
|
38
|
-
5. The built-in no-scan guidance has been applied
|
|
39
|
-
|
|
40
|
-
If any condition fails, use internal discovery instead.
|
|
41
|
-
|
|
42
|
-
## Gemini CLI
|
|
43
|
-
|
|
44
|
-
### Command
|
|
45
|
-
```bash
|
|
46
|
-
gemini -y -m <model> "[prompt]"
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Example
|
|
50
|
-
```bash
|
|
51
|
-
gemini -y -m gemma-4-31b-it "Search src/ for authentication files. List paths with brief descriptions."
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Installation Check
|
|
55
|
-
|
|
56
|
-
Before using, verify Gemini CLI is installed:
|
|
57
|
-
```bash
|
|
58
|
-
which gemini
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
If not installed, ask user:
|
|
62
|
-
1. **Yes** - Provide installation instructions (may need manual auth steps)
|
|
63
|
-
2. **No** - Fall back to internal discovery (`internal-inspection.md`)
|
|
64
|
-
|
|
65
|
-
## Running Parallel Gemini Commands
|
|
66
|
-
|
|
67
|
-
Use the `Bash` tool for each scoped Gemini command. Run them in the same tool turn when the runtime supports parallel tool calls; otherwise run them sequentially. Do not use `Agent` with `subagent_type: "Bash"` — `Bash` is a tool, not a subagent.
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
Bash: gemini -y -m gemma-4-31b-it '[prompt1]'
|
|
71
|
-
Bash: gemini -y -m gemma-4-31b-it '[prompt2]'
|
|
72
|
-
Bash: gemini -y -m gemma-4-31b-it '[prompt3]'
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Group by independent scopes so each command can return a focused report.
|
|
76
|
-
|
|
77
|
-
## Prompt Guidelines
|
|
78
|
-
|
|
79
|
-
- Be specific about directories to search
|
|
80
|
-
- Request file paths with descriptions
|
|
81
|
-
- Set clear scope boundaries
|
|
82
|
-
- Ask for patterns/relationships if relevant
|
|
83
|
-
- Do not include no-scan paths
|
|
84
|
-
- Do not request secret material or generated bundles
|
|
85
|
-
|
|
86
|
-
## Prompt Template
|
|
87
|
-
|
|
88
|
-
```text
|
|
89
|
-
Inspect these scoped directories only:
|
|
90
|
-
- {dir-1}
|
|
91
|
-
- {dir-2}
|
|
92
|
-
|
|
93
|
-
Goal:
|
|
94
|
-
- {what to find}
|
|
95
|
-
|
|
96
|
-
Return:
|
|
97
|
-
1. relevant file paths
|
|
98
|
-
2. one-line description per file
|
|
99
|
-
3. notable patterns or relationships
|
|
100
|
-
4. unresolved questions
|
|
101
|
-
|
|
102
|
-
Do not inspect repo root, generated directories, or sensitive content.
|
|
103
|
-
Do not expand beyond the provided scope.
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Example Workflow
|
|
107
|
-
|
|
108
|
-
User: "Find database migration files" with `ext`
|
|
109
|
-
|
|
110
|
-
Run 3 scoped Gemini CLI commands:
|
|
111
|
-
```
|
|
112
|
-
Bash: gemini -y -m gemma-4-31b-it 'Search db/, migrations/ for migration files'
|
|
113
|
-
Bash: gemini -y -m gemma-4-31b-it 'Search lib/, src/ for database schema files'
|
|
114
|
-
Bash: gemini -y -m gemma-4-31b-it 'Search config/ for database configuration'
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Reading File Content
|
|
118
|
-
|
|
119
|
-
When needing to read file content, use chunking to stay within context limits (<150K tokens safe zone).
|
|
120
|
-
|
|
121
|
-
### Step 1: Get Line Counts
|
|
122
|
-
Use Read tool to check file size before reading.
|
|
123
|
-
|
|
124
|
-
### Step 2: Calculate Chunks
|
|
125
|
-
- **Target:** ~500 lines per chunk (safe for most files)
|
|
126
|
-
- **Max files per agent:** 3-5 small files OR 1 large file chunked
|
|
127
|
-
|
|
128
|
-
**Chunking formula:**
|
|
129
|
-
```
|
|
130
|
-
chunks = ceil(total_lines / 500)
|
|
131
|
-
lines_per_chunk = ceil(total_lines / chunks)
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Step 3: Read Strategy
|
|
135
|
-
|
|
136
|
-
**Small files (<500 lines each):**
|
|
137
|
-
- Read entire file with Read tool
|
|
138
|
-
|
|
139
|
-
**Large file (>500 lines):**
|
|
140
|
-
- Use Read tool with offset/limit parameters
|
|
141
|
-
- Read in 500-line chunks
|
|
142
|
-
|
|
143
|
-
### Chunking Decision Tree
|
|
144
|
-
```
|
|
145
|
-
File < 500 lines → Read entire file
|
|
146
|
-
File 500-1500 lines → Split into 2-3 chunks
|
|
147
|
-
File > 1500 lines → Split into ceil(lines/500) chunks
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Timeout and Error Handling
|
|
151
|
-
|
|
152
|
-
- Set 3-minute timeout per bash call
|
|
153
|
-
- Skip timed-out agents
|
|
154
|
-
- Don't restart failed agents
|
|
155
|
-
- On persistent failures, fall back to internal discovery
|
|
156
|
-
|
|
157
|
-
## Fallback Behavior
|
|
158
|
-
|
|
159
|
-
If Gemini is unavailable, misconfigured, or errors out:
|
|
160
|
-
1. Stop external mode
|
|
161
|
-
2. Continue with internal discovery
|
|
162
|
-
3. Note the fallback in the final report
|
|
163
|
-
|
|
164
|
-
## Expected Outcome
|
|
165
|
-
|
|
166
|
-
- Scoped Gemini discovery
|
|
167
|
-
- Concise file list with descriptions
|
|
168
|
-
- Patterns and relationships
|
|
169
|
-
- Fallback to internal mode if Gemini cannot run
|