@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.
Files changed (99) hide show
  1. package/README.md +6 -2
  2. package/bin/__tests__/codex-hooks.test.js +140 -0
  3. package/bin/__tests__/codex-native.test.js +230 -2
  4. package/bin/__tests__/develop-contract.test.js +1120 -0
  5. package/bin/__tests__/installer-safety.test.js +553 -2
  6. package/bin/__tests__/package-inventory.test.js +258 -0
  7. package/bin/__tests__/secret-scanner.test.js +157 -0
  8. package/bin/__tests__/validator-grounding.test.js +713 -0
  9. package/bin/install.js +5 -1
  10. package/bin/lib/codex-install.js +21 -6
  11. package/bin/lib/context.js +17 -6
  12. package/bin/lib/copy-utils.js +31 -0
  13. package/bin/lib/i18n.js +12 -3
  14. package/bin/lib/instruction-blocks.js +85 -0
  15. package/bin/lib/managed-writer.js +4 -1
  16. package/bin/lib/opencode-install.js +75 -30
  17. package/bin/lib/skill-deps.js +45 -7
  18. package/bin/phases/claude-runtime.js +103 -4
  19. package/bin/phases/codex-runtime.js +41 -7
  20. package/bin/phases/post-install.js +29 -22
  21. package/bin/phases/root-config.js +89 -10
  22. package/bin/phases/select-platform.js +23 -0
  23. package/bin/phases/skills-setup.js +7 -3
  24. package/package.json +15 -5
  25. package/src/claude/CLAUDE.md +6 -122
  26. package/src/claude/agents/code-auditor.md +21 -22
  27. package/src/claude/agents/debugger.md +3 -3
  28. package/src/claude/agents/deployer.md +1 -1
  29. package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
  30. package/src/claude/agents/project-manager.md +1 -1
  31. package/src/claude/agents/spec-maker.md +2 -2
  32. package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
  33. package/src/claude/hooks/agent.cjs +7 -14
  34. package/src/claude/hooks/rules.cjs +33 -53
  35. package/src/claude/hooks/spec-gate.cjs +66 -21
  36. package/src/claude/hooks/spec-state.cjs +5 -0
  37. package/src/claude/migration-manifest.json +9 -2
  38. package/src/claude/runtime.json +0 -3
  39. package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
  40. package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
  41. package/src/claude/scripts/spec-ground.cjs +189 -83
  42. package/src/claude/scripts/validate-spec-output.cjs +294 -30
  43. package/src/claude/scripts/workflow-policy.cjs +609 -0
  44. package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
  45. package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
  46. package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
  47. package/src/claude/skills/backend-development/SKILL.md +4 -4
  48. package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
  49. package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
  50. package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
  51. package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
  52. package/src/claude/skills/backend-development/references/backend-security.md +3 -3
  53. package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
  54. package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
  55. package/src/claude/skills/brainstorm/SKILL.md +2 -3
  56. package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
  57. package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
  58. package/src/claude/skills/code-review/SKILL.md +12 -8
  59. package/src/claude/skills/code-review/references/verification-gate.md +4 -4
  60. package/src/claude/skills/debug/SKILL.md +2 -0
  61. package/src/claude/skills/develop/SKILL.md +100 -52
  62. package/src/claude/skills/develop/references/parallel-waves.md +105 -26
  63. package/src/claude/skills/develop/references/quality-gate.md +58 -38
  64. package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
  65. package/src/claude/skills/git/SKILL.md +44 -50
  66. package/src/claude/skills/hotfix/SKILL.md +10 -8
  67. package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
  68. package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
  69. package/src/claude/skills/inspect/SKILL.md +12 -30
  70. package/src/claude/skills/mobile-development/SKILL.md +12 -12
  71. package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
  72. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
  73. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
  74. package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
  75. package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
  76. package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
  77. package/src/claude/skills/pptx/scripts/package.json +1 -1
  78. package/src/claude/skills/question/SKILL.md +13 -13
  79. package/src/claude/skills/research/SKILL.md +2 -2
  80. package/src/claude/skills/specs/SKILL.md +32 -15
  81. package/src/claude/skills/specs/references/review.md +3 -3
  82. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  83. package/src/claude/skills/specs/templates/spec-state.json +7 -3
  84. package/src/claude/skills/sync/SKILL.md +26 -1
  85. package/src/claude/skills/sync/references/sync-protocols.md +3 -3
  86. package/src/claude/skills/test/SKILL.md +12 -4
  87. package/src/claude/skills/test/references/failure-triage.md +17 -17
  88. package/src/claude/skills/test/references/test-memory.md +2 -2
  89. package/src/codex/AGENTS.md +16 -24
  90. package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
  91. package/src/codex/hooks/rules.cjs +27 -26
  92. package/src/codex/hooks/spec-gate.cjs +19 -8
  93. package/src/codex/hooks/spec-state.cjs +10 -1
  94. package/src/codex/hooks.json +14 -14
  95. package/src/common/AGENTS.md +43 -0
  96. package/src/opencode/AGENTS.md +21 -121
  97. package/src/opencode/plugins/rules.ts +26 -34
  98. package/src/opencode/plugins/spec-gate.ts +283 -0
  99. 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