Skip to content

Commit 5bea24a

Browse files
hyperpolymathclaude
andcommitted
feat: fix scanner failures, expand dispatch to 40+ patterns, implement automaton stubs
Scanner fixes (was 78% failure rate): - Stop swallowing stderr in run-fleet.sh and fleet-coordinator.sh - Distinguish exit 0 (clean), exit 1 (findings), exit 2 (error) - Add fallback hypatia-cli.sh paths in fleet-coordinator.sh - Validate JSON output is non-empty before parsing Dispatch expansion (was 9 patterns, now 40+): - Expand strategy 3 case mapping to cover all hypatia finding types - Map workflow hygiene, code safety, root hygiene, and structural rules - Use combined pattern+type matching for broader coverage Robot-repo-automaton stubs implemented: - detector.rs: detect_language_mismatch checks workflow language matrices against actual repo languages; detect_content_match does regex scanning - hypatia.rs: fetch_ruleset tries API then falls back to local verisimdb-data recipes; Command fixes execute via subprocess; Patch fixes use git apply with dry-run check; 5 core RSR rules as baseline - fleet.rs: session persistence to ~/.gitbot-fleet/sessions/ as JSON Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e817e9a commit 5bea24a

7 files changed

Lines changed: 599 additions & 66 deletions

File tree

fleet-coordinator.sh

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,45 @@ run_hypatia_scan() {
6666
# Generate findings file with timestamp
6767
local findings_file="$repo_findings_dir/${SESSION_ID}.json"
6868

69+
local scan_stderr
70+
scan_stderr=$(mktemp /tmp/hypatia-stderr-XXXXXX)
71+
local scan_exit=0
72+
6973
if [[ -x "$FLEET_DIR/../hypatia/hypatia-cli.sh" ]]; then
7074
HYPATIA_FORMAT=json "$FLEET_DIR/../hypatia/hypatia-cli.sh" scan "$repo_path" \
71-
> "$findings_file" 2>/dev/null || true
75+
> "$findings_file" 2>"$scan_stderr" || scan_exit=$?
76+
elif [[ -x "/var/mnt/eclipse/repos/hypatia/hypatia-cli.sh" ]]; then
77+
HYPATIA_FORMAT=json "/var/mnt/eclipse/repos/hypatia/hypatia-cli.sh" scan "$repo_path" \
78+
> "$findings_file" 2>"$scan_stderr" || scan_exit=$?
7279
else
73-
log_warn "Hypatia CLI not found, using POC scanner"
74-
"$FLEET_DIR/../hypatia/poc-scanner.sh" "$repo_path" > "$findings_file" 2>/dev/null || true
80+
log_warn "Hypatia CLI not found at expected paths"
81+
rm -f "$scan_stderr"
82+
return 1
83+
fi
84+
85+
# Exit 2 = real error; exit 0 = clean; exit 1 = findings found (both valid)
86+
if [[ "$scan_exit" -eq 2 ]]; then
87+
local err_msg
88+
err_msg=$(head -3 "$scan_stderr" 2>/dev/null || echo "unknown error")
89+
log_error "Scanner error for $repo_name (exit 2): $err_msg"
90+
rm -f "$scan_stderr" "$findings_file"
91+
return 1
7592
fi
93+
rm -f "$scan_stderr"
7694

77-
if [[ -f "$findings_file" ]]; then
78-
local issue_count=$(jq 'length' "$findings_file" 2>/dev/null || echo 0)
95+
# Validate JSON output
96+
if [[ -f "$findings_file" ]] && [[ -s "$findings_file" ]] && jq empty "$findings_file" 2>/dev/null; then
97+
local issue_count
98+
issue_count=$(jq 'if type == "array" then length elif .findings then (.findings | length) else 0 end' "$findings_file" 2>/dev/null || echo 0)
7999
log_bot "hypatia" "Found $issue_count issues in $repo_name"
80100

81101
# Create/update latest.json symlink
82102
ln -sf "$(basename "$findings_file")" "$repo_findings_dir/latest.json"
83103

84104
echo "$findings_file"
85105
else
86-
log_error "Scan failed for $repo_name"
106+
log_error "Scan produced invalid/empty JSON for $repo_name"
107+
rm -f "$findings_file"
87108
return 1
88109
fi
89110
}

robot-repo-automaton/Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

