clink-cli 0.2.1__py3-none-any.whl

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 (141) hide show
  1. clink/__init__.py +19 -0
  2. clink/agents/__init__.py +30 -0
  3. clink/agents/base.py +70 -0
  4. clink/agents/claude.py +7 -0
  5. clink/agents/codex.py +7 -0
  6. clink/agents/gemini.py +7 -0
  7. clink/allowed_roots.py +356 -0
  8. clink/backend.py +765 -0
  9. clink/cli.py +594 -0
  10. clink/constants.py +78 -0
  11. clink/contracts.py +167 -0
  12. clink/files.py +70 -0
  13. clink/generation.py +274 -0
  14. clink/generation_capabilities.py +207 -0
  15. clink/generation_contracts.py +214 -0
  16. clink/generation_policy.py +174 -0
  17. clink/models.py +243 -0
  18. clink/native.py +409 -0
  19. clink/native_adapters.py +244 -0
  20. clink/native_contracts.py +68 -0
  21. clink/native_generation.py +421 -0
  22. clink/native_generation_claude_policy.py +59 -0
  23. clink/native_generation_contracts.py +264 -0
  24. clink/native_generation_families.py +218 -0
  25. clink/native_generation_family_policy.py +14 -0
  26. clink/native_generation_files.py +153 -0
  27. clink/native_generation_policy.py +82 -0
  28. clink/native_policy.py +86 -0
  29. clink/parsers/__init__.py +34 -0
  30. clink/parsers/agy.py +53 -0
  31. clink/parsers/agy_native.py +161 -0
  32. clink/parsers/agy_native_generation.py +189 -0
  33. clink/parsers/base.py +209 -0
  34. clink/parsers/claude.py +104 -0
  35. clink/parsers/claude_native.py +113 -0
  36. clink/parsers/claude_native_generation.py +346 -0
  37. clink/parsers/codex.py +172 -0
  38. clink/parsers/codex_generation.py +212 -0
  39. clink/parsers/codex_native.py +236 -0
  40. clink/parsers/codex_native_generation.py +367 -0
  41. clink/parsers/gemini.py +110 -0
  42. clink/parsers/grok.py +63 -0
  43. clink/parsers/grok_native.py +91 -0
  44. clink/parsers/grok_native_generation.py +157 -0
  45. clink/policy.py +180 -0
  46. clink/registry.py +544 -0
  47. clink/resources/CODEX-LICENSE +201 -0
  48. clink/resources/CODEX-NOTICE +6 -0
  49. clink/resources/PROVENANCE.md +16 -0
  50. clink/resources/codex-0.166.0-models-direct.json +1284 -0
  51. clink/resources/codex-0.166.0-models-generation.json +1269 -0
  52. clink/resources/native-agy-1.4.12-admission.json +746 -0
  53. clink/resources/native-claude-2.1.304-admission.json +99 -0
  54. clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
  55. clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
  56. clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
  57. clink/resources/native-grok-1.0.70-admission.json +644 -0
  58. clink/runner.py +303 -0
  59. clink/setup.py +399 -0
  60. clink/tasks.py +695 -0
  61. clink_cli-0.2.1.dist-info/METADATA +225 -0
  62. clink_cli-0.2.1.dist-info/RECORD +141 -0
  63. clink_cli-0.2.1.dist-info/WHEEL +5 -0
  64. clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
  65. clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
  66. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
  67. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
  68. clink_cli-0.2.1.dist-info/top_level.txt +7 -0
  69. conf/__init__.py +1 -0
  70. conf/cli_clients/agy.json +40 -0
  71. conf/cli_clients/claude.json +39 -0
  72. conf/cli_clients/codex.json +40 -0
  73. conf/cli_clients/gemini.json +23 -0
  74. conf/cli_clients/grok.json +40 -0
  75. config.py +40 -0
  76. server.py +896 -0
  77. systemprompts/__init__.py +35 -0
  78. systemprompts/analyze_prompt.py +87 -0
  79. systemprompts/chat_prompt.py +57 -0
  80. systemprompts/clink/codex_codereviewer.txt +8 -0
  81. systemprompts/clink/default.txt +8 -0
  82. systemprompts/clink/default_codereviewer.txt +8 -0
  83. systemprompts/clink/default_planner.txt +7 -0
  84. systemprompts/clink/native_codereviewer.txt +1 -0
  85. systemprompts/clink/native_default.txt +1 -0
  86. systemprompts/clink/native_planner.txt +1 -0
  87. systemprompts/codereview_prompt.py +91 -0
  88. systemprompts/consensus_prompt.py +120 -0
  89. systemprompts/debug_prompt.py +150 -0
  90. systemprompts/docgen_prompt.py +252 -0
  91. systemprompts/generate_code_prompt.py +181 -0
  92. systemprompts/planner_prompt.py +127 -0
  93. systemprompts/precommit_prompt.py +179 -0
  94. systemprompts/refactor_prompt.py +323 -0
  95. systemprompts/secaudit_prompt.py +420 -0
  96. systemprompts/testgen_prompt.py +133 -0
  97. systemprompts/thinkdeep_prompt.py +55 -0
  98. systemprompts/tracer_prompt.py +145 -0
  99. tools/__init__.py +43 -0
  100. tools/analyze.py +583 -0
  101. tools/apilookup.py +97 -0
  102. tools/challenge.py +198 -0
  103. tools/chat.py +187 -0
  104. tools/clink.py +130 -0
  105. tools/codereview.py +724 -0
  106. tools/consensus.py +790 -0
  107. tools/debug.py +573 -0
  108. tools/docgen.py +577 -0
  109. tools/listmodels.py +48 -0
  110. tools/models.py +373 -0
  111. tools/planner.py +533 -0
  112. tools/precommit.py +776 -0
  113. tools/refactor.py +645 -0
  114. tools/secaudit.py +685 -0
  115. tools/shared/__init__.py +19 -0
  116. tools/shared/base_models.py +167 -0
  117. tools/shared/base_tool.py +659 -0
  118. tools/shared/exceptions.py +20 -0
  119. tools/shared/schema_builders.py +159 -0
  120. tools/simple/__init__.py +18 -0
  121. tools/simple/base.py +539 -0
  122. tools/testgen.py +513 -0
  123. tools/thinkdeep.py +586 -0
  124. tools/tracer.py +763 -0
  125. tools/version.py +53 -0
  126. tools/workflow/__init__.py +22 -0
  127. tools/workflow/base.py +448 -0
  128. tools/workflow/schema_builders.py +166 -0
  129. tools/workflow/workflow_mixin.py +1195 -0
  130. utils/__init__.py +21 -0
  131. utils/call_state.py +349 -0
  132. utils/client_info.py +293 -0
  133. utils/consensus_manifest.py +131 -0
  134. utils/conversation_memory.py +582 -0
  135. utils/env.py +111 -0
  136. utils/file_types.py +271 -0
  137. utils/file_utils.py +803 -0
  138. utils/image_utils.py +94 -0
  139. utils/security_config.py +163 -0
  140. utils/storage_backend.py +177 -0
  141. utils/token_utils.py +54 -0
