huskies: merge 980

This commit is contained in:
dave
2026-05-13 14:38:55 +00:00
parent 246f44d8f3
commit 14a39b6205
5 changed files with 167 additions and 6 deletions
+6
View File
@@ -75,6 +75,12 @@ The frontend is embedded into the Rust binary via `rust-embed`. Run `npm run bui
Clippy is zero-tolerance: no warnings allowed. Fix every warning before committing. Clippy is zero-tolerance: no warnings allowed. Fix every warning before committing.
## Pre-commit hook
Every agent worktree has a pre-commit hook installed at `.git-hooks/pre-commit` that runs `script/check` (fmt-check, clippy, cargo check, source-map-check) before every `git commit`. If the hook fails, fix the issues shown and re-run `script/check` to validate.
`git commit --no-verify` bypasses the hook. Do **not** use it. The hook exists to prevent broken commits from reaching the merge gate; bypassing it defeats the purpose and wastes CI cycles.
## File size ## File size
Target a maximum of 800 lines per source file as a soft guide. If a file grows beyond 800 lines, decompose it by concern into smaller modules. Split at natural seams: group related types, functions, or handlers together and move each cohesive group to its own file. This keeps files readable and diffs focused. Target a maximum of 800 lines per source file as a soft guide. If a file grows beyond 800 lines, decompose it by concern into smaller modules. Split at natural seams: group related types, functions, or handlers together and move each cohesive group to its own file. This keeps files readable and diffs focused.
+15 -6
View File
@@ -1,8 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Fast compile-only check + doc-coverage check on changed files. # Pre-commit quality gate: fmt-check, clippy, cargo check, and doc-coverage.
# Use this for rapid iteration feedback while writing code. # Run this before committing to catch fmt drift, clippy warnings, compile
# Catches the doc-coverage gate failures locally instead of waiting for # errors, and missing doc comments without waiting for the full test suite.
# the merge gate to bounce on a single missing `///`.
set -euo pipefail set -euo pipefail
cargo check --tests --workspace
cargo run -p source-map-gen --bin source-map-check -- --worktree . --base master SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "=== Checking Rust formatting ==="
cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --all --check
echo "=== Running cargo clippy ==="
cargo clippy --manifest-path "$PROJECT_ROOT/Cargo.toml" --workspace --all-targets -- -D warnings
echo "=== Checking doc coverage on changed files ==="
cargo run --manifest-path "$PROJECT_ROOT/Cargo.toml" -p source-map-gen --bin source-map-check --quiet -- --worktree "$PROJECT_ROOT" --base master
+7
View File
@@ -195,6 +195,13 @@ pub(super) async fn run_agent_spawn(
} }
}; };
// Step 1.1: Install the pre-commit quality-gate hook in the worktree.
// Non-fatal — if installation fails the agent can still run; the failure
// is logged so the operator can investigate.
if let Err(e) = crate::worktree::install_pre_commit_hook(&wt_info.path) {
slog_error!("[agents] pre-commit hook install failed for {sid}: {e}");
}
// Step 1.5: Update the source map for changed files since master. // Step 1.5: Update the source map for changed files since master.
// Non-blocking — failures are logged but do not gate the spawn. // Non-blocking — failures are logged but do not gate the spawn.
{ {
+138
View File
@@ -89,6 +89,69 @@ pub(crate) async fn run_teardown_commands(
Ok(()) Ok(())
} }
/// Install a pre-commit git hook in an agent worktree.
///
/// Creates `{wt_path}/.git-hooks/pre-commit` containing a shell script that
/// runs `script/check` and aborts the commit if it fails. Configures the
/// worktree's `core.hooksPath` (via `git config --worktree`) so only this
/// worktree uses the per-worktree hooks directory.
pub fn install_pre_commit_hook(wt_path: &Path) -> Result<(), String> {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
let hooks_dir = wt_path.join(".git-hooks");
std::fs::create_dir_all(&hooks_dir).map_err(|e| format!("create .git-hooks dir: {e}"))?;
let hook = "#!/bin/sh\n\
#\n\
# Pre-commit hook installed by huskies.\n\
# Runs script/check (fmt-check, clippy, cargo check, source-map-check)\n\
# before every commit. Aborts if any gate fails.\n\
#\n\
# Emergency bypass: git commit --no-verify (see AGENT.md — avoid this)\n\
\n\
REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\n\
\n\
printf '[pre-commit] Running script/check ...\\n'\n\
OUTPUT=$(\"$REPO_ROOT/script/check\" 2>&1)\n\
STATUS=$?\n\
\n\
if [ \"$STATUS\" -ne 0 ]; then\n\
printf '\\n=== PRE-COMMIT HOOK FAILED ===\\n\\n'\n\
printf '%s\\n' \"$OUTPUT\"\n\
printf '\\nFix the issues above, then re-validate with:\\n'\n\
printf ' script/check\\n'\n\
printf '\\nEmergency bypass (see AGENT.md -- avoid this):\\n'\n\
printf ' git commit --no-verify\\n\\n'\n\
exit 1\n\
fi\n";
let hook_path = hooks_dir.join("pre-commit");
std::fs::write(&hook_path, hook).map_err(|e| format!("write pre-commit hook: {e}"))?;
#[cfg(unix)]
std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("chmod pre-commit hook: {e}"))?;
// Point git at the per-worktree hooks dir so only this worktree uses
// these hooks (not the main repo or other worktrees).
// Requires extensions.worktreeConfig = true in the repository config.
let output = std::process::Command::new("git")
.args(["config", "--worktree", "core.hooksPath", ".git-hooks"])
.current_dir(wt_path)
.output()
.map_err(|e| format!("git config --worktree core.hooksPath: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"git config --worktree core.hooksPath failed: {stderr}"
));
}
Ok(())
}
pub(crate) async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> { pub(crate) async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> {
let cmd = cmd.to_string(); let cmd = cmd.to_string();
let cwd = cwd.to_path_buf(); let cwd = cwd.to_path_buf();
@@ -335,4 +398,79 @@ mod tests {
result.err() result.err()
); );
} }
#[test]
fn install_pre_commit_hook_creates_executable_hook_and_sets_hookspath() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path().join("main-repo");
fs::create_dir_all(&project_root).unwrap();
init_git_repo(&project_root);
// Enable per-worktree config so git config --worktree works.
Command::new("git")
.args(["config", "extensions.worktreeConfig", "true"])
.current_dir(&project_root)
.output()
.expect("enable extensions.worktreeConfig");
// Create a linked worktree to simulate what huskies does for agents.
let wt_path = tmp.path().join("linked-wt");
let out = Command::new("git")
.args([
"worktree",
"add",
wt_path.to_str().unwrap(),
"-b",
"feature/hook-test",
])
.current_dir(&project_root)
.output()
.expect("git worktree add");
assert!(
out.status.success(),
"git worktree add failed: {}",
String::from_utf8_lossy(&out.stderr)
);
install_pre_commit_hook(&wt_path).expect("install_pre_commit_hook must succeed");
// Hook file must exist.
let hook_path = wt_path.join(".git-hooks").join("pre-commit");
assert!(hook_path.exists(), "pre-commit hook must be created");
// Hook must be executable.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(&hook_path).unwrap().permissions().mode();
assert!(mode & 0o111 != 0, "pre-commit hook must be executable");
}
// Hook content must reference script/check and --no-verify.
let content = fs::read_to_string(&hook_path).unwrap();
assert!(
content.contains("script/check"),
"hook must invoke script/check; got:\n{content}"
);
assert!(
content.contains("--no-verify"),
"hook must mention --no-verify bypass; got:\n{content}"
);
// git config core.hooksPath for the worktree must be .git-hooks.
let cfg_out = Command::new("git")
.args(["config", "--worktree", "core.hooksPath"])
.current_dir(&wt_path)
.output()
.expect("git config --worktree core.hooksPath");
assert!(
cfg_out.status.success(),
"git config --worktree core.hooksPath lookup failed"
);
let value = String::from_utf8_lossy(&cfg_out.stdout).trim().to_string();
assert_eq!(
value, ".git-hooks",
"core.hooksPath must be set to .git-hooks"
);
}
} }
+1
View File
@@ -9,6 +9,7 @@ mod sweep;
pub use cleanup::{format_report, run_cleanup}; pub use cleanup::{format_report, run_cleanup};
pub use create::create_worktree; pub use create::create_worktree;
pub use create::install_pre_commit_hook;
pub use git::migrate_slug_paths; pub use git::migrate_slug_paths;
pub use remove::remove_worktree_by_story_id; pub use remove::remove_worktree_by_story_id;
pub use sweep::sweep_orphaned_worktrees; pub use sweep::sweep_orphaned_worktrees;