robot-repo-automaton/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ anyhow = "1"
6464
glob = "0.3"
6565
walkdir = "2"
6666
regex = "1"
67+
dirs = "6.0"
6768

6869
# Async utilities
6970
futures = "0.3"

robot-repo-automaton/src/detector.rs

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,144 @@ impl Detector {
188188
}
189189
}
190190

191-
/// Detect language mismatch issues (e.g., CodeQL configured for wrong languages)
192-
fn detect_language_mismatch(&self, _error_type: &ErrorType) -> Option<DetectedIssue> {
193-
// This would require parsing the workflow file to compare configured vs actual
194-
// For now, return None - implement when we have YAML parsing
195-
None
191+
/// Detect language mismatch issues (e.g., CodeQL configured for wrong languages).
192+
///
193+
/// Checks that CI workflow language matrices match the actual languages
194+
/// detected in the repository. Catches stale CodeQL configs, wrong
195+
/// test runners, and mismatched linter configurations.
196+
fn detect_language_mismatch(&self, error_type: &ErrorType) -> Option<DetectedIssue> {
197+
let detection = &error_type.detection;
198+
199+
// extension_map maps workflow file patterns to expected language keys
200+
// e.g., {"codeql.yml": "javascript-typescript", "rust.yml": "rust"}
201+
if detection.extension_map.is_empty() {
202+
return None;
203+
}
204+
205+
let mut mismatches = Vec::new();
206+
207+
for (workflow_glob, expected_lang) in &detection.extension_map {
208+
// Check if the workflow file exists
209+
let matching_files = self.files_matching(workflow_glob);
210+
211+
if matching_files.is_empty() {
212+
continue;
213+
}
214+
215+
// Check if the expected language is actually present in the repo
216+
let lang_present = self.languages.contains(expected_lang);
217+
218+
if !lang_present {
219+
// Workflow references a language not found in this repo
220+
for file in matching_files {
221+
mismatches.push(file.clone());
222+
}
223+
}
224+
}
225+
226+
// Also check the reverse: repo has languages but no corresponding workflow
227+
let repo_languages: Vec<String> = self.languages.iter().cloned().collect();
228+
let configured_langs: Vec<&String> = detection.extension_map.values().collect();
229+
230+
for lang in &repo_languages {
231+
// Map repo language to CodeQL language name
232+
let codeql_lang = match lang.as_str() {
233+
"javascript" | "typescript" => "javascript-typescript",
234+
"python" => "python",
235+
"go" => "go",
236+
"rust" => "rust",
237+
"java" | "kotlin" => "java-kotlin",
238+
"ruby" => "ruby",
239+
"csharp" => "csharp",
240+
_ => continue,
241+
};
242+
243+
if !configured_langs.iter().any(|c| c.as_str() == codeql_lang) {
244+
// Language exists in repo but isn't in any workflow config
245+
// This is informational, not always a problem
246+
}
247+
}
248+
249+
if mismatches.is_empty() {
250+
None
251+
} else {
252+
Some(DetectedIssue {
253+
error_type_id: error_type.id.clone(),
254+
error_name: error_type.name.clone(),
255+
severity: error_type.severity,
256+
description: format!(
257+
"{} — {} workflow(s) reference languages not found in repo",
258+
error_type.description,
259+
mismatches.len()
260+
),
261+
affected_files: mismatches,
262+
confidence: 0.90,
263+
suggested_fix: format!(
264+
"{:?} {} — update language matrix to match actual repo languages",
265+
error_type.fix.action, error_type.fix.target
266+
),
267+
commit_message: error_type.commit_message.clone(),
268+
})
269+
}
196270
}
197271

198-
/// Detect content match issues
199-
fn detect_content_match(&self, _error_type: &ErrorType) -> Option<DetectedIssue> {
200-
// Would require regex matching in file contents
201-
// Implement when needed
202-
None
272+
/// Detect content match issues by regex scanning file contents.
273+
///
274+
/// Searches files matching the glob patterns in `detection.files` for
275+
/// content matching the regex in `detection.condition`. This enables
276+
/// detection of anti-patterns like `believe_me`, `sorry`, hardcoded
277+
/// secrets, eval() usage, unpinned actions, etc.
278+
fn detect_content_match(&self, error_type: &ErrorType) -> Option<DetectedIssue> {
279+
let detection = &error_type.detection;
280+
281+
// condition holds the regex pattern to search for
282+
let regex_str = detection.condition.as_deref()?;
283+
let re = regex::Regex::new(regex_str).ok()?;
284+
285+
let mut affected = Vec::new();
286+
287+
// files holds glob patterns for which files to search
288+
for file_glob in &detection.files {
289+
let matching = self.files_matching(file_glob);
290+
291+
for file_path in matching {
292+
// Skip binary files (> 1MB or non-UTF8)
293+
if let Ok(metadata) = std::fs::metadata(file_path) {
294+
if metadata.len() > 1_048_576 {
295+
continue;
296+
}
297+
}
298+
299+
if let Ok(content) = std::fs::read_to_string(file_path) {
300+
if re.is_match(&content) {
301+
affected.push(file_path.clone());
302+
}
303+
}
304+
}
305+
}
306+
307+
if affected.is_empty() {
308+
None
309+
} else {
310+
let match_count = affected.len();
311+
312+
Some(DetectedIssue {
313+
error_type_id: error_type.id.clone(),
314+
error_name: error_type.name.clone(),
315+
severity: error_type.severity,
316+
description: format!(
317+
"{} — pattern found in {} file(s)",
318+
error_type.description, match_count
319+
),
320+
affected_files: affected,
321+
confidence: 0.95,
322+
suggested_fix: format!(
323+
"{:?} {}",
324+
error_type.fix.action, error_type.fix.target
325+
),
326+
commit_message: error_type.commit_message.clone(),
327+
})
328+
}
203329
}
204330

205331
/// Run all detections from a catalog

robot-repo-automaton/src/fleet.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use crate::error::{Error, Result};
1414
use gitbot_shared_context::{BotId, Context, Finding, Severity as FleetSeverity};
1515
use std::path::PathBuf;
1616
use tracing::{debug, info};
17+
use chrono;
18+
use dirs;
1719

1820
/// Fleet coordinator for robot-repo-automaton
1921
pub struct FleetCoordinator {
@@ -52,8 +54,13 @@ impl FleetCoordinator {
5254

5355
ctx.complete_bot(BotId::RobotRepoAutomaton, findings_count, errors_count, files_analyzed)
5456
.map_err(|e| Error::Internal(format!("Failed to complete bot: {}", e)))?;
57+
}
5558

56-
// TODO: Persist context to ~/.gitbot-fleet/sessions/
59+
// Persist session to disk (after mutable borrow is released)
60+
if let Some(ref ctx) = self.context {
61+
if let Err(e) = Self::persist_session_static(ctx) {
62+
debug!("Failed to persist session (non-fatal): {}", e);
63+
}
5764
}
5865

5966
self.context = None;
@@ -187,6 +194,38 @@ impl FleetCoordinator {
187194
self.context.as_ref()
188195
}
189196

197+
/// Persist session context to disk for cross-session visibility.
198+
///
199+
/// Writes a JSON summary of the session to ~/.gitbot-fleet/sessions/
200+
/// so other bots and the fleet coordinator can see completed work.
201+
fn persist_session_static(ctx: &Context) -> Result<()> {
202+
let sessions_dir = dirs::home_dir()
203+
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
204+
.join(".gitbot-fleet/sessions");
205+
206+
std::fs::create_dir_all(&sessions_dir)
207+
.map_err(|e| Error::Internal(format!("Failed to create sessions dir: {}", e)))?;
208+
209+
let session_file = sessions_dir.join(format!("{}.json", ctx.session_id));
210+
211+
let session_data = serde_json::json!({
212+
"session_id": ctx.session_id.to_string(),
213+
"repo_name": ctx.repo_name,
214+
"completed_at": chrono::Utc::now().to_rfc3339(),
215+
"findings_count": ctx.findings_from(BotId::RobotRepoAutomaton).len(),
216+
"bot": "robot-repo-automaton",
217+
});
218+
219+
let json = serde_json::to_string_pretty(&session_data)
220+
.map_err(|e| Error::Internal(format!("Failed to serialize session: {}", e)))?;
221+
222+
std::fs::write(&session_file, json)
223+
.map_err(|e| Error::Internal(format!("Failed to write session file: {}", e)))?;
224+
225+
info!("Session persisted to {}", session_file.display());
226+
Ok(())
227+
}
228+
190229
/// Check if connected to fleet
191230
pub fn is_connected(&self) -> bool {
192231
self.context.is_some()

0 commit comments

Comments
 (0)