tools/precommit.py ADDED
@@ -0,0 +1,776 @@
1
+ """
2
+ Precommit Workflow tool - Step-by-step pre-commit validation with expert analysis
3
+
4
+ This tool provides a structured workflow for comprehensive pre-commit validation.
5
+ It guides the CLI agent through systematic investigation steps with forced pauses between each step
6
+ to ensure thorough code examination, git change analysis, and issue detection before proceeding.
7
+ The tool supports finding updates and expert analysis integration.
8
+
9
+ Key features:
10
+ - Step-by-step pre-commit investigation workflow with progress tracking
11
+ - Context-aware file embedding (references during investigation, full content for analysis)
12
+ - Automatic git repository discovery and change analysis
13
+ - Expert analysis integration with external models (default)
14
+ - Support for multiple repositories and change types
15
+ - Configurable validation type (external with expert model or internal only)
16
+ """
17
+
18
+ import logging
19
+ from typing import TYPE_CHECKING, Any, Literal, Optional
20
+
21
+ from pydantic import Field, model_validator
22
+
23
+ if TYPE_CHECKING:
24
+ from tools.models import ToolModelCategory
25
+
26
+ from config import TEMPERATURE_ANALYTICAL
27
+ from systemprompts import PRECOMMIT_PROMPT
28
+ from tools.shared.base_models import WorkflowRequest
29
+
30
+ from .workflow.base import WorkflowTool
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Tool-specific field descriptions for precommit workflow
35
+ PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS = {
36
+ "step": (
37
+ "Step 1: outline how you'll validate the git changes. Later steps: report findings. Review diffs and impacts, use `relevant_files`, and avoid pasting large snippets."
38
+ ),
39
+ "step_number": "Current pre-commit step number (starts at 1).",
40
+ "total_steps": (
41
+ "Planned number of validation steps. External validation: use at most three (analysis → follow-ups → summary). Internal validation: a single step. Honour these limits when resuming via continuation_id."
42
+ ),
43
+ "next_step_required": (
44
+ "True to continue with another step, False when validation is complete. "
45
+ "CRITICAL: If total_steps>=3 or when `precommit_type = external`, set to True until the final step. "
46
+ "When continuation_id is provided: Follow the same validation rules based on precommit_type."
47
+ ),
48
+ "findings": "Record git diff insights, risks, missing tests, security concerns, and positives; update previous notes as you go.",
49
+ "files_checked": "Absolute paths for every file examined, including ruled-out candidates.",
50
+ "relevant_files": "Absolute paths of files involved in the change or validation (code, configs, tests, docs). Must be absolute full non-abbreviated paths.",
51
+ "relevant_context": "Key functions/methods touched by the change (e.g. 'Class.method', 'function_name').",
52
+ "issues_found": "List issues with severity (critical/high/medium/low) plus descriptions (bugs, security, performance, coverage).",
53
+ "precommit_type": "'external' (default, triggers expert model) or 'internal' (local-only validation).",
54
+ "images": "Optional absolute paths to screenshots or diagrams that aid validation.",
55
+ "path": "Absolute path to the repository root. Required in step 1.",
56
+ "compare_to": "Optional git ref (branch/tag/commit) to diff against; falls back to staged/unstaged changes.",
57
+ "include_staged": "Whether to inspect staged changes (ignored when `compare_to` is set).",
58
+ "include_unstaged": "Whether to inspect unstaged changes (ignored when `compare_to` is set).",
59
+ "focus_on": "Optional emphasis areas such as security, performance, or test coverage.",
60
+ "severity_filter": "Lowest severity to include when reporting issues.",
61
+ }
62
+
63
+
64
+ class PrecommitRequest(WorkflowRequest):
65
+ """Request model for precommit workflow investigation steps"""
66
+
67
+ # Required fields for each investigation step
68
+ step: str = Field(..., description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["step"])
69
+ step_number: int = Field(..., description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["step_number"])
70
+ total_steps: int = Field(..., description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["total_steps"])
71
+ next_step_required: bool = Field(..., description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["next_step_required"])
72
+
73
+ # Investigation tracking fields
74
+ findings: str = Field(..., description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["findings"])
75
+ files_checked: list[str] = Field(
76
+ default_factory=list, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["files_checked"]
77
+ )
78
+ relevant_files: list[str] = Field(
79
+ default_factory=list, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["relevant_files"]
80
+ )
81
+ relevant_context: list[str] = Field(
82
+ default_factory=list, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["relevant_context"]
83
+ )
84
+ issues_found: list[dict] = Field(
85
+ default_factory=list, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["issues_found"]
86
+ )
87
+ precommit_type: Optional[Literal["external", "internal"]] = Field(
88
+ "external", description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["precommit_type"]
89
+ )
90
+
91
+ # Optional images for visual validation
92
+ images: Optional[list[str]] = Field(default=None, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["images"])
93
+
94
+ # Precommit-specific fields (only used in step 1 to initialize)
95
+ # Required for step 1, validated in model_validator
96
+ path: Optional[str] = Field(None, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["path"])
97
+ compare_to: Optional[str] = Field(None, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["compare_to"])
98
+ include_staged: Optional[bool] = Field(True, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["include_staged"])
99
+ include_unstaged: Optional[bool] = Field(
100
+ True, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["include_unstaged"]
101
+ )
102
+ focus_on: Optional[str] = Field(None, description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["focus_on"])
103
+ severity_filter: Optional[Literal["critical", "high", "medium", "low", "all"]] = Field(
104
+ "all", description=PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["severity_filter"]
105
+ )
106
+
107
+ # Override inherited fields to exclude them from schema (except model which needs to be available)
108
+ temperature: Optional[float] = Field(default=None, exclude=True)
109
+ thinking_mode: Optional[str] = Field(default=None, exclude=True)
110
+
111
+ @model_validator(mode="after")
112
+ def validate_step_one_requirements(self):
113
+ """Ensure step 1 has required path field."""
114
+ if self.step_number == 1 and not self.path:
115
+ raise ValueError("Step 1 requires 'path' field to specify git repository location")
116
+ return self
117
+
118
+
119
+ class PrecommitTool(WorkflowTool):
120
+ """
121
+ Precommit workflow tool for step-by-step pre-commit validation and expert analysis.
122
+
123
+ This tool implements a structured pre-commit validation workflow that guides users through
124
+ methodical investigation steps, ensuring thorough change examination, issue identification,
125
+ and validation before reaching conclusions. It supports complex validation scenarios including
126
+ multi-repository analysis, security review, performance validation, and integration testing.
127
+ """
128
+
129
+ def __init__(self):
130
+ super().__init__()
131
+ self.initial_request = None
132
+ self.git_config = {}
133
+
134
+ def get_name(self) -> str:
135
+ return "precommit"
136
+
137
+ def get_description(self) -> str:
138
+ return (
139
+ "Validates git changes and repository state before committing with systematic analysis. "
140
+ "Use for multi-repository validation, security review, change impact assessment, and completeness verification. "
141
+ "Guides through structured investigation with expert analysis."
142
+ )
143
+
144
+ def get_system_prompt(self) -> str:
145
+ return PRECOMMIT_PROMPT
146
+
147
+ def get_default_temperature(self) -> float:
148
+ return TEMPERATURE_ANALYTICAL
149
+
150
+ def get_model_category(self) -> "ToolModelCategory":
151
+ """Precommit requires thorough analysis and reasoning"""
152
+ from tools.models import ToolModelCategory
153
+
154
+ return ToolModelCategory.EXTENDED_REASONING
155
+
156
+ def get_workflow_request_model(self):
157
+ """Return the precommit workflow-specific request model."""
158
+ return PrecommitRequest
159
+
160
+ def get_input_schema(self) -> dict[str, Any]:
161
+ """Generate input schema using WorkflowSchemaBuilder with precommit-specific overrides."""
162
+ from .workflow.schema_builders import WorkflowSchemaBuilder
163
+
164
+ # Precommit workflow-specific field overrides
165
+ precommit_field_overrides = {
166
+ "step": {
167
+ "type": "string",
168
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["step"],
169
+ },
170
+ "step_number": {
171
+ "type": "integer",
172
+ "minimum": 1,
173
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["step_number"],
174
+ },
175
+ "total_steps": {
176
+ "type": "integer",
177
+ "minimum": 3,
178
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["total_steps"],
179
+ },
180
+ "next_step_required": {
181
+ "type": "boolean",
182
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["next_step_required"],
183
+ },
184
+ "findings": {
185
+ "type": "string",
186
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["findings"],
187
+ },
188
+ "files_checked": {
189
+ "type": "array",
190
+ "items": {"type": "string"},
191
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["files_checked"],
192
+ },
193
+ "relevant_files": {
194
+ "type": "array",
195
+ "items": {"type": "string"},
196
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["relevant_files"],
197
+ },
198
+ "precommit_type": {
199
+ "type": "string",
200
+ "enum": ["external", "internal"],
201
+ "default": "external",
202
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["precommit_type"],
203
+ },
204
+ "issues_found": {
205
+ "type": "array",
206
+ "items": {"type": "object"},
207
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["issues_found"],
208
+ },
209
+ "images": {
210
+ "type": "array",
211
+ "items": {"type": "string"},
212
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["images"],
213
+ },
214
+ # Precommit-specific fields (for step 1)
215
+ "path": {
216
+ "type": "string",
217
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["path"],
218
+ },
219
+ "compare_to": {
220
+ "type": "string",
221
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["compare_to"],
222
+ },
223
+ "include_staged": {
224
+ "type": "boolean",
225
+ "default": True,
226
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["include_staged"],
227
+ },
228
+ "include_unstaged": {
229
+ "type": "boolean",
230
+ "default": True,
231
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["include_unstaged"],
232
+ },
233
+ "focus_on": {
234
+ "type": "string",
235
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["focus_on"],
236
+ },
237
+ "severity_filter": {
238
+ "type": "string",
239
+ "enum": ["critical", "high", "medium", "low", "all"],
240
+ "default": "all",
241
+ "description": PRECOMMIT_WORKFLOW_FIELD_DESCRIPTIONS["severity_filter"],
242
+ },
243
+ }
244
+
245
+ # Use WorkflowSchemaBuilder with precommit-specific tool fields
246
+ return WorkflowSchemaBuilder.build_schema(
247
+ tool_specific_fields=precommit_field_overrides,
248
+ model_field_schema=self.get_model_field_schema(),
249
+ auto_mode=self.is_effective_auto_mode(),
250
+ tool_name=self.get_name(),
251
+ )
252
+
253
+ def get_required_actions(
254
+ self, step_number: int, confidence: str, findings: str, total_steps: int, request=None
255
+ ) -> list[str]:
256
+ """Define required actions for each investigation phase.
257
+
258
+ Now includes request parameter for continuation-aware decisions.
259
+ """
260
+ # Check for continuation - fast track mode
261
+ if request:
262
+ continuation_id = self.get_request_continuation_id(request)
263
+ precommit_type = self.get_precommit_type(request)
264
+ if continuation_id and precommit_type == "external":
265
+ if step_number == 1:
266
+ return [
267
+ "Execute git status to see all changes",
268
+ "Execute git diff --cached for staged changes (exclude binary files)",
269
+ "Execute git diff for unstaged changes (exclude binary files)",
270
+ "List any relevant untracked files as well.",
271
+ ]
272
+ else:
273
+ return ["Complete validation and proceed to expert analysis with changeset file"]
274
+
275
+ # Extract counts for normal flow
276
+ findings_count = len(findings.split("\n")) if findings else 0
277
+ issues_count = self.get_consolidated_issues_count()
278
+
279
+ if step_number == 1:
280
+ # Initial pre-commit investigation tasks
281
+ return [
282
+ "Search for all git repositories in the specified path using appropriate tools",
283
+ "Check git status to identify staged, unstaged, and untracked changes as required",
284
+ "Execute git status to see all changes",
285
+ "Execute git diff --cached for staged changes (exclude binary files)",
286
+ "Execute git diff for unstaged changes (exclude binary files)",
287
+ "List any relevant untracked files as well.",
288
+ "Understand what functionality was added, modified, or removed",
289
+ "Identify the scope and intent of the changes being committed",
290
+ "CRITICAL: You are on step 1 - you MUST set next_step_required=True and continue to at least step 3 minimum",
291
+ ]
292
+ elif step_number == 2:
293
+ # Need deeper investigation
294
+ actions = [
295
+ "Examine the specific files you've identified as changed or relevant",
296
+ "Analyze the logic and implementation details of modifications",
297
+ "Check for potential issues: bugs, security risks, performance problems",
298
+ "Verify that changes align with good coding practices and patterns",
299
+ "Look for missing tests, documentation, or configuration updates",
300
+ ]
301
+
302
+ # Add step validation reminder
303
+ if request and request.total_steps >= 3:
304
+ actions.append(
305
+ f"CRITICAL: You are on step 2 of {request.total_steps} minimum steps - you MUST set next_step_required=True unless this is the final step"
306
+ )
307
+
308
+ return actions
309
+ elif step_number >= 2 and (findings_count > 2 or issues_count > 0):
310
+ # Close to completion - need final verification
311
+ actions = [
312
+ "Verify all identified issues have been properly documented",
313
+ "Check for any missed dependencies or related files that need review",
314
+ "Confirm the completeness and correctness of your assessment",
315
+ "Ensure all security, performance, and quality concerns are captured",
316
+ "Validate that your findings are comprehensive and actionable",
317
+ ]
318
+
319
+ # Add step validation reminder
320
+ if request and request.total_steps >= 3 and step_number < request.total_steps:
321
+ actions.append(
322
+ f"CRITICAL: You are on step {step_number} of {request.total_steps} minimum steps - set next_step_required=True to continue"
323
+ )
324
+ elif request and request.total_steps >= 3 and step_number >= request.total_steps:
325
+ actions.append(
326
+ f"You are on final step {step_number} - you may now set next_step_required=False to complete"
327
+ )
328
+
329
+ return actions
330
+ else:
331
+ # General investigation needed
332
+ actions = [
333
+ "Continue examining the changes and their potential impact",
334
+ "Gather more evidence using appropriate investigation tools",
335
+ "Test your assumptions about the changes and their effects",
336
+ "Look for patterns that confirm or refute your current assessment",
337
+ ]
338
+
339
+ # Add step validation reminder for all other cases
340
+ if request and request.total_steps >= 3:
341
+ if step_number < request.total_steps:
342
+ actions.append(
343
+ f"CRITICAL: You are on step {step_number} of {request.total_steps} minimum steps - set next_step_required=True to continue"
344
+ )
345
+ else:
346
+ actions.append(
347
+ f"You are on final step {step_number} - you may now set next_step_required=False to complete"
348
+ )
349
+
350
+ return actions
351
+
352
+ def should_call_expert_analysis(self, consolidated_findings, request=None) -> bool:
353
+ """
354
+ Decide when to call external model based on investigation completeness.
355
+
356
+ For continuations with external type, always proceed with expert analysis.
357
+ """
358
+ # Check if user requested to skip assistant model
359
+ if request and not self.get_request_use_assistant_model(request):
360
+ return False
361
+
362
+ # For continuations with external type, always proceed with expert analysis
363
+ continuation_id = self.get_request_continuation_id(request)
364
+ if continuation_id and request.precommit_type == "external":
365
+ return True # Always perform expert analysis for external continuations
366
+
367
+ # Check if we have meaningful investigation data
368
+ return (
369
+ len(consolidated_findings.relevant_files) > 0
370
+ or len(consolidated_findings.findings) >= 2
371
+ or len(consolidated_findings.issues_found) > 0
372
+ )
373
+
374
+ def prepare_expert_analysis_context(self, consolidated_findings) -> str:
375
+ """Prepare context for external model call for final pre-commit validation."""
376
+ context_parts = [
377
+ f"=== PRE-COMMIT ANALYSIS REQUEST ===\\n{self.get_expert_initial_request(self.initial_request, 'Pre-commit validation initiated')}\\n=== END REQUEST ==="
378
+ ]
379
+
380
+ # Add investigation summary
381
+ investigation_summary = self._build_precommit_summary(consolidated_findings)
382
+ context_parts.append(
383
+ f"\\n=== AGENT'S PRE-COMMIT INVESTIGATION ===\\n{investigation_summary}\\n=== END INVESTIGATION ==="
384
+ )
385
+
386
+ # Add git configuration context if available
387
+ if self.git_config:
388
+ config_text = "\\n".join(f"- {key}: {value}" for key, value in self.git_config.items())
389
+ context_parts.append(f"\\n=== GIT CONFIGURATION ===\\n{config_text}\\n=== END CONFIGURATION ===")
390
+
391
+ # Add relevant methods/functions if available
392
+ if consolidated_findings.relevant_context:
393
+ methods_text = "\\n".join(f"- {method}" for method in consolidated_findings.relevant_context)
394
+ context_parts.append(f"\\n=== RELEVANT CODE ELEMENTS ===\\n{methods_text}\\n=== END CODE ELEMENTS ===")
395
+
396
+ # Add issues found evolution if available
397
+ if consolidated_findings.issues_found:
398
+ issues_text = "\\n".join(
399
+ f"[{issue.get('severity', 'unknown').upper()}] {issue.get('description', 'No description')}"
400
+ for issue in consolidated_findings.issues_found
401
+ )
402
+ context_parts.append(f"\\n=== ISSUES IDENTIFIED ===\\n{issues_text}\\n=== END ISSUES ===")
403
+
404
+ # Add assessment evolution if available
405
+ if consolidated_findings.hypotheses:
406
+ assessments_text = "\\n".join(
407
+ f"Step {h['step']}: {h['hypothesis']}" for h in consolidated_findings.hypotheses
408
+ )
409
+ context_parts.append(f"\\n=== ASSESSMENT EVOLUTION ===\\n{assessments_text}\\n=== END ASSESSMENTS ===")
410
+
411
+ # Add images if available
412
+ if consolidated_findings.images:
413
+ images_text = "\\n".join(f"- {img}" for img in consolidated_findings.images)
414
+ context_parts.append(
415
+ f"\\n=== VISUAL VALIDATION INFORMATION ===\\n{images_text}\\n=== END VISUAL INFORMATION ==="
416
+ )
417
+
418
+ return "\\n".join(context_parts)
419
+
420
+ def _build_precommit_summary(self, consolidated_findings) -> str:
421
+ """Prepare a comprehensive summary of the pre-commit investigation."""
422
+ summary_parts = [
423
+ "=== SYSTEMATIC PRE-COMMIT INVESTIGATION SUMMARY ===",
424
+ f"Total steps: {len(consolidated_findings.findings)}",
425
+ f"Files examined: {len(consolidated_findings.files_checked)}",
426
+ f"Relevant files identified: {len(consolidated_findings.relevant_files)}",
427
+ f"Code elements analyzed: {len(consolidated_findings.relevant_context)}",
428
+ f"Issues identified: {len(consolidated_findings.issues_found)}",
429
+ "",
430
+ "=== INVESTIGATION PROGRESSION ===",
431
+ ]
432
+
433
+ for finding in consolidated_findings.findings:
434
+ summary_parts.append(finding)
435
+
436
+ return "\\n".join(summary_parts)
437
+
438
+ def should_include_files_in_expert_prompt(self) -> bool:
439
+ """Include files in expert analysis for comprehensive validation."""
440
+ return True
441
+
442
+ def should_embed_system_prompt(self) -> bool:
443
+ """Embed system prompt in expert analysis for proper context."""
444
+ return True
445
+
446
+ def get_expert_thinking_mode(self) -> str:
447
+ """Use high thinking mode for thorough pre-commit analysis."""
448
+ return "high"
449
+
450
+ def get_expert_analysis_instruction(self) -> str:
451
+ """Get specific instruction for pre-commit expert analysis."""
452
+ return (
453
+ "Please provide comprehensive pre-commit validation based on the investigation findings. "
454
+ "Focus on identifying any remaining issues, validating the completeness of the analysis, "
455
+ "and providing final recommendations for commit readiness."
456
+ )
457
+
458
+ # Hook method overrides for precommit-specific behavior
459
+
460
+ def prepare_step_data(self, request) -> dict:
461
+ """
462
+ Map precommit-specific fields for internal processing.
463
+ """
464
+ step_data = {
465
+ "step": request.step,
466
+ "step_number": request.step_number,
467
+ "findings": request.findings,
468
+ "files_checked": request.files_checked,
469
+ "relevant_files": request.relevant_files,
470
+ "relevant_context": request.relevant_context,
471
+ "issues_found": request.issues_found,
472
+ "precommit_type": request.precommit_type,
473
+ "hypothesis": request.findings, # Map findings to hypothesis for compatibility
474
+ "images": request.images or [],
475
+ "confidence": "high", # Dummy value for workflow_mixin compatibility
476
+ }
477
+ return step_data
478
+
479
+ def should_skip_expert_analysis(self, request, consolidated_findings) -> bool:
480
+ """
481
+ Precommit workflow skips expert analysis only when precommit_type is "internal".
482
+ Default is always to use expert analysis (external).
483
+ For continuations with external type, always perform expert analysis immediately.
484
+ """
485
+ # If it's a continuation and precommit_type is external, don't skip
486
+ continuation_id = self.get_request_continuation_id(request)
487
+ if continuation_id and request.precommit_type != "internal":
488
+ return False # Always do expert analysis for external continuations
489
+
490
+ return request.precommit_type == "internal" and not request.next_step_required
491
+
492
+ def store_initial_issue(self, step_description: str):
493
+ """Store initial request for expert analysis."""
494
+ self.initial_request = step_description
495
+
496
+ # Override inheritance hooks for precommit-specific behavior
497
+
498
+ def get_completion_status(self) -> str:
499
+ """Precommit tools use precommit-specific status."""
500
+ return "validation_complete_ready_for_commit"
501
+
502
+ def get_completion_data_key(self) -> str:
503
+ """Precommit uses 'complete_validation' key."""
504
+ return "complete_validation"
505
+
506
+ def get_final_analysis_from_request(self, request):
507
+ """Precommit tools use 'findings' field."""
508
+ return request.findings
509
+
510
+ def get_precommit_type(self, request) -> str:
511
+ """Get precommit type from request. Hook method for clean inheritance."""
512
+ try:
513
+ return request.precommit_type or "external"
514
+ except AttributeError:
515
+ return "external" # Default to external validation
516
+
517
+ def get_consolidated_issues_count(self) -> int:
518
+ """Get count of issues from consolidated findings. Hook method for clean access."""
519
+ try:
520
+ return len(self.consolidated_findings.issues_found)
521
+ except AttributeError:
522
+ return 0
523
+
524
+ def get_completion_message(self) -> str:
525
+ """Precommit-specific completion message."""
526
+ return (
527
+ "Pre-commit validation complete. You have identified all issues "
528
+ "and verified commit readiness. MANDATORY: Present the user with the complete validation results "
529
+ "and IMMEDIATELY proceed with commit if no critical issues found, or provide specific fix guidance "
530
+ "if issues need resolution. Focus on actionable next steps."
531
+ )
532
+
533
+ def get_skip_reason(self) -> str:
534
+ """Precommit-specific skip reason."""
535
+ return (
536
+ "Completed comprehensive pre-commit validation with internal analysis only (no external model validation)"
537
+ )
538
+
539
+ def get_skip_expert_analysis_status(self) -> str:
540
+ """Precommit-specific expert analysis skip status."""
541
+ return "skipped_due_to_internal_analysis_type"
542
+
543
+ def prepare_work_summary(self) -> str:
544
+ """Precommit-specific work summary."""
545
+ return self._build_precommit_summary(self.consolidated_findings)
546
+
547
+ def get_completion_next_steps_message(self, expert_analysis_used: bool = False) -> str:
548
+ """
549
+ Precommit-specific completion message.
550
+
551
+ Args:
552
+ expert_analysis_used: True if expert analysis was successfully executed
553
+ """
554
+ base_message = (
555
+ "PRE-COMMIT VALIDATION IS COMPLETE. You may delete any `pal_precommit.changeset` created. You MUST now summarize "
556
+ "and present ALL validation results, identified issues with their severity levels, and exact commit recommendations. "
557
+ "Clearly state whether the changes are ready for commit or require fixes first. Provide concrete, actionable guidance for "
558
+ "any issues that need resolution—make it easy for a developer to understand exactly what needs to be "
559
+ "done before committing."
560
+ )
561
+
562
+ # Add expert analysis guidance only when expert analysis was actually used
563
+ if expert_analysis_used:
564
+ expert_guidance = self.get_expert_analysis_guidance()
565
+ if expert_guidance:
566
+ return f"{base_message}\n\n{expert_guidance}"
567
+
568
+ return base_message
569
+
570
+ def get_expert_analysis_guidance(self) -> str:
571
+ """
572
+ Get additional guidance for handling expert analysis results in pre-commit context.
573
+
574
+ Returns:
575
+ Additional guidance text for validating and using expert analysis findings
576
+ """
577
+ return (
578
+ "IMPORTANT: Expert analysis has been provided above. You MUST carefully review "
579
+ "the expert's validation findings and security assessments. Cross-reference the "
580
+ "expert's analysis with your own investigation to ensure all critical issues are "
581
+ "addressed. Pay special attention to any security vulnerabilities, performance "
582
+ "concerns, or architectural issues identified by the expert review."
583
+ )
584
+
585
+ def get_step_guidance_message(self, request) -> str:
586
+ """
587
+ Precommit-specific step guidance with detailed investigation instructions.
588
+ """
589
+ step_guidance = self.get_precommit_step_guidance(request.step_number, request)
590
+ return step_guidance["next_steps"]
591
+
592
+ def get_precommit_step_guidance(self, step_number: int, request) -> dict[str, Any]:
593
+ """
594
+ Provide step-specific guidance for precommit workflow.
595
+ Uses get_required_actions to determine what needs to be done,
596
+ then formats those actions into appropriate guidance messages.
597
+ """
598
+ # Get the required actions from the single source of truth
599
+ required_actions = self.get_required_actions(
600
+ step_number,
601
+ request.precommit_type or "external", # Using precommit_type as confidence proxy
602
+ request.findings or "",
603
+ request.total_steps,
604
+ request, # Pass request for continuation-aware decisions
605
+ )
606
+
607
+ # Check if this is a continuation to provide context-aware guidance
608
+ continuation_id = self.get_request_continuation_id(request)
609
+ is_external_continuation = continuation_id and request.precommit_type == "external"
610
+ is_internal_continuation = continuation_id and request.precommit_type == "internal"
611
+
612
+ # Format the guidance based on step number and continuation status
613
+ if step_number == 1:
614
+ if is_external_continuation:
615
+ # Fast-track mode for external continuations
616
+ next_steps = (
617
+ "You are on step 1 of MAXIMUM 2 steps. CRITICAL: Gather and save the complete git changeset NOW. "
618
+ "MANDATORY ACTIONS:\\n"
619
+ + "\\n".join(f"{i+1}. {action}" for i, action in enumerate(required_actions))
620
+ + "\\n\\nMANDATORY: The changeset may be large. You MUST save the required changeset as a 'pal_precommit.changeset' file "
621
+ "(replacing any existing one) in your work directory and include the FULL absolute path in relevant_files (exclude any "
622
+ "binary files). ONLY include the code changes, no extra commentary."
623
+ "Set next_step_required=True and step_number=2 for the next call."
624
+ )
625
+ elif is_internal_continuation:
626
+ # Internal validation mode
627
+ next_steps = (
628
+ "Continuing previous conversation with internal validation only. The analysis will build "
629
+ "upon the prior findings without external model validation. REQUIRED ACTIONS:\\n"
630
+ + "\\n".join(f"{i+1}. {action}" for i, action in enumerate(required_actions))
631
+ )
632
+ else:
633
+ # Normal flow for new validations
634
+ next_steps = (
635
+ f"MANDATORY: DO NOT call the {self.get_name()} tool again immediately. You MUST first investigate "
636
+ f"the git repositories and changes using appropriate tools. CRITICAL AWARENESS: You need to:\\n"
637
+ + "\\n".join(f"{i+1}. {action}" for i, action in enumerate(required_actions))
638
+ + f"\\n\\nOnly call {self.get_name()} again AFTER completing your investigation. "
639
+ f"When you call {self.get_name()} next time, use step_number: {step_number + 1} "
640
+ f"and report specific files examined, changes analyzed, and validation findings discovered."
641
+ )
642
+
643
+ elif step_number == 2:
644
+ # CRITICAL: Check if violating minimum step requirement
645
+ if (
646
+ request.total_steps >= 3
647
+ and request.step_number < request.total_steps
648
+ and not request.next_step_required
649
+ ):
650
+ next_steps = (
651
+ f"ERROR: You set total_steps={request.total_steps} but next_step_required=False on step {request.step_number}. "
652
+ f"This violates the minimum step requirement. You MUST set next_step_required=True until you reach the final step. "
653
+ f"Call {self.get_name()} again with next_step_required=True and continue your investigation."
654
+ )
655
+ elif is_external_continuation or (not request.next_step_required and request.precommit_type == "external"):
656
+ # Fast-track completion or about to complete - ensure changeset is saved
657
+ next_steps = (
658
+ "Proceeding immediately to expert analysis. "
659
+ f"MANDATORY: call {self.get_name()} tool immediately again, and set next_step_required=False to "
660
+ f"trigger external validation NOW. "
661
+ f"MANDATORY: Include the entire changeset! The changeset may be large. You MUST save the required "
662
+ f"changeset as a 'pal_precommit.changeset' file (replacing any existing one) in your work directory "
663
+ f"and include the FULL absolute path in relevant_files so the expert can access the complete changeset. "
664
+ f"ONLY include the code changes, no extra commentary."
665
+ )
666
+ else:
667
+ # Normal flow - deeper analysis needed
668
+ next_steps = (
669
+ f"STOP! Do NOT call {self.get_name()} again yet. You are on step 2 of {request.total_steps} minimum required steps. "
670
+ f"MANDATORY ACTIONS before calling {self.get_name()} step {step_number + 1}:\\n"
671
+ + "\\n".join(f"{i+1}. {action}" for i, action in enumerate(required_actions))
672
+ + f"\\n\\nRemember: You MUST set next_step_required=True until step {request.total_steps}. "
673
+ + f"Only call {self.get_name()} again with step_number: {step_number + 1} AFTER completing these validations."
674
+ )
675
+
676
+ elif step_number >= 3:
677
+ if not request.next_step_required and request.precommit_type == "external":
678
+ # About to complete - ensure changeset is saved
679
+ next_steps = (
680
+ "Completing validation and proceeding to expert analysis. "
681
+ "MANDATORY: Save the complete git changeset as a 'pal_precommit.changeset' file "
682
+ "in your work directory and include the FULL absolute path in relevant_files."
683
+ )
684
+ else:
685
+ # Later steps - final verification
686
+ next_steps = (
687
+ f"WAIT! Your validation needs final verification. DO NOT call {self.get_name()} immediately. REQUIRED ACTIONS:\\n"
688
+ + "\\n".join(f"{i+1}. {action}" for i, action in enumerate(required_actions))
689
+ + f"\\n\\nREMEMBER: Ensure you have identified all potential issues and verified commit readiness. "
690
+ f"Document findings with specific file references and issue descriptions, then call {self.get_name()} "
691
+ f"with step_number: {step_number + 1}."
692
+ )
693
+ else:
694
+ # Fallback for any other case - check minimum step violation first
695
+ if (
696
+ request.total_steps >= 3
697
+ and request.step_number < request.total_steps
698
+ and not request.next_step_required
699
+ ):
700
+ next_steps = (
701
+ f"ERROR: You set total_steps={request.total_steps} but next_step_required=False on step {request.step_number}. "
702
+ f"This violates the minimum step requirement. You MUST set next_step_required=True until step {request.total_steps}."
703
+ )
704
+ elif not request.next_step_required and request.precommit_type == "external":
705
+ next_steps = (
706
+ "Completing validation. "
707
+ "MANDATORY: Save complete git changeset as 'pal_precommit.changeset' file and include path in relevant_files, "
708
+ "excluding any binary files."
709
+ )
710
+ else:
711
+ next_steps = (
712
+ f"PAUSE VALIDATION. Before calling {self.get_name()} step {step_number + 1}, you MUST examine more code and changes. "
713
+ + "Required: "
714
+ + ", ".join(required_actions[:2])
715
+ + ". "
716
+ + f"Your next {self.get_name()} call (step_number: {step_number + 1}) must include "
717
+ f"NEW evidence from actual change analysis, not just theories. NO recursive {self.get_name()} calls "
718
+ f"without investigation work!"
719
+ )
720
+
721
+ return {"next_steps": next_steps}
722
+
723
+ def customize_workflow_response(self, response_data: dict, request) -> dict:
724
+ """
725
+ Customize response to match precommit workflow format.
726
+ """
727
+ # Store initial request on first step
728
+ if request.step_number == 1:
729
+ self.initial_request = request.step
730
+ # Store git configuration for expert analysis
731
+ if request.path:
732
+ self.git_config = {
733
+ "path": request.path,
734
+ "compare_to": request.compare_to,
735
+ "include_staged": request.include_staged,
736
+ "include_unstaged": request.include_unstaged,
737
+ "severity_filter": request.severity_filter,
738
+ }
739
+
740
+ # Convert generic status names to precommit-specific ones
741
+ tool_name = self.get_name()
742
+ status_mapping = {
743
+ f"{tool_name}_in_progress": "validation_in_progress",
744
+ f"pause_for_{tool_name}": "pause_for_validation",
745
+ f"{tool_name}_required": "validation_required",
746
+ f"{tool_name}_complete": "validation_complete",
747
+ }
748
+
749
+ if response_data["status"] in status_mapping:
750
+ response_data["status"] = status_mapping[response_data["status"]]
751
+
752
+ # Rename status field to match precommit workflow
753
+ if f"{tool_name}_status" in response_data:
754
+ response_data["validation_status"] = response_data.pop(f"{tool_name}_status")
755
+ # Add precommit-specific status fields
756
+ response_data["validation_status"]["issues_identified"] = len(self.consolidated_findings.issues_found)
757
+ response_data["validation_status"]["precommit_type"] = request.precommit_type or "external"
758
+
759
+ # Map complete_precommitworkflow to complete_validation
760
+ if f"complete_{tool_name}" in response_data:
761
+ response_data["complete_validation"] = response_data.pop(f"complete_{tool_name}")
762
+
763
+ # Map the completion flag to match precommit workflow
764
+ if f"{tool_name}_complete" in response_data:
765
+ response_data["validation_complete"] = response_data.pop(f"{tool_name}_complete")
766
+
767
+ return response_data
768
+
769
+ # Required abstract methods from BaseTool
770
+ def get_request_model(self):
771
+ """Return the precommit workflow-specific request model."""
772
+ return PrecommitRequest
773
+
774
+ async def prepare_prompt(self, request) -> str:
775
+ """Not used - workflow tools use execute_workflow()."""
776
+ return "" # Workflow tools use execute_workflow() directly