Skip to content
8 changes: 6 additions & 2 deletions prompts/dev-lead/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,12 @@ Follow the checklist from Phase 1:
Before declaring done:

1. Run the **full** test suite — every test must pass, not just the new ones
2. Run any available lint/format checks (check AGENTS.md or CI config for the commands):
- Common examples: `npm run lint`, `ruff check .`, `golangci-lint run`, `cargo clippy`
2. Run the repo lint check — this is **required** and must pass before you finish:
```bash
bash scripts/dev-lead-lint.sh
```
Comment thread
don-petry marked this conversation as resolved.
Resolved
This checks shellcheck (warning level) on all `scripts/**/*.sh` files and validates
`agents/*.md` frontmatter. Fix any failures it reports before proceeding.
Comment thread
don-petry marked this conversation as resolved.
Resolved
3. **Do not suppress or delete tests to force a pass — fix the implementation instead**
4. If linting requires changes, apply them and re-run tests to confirm nothing broke

Expand Down
28 changes: 28 additions & 0 deletions scripts/dev-lead-fix-issue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ main() {
exit 0
fi

# Run lint before committing to prevent avoidable CI failures.
# LINT_SCRIPT can be overridden in tests; defaults to sibling script.
local _lint_script="${LINT_SCRIPT:-"$(dirname "$0")/dev-lead-lint.sh"}"
local lint_rc=0
local lint_output=""
if [ -f "$_lint_script" ]; then
lint_output=$(bash "$_lint_script" 2>&1) || lint_rc=$?
fi

if [ "$lint_rc" -ne 0 ]; then
echo "::error::Lint check failed — aborting commit to prevent CI failure. Re-apply the dev-lead label after fixing lint errors."
echo "$lint_output"
local _lint_body
_lint_body="<!-- dev-lead-lint-failed -->
## Dev-Lead: Lint Check Failed

The implementation for issue #${ISSUE_NUMBER} contained lint errors. The commit was **aborted** to prevent a CI failure.

\`\`\`
${lint_output}
\`\`\`

**To retry:** fix the lint errors locally (or re-apply the \`dev-lead\` label — the agent will try again)."
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$_lint_body" 2>/dev/null || true
rm -f "$prompt_file"
exit 1
fi

if $has_uncommitted; then
git add -A
git commit -m "feat: implement issue #${ISSUE_NUMBER} — ${ISSUE_TITLE}"
Expand Down
88 changes: 88 additions & 0 deletions scripts/dev-lead-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
# dev-lead-lint.sh — runs repo-specific lint checks before committing.
# Mirrors the checks in lint.yml and ci.yml that are fast and tool-free.
# Run from the repo root. Exits 0 if all checks pass, non-zero on any failure.

fail=0

# ── 1. ShellCheck ─────────────────────────────────────────────────────────────
# Matches the ci.yml ShellCheck job: severity=warning, follow sources (-x),
# recurse scripts/**/*.sh.
if command -v shellcheck >/dev/null 2>&1; then
shopt -s globstar nullglob
shell_files=(scripts/**/*.sh)
shopt -u globstar nullglob
if [ "${#shell_files[@]}" -gt 0 ]; then
echo " [lint] shellcheck on ${#shell_files[@]} file(s)..."
shellcheck --severity=warning -x "${shell_files[@]}" || fail=1
else
echo " [lint] shellcheck: no .sh files found — skipping"
fi
else
echo " [lint] shellcheck not found — skipping (install shellcheck to enable this check)"
fi

# ── 2. Agent profile frontmatter ──────────────────────────────────────────────
# Matches the lint.yml validate-agent-profiles job: checks frontmatter exists,
# is valid YAML, name field matches filename (kebab-case), description and
# tools fields are present.
shopt -s nullglob
agent_files=(agents/*.md)
shopt -u nullglob
if [ "${#agent_files[@]}" -gt 0 ]; then
echo " [lint] validating ${#agent_files[@]} agent profile(s)..."
profile_fail=0
for f in "${agent_files[@]}"; do
Comment thread
don-petry marked this conversation as resolved.
Resolved
name_from_file=$(basename "$f" .md)

if ! awk 'NR==1 && $0!="---"{exit 1} /^---/{n++; if(n==2) exit} END{exit (n<2)}' "$f" 2>/dev/null; then
echo "FAIL: $f — frontmatter missing or does not begin on line 1"
profile_fail=1
continue
fi

frontmatter=$(awk 'BEGIN{found=0} /^---/{found++; if(found==2) exit; next} found==1{print}' "$f")

if command -v python3 >/dev/null 2>&1 && python3 -c "import yaml" >/dev/null 2>&1; then
if ! printf '%s\n' "$frontmatter" | python3 -c "import sys, yaml; yaml.safe_load(sys.stdin)" 2>/dev/null; then
echo "FAIL: $f — invalid YAML in frontmatter"
profile_fail=1
continue
fi
else
echo " [lint] python3 or PyYAML not found — skipping YAML syntax validation for $f"
Comment thread
don-petry marked this conversation as resolved.
Resolved
fi

name_from_front=$(printf '%s\n' "$frontmatter" \
| sed -n 's/^name:[[:space:]]*//p' \
| sed "s/^['\"]//;s/['\"]$//" \
| head -1)

if [ -z "$name_from_front" ]; then
echo "FAIL: $f — missing 'name' field in frontmatter"
profile_fail=1
elif [ "$name_from_front" != "$name_from_file" ]; then
echo "FAIL: $f — name '$name_from_front' does not match filename '$name_from_file'"
profile_fail=1
elif ! printf '%s\n' "$name_from_file" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
echo "FAIL: $f — name '$name_from_file' is not kebab-case"
profile_fail=1
fi

for field in description tools; do
if ! printf '%s\n' "$frontmatter" | grep -q "^${field}:"; then
echo "FAIL: $f — missing '${field}' field in frontmatter"
profile_fail=1
fi
done

[ "$profile_fail" -eq 0 ] && echo "OK: $f"
done
[ "$profile_fail" -eq 0 ] || fail=1
fi

if [ "$fail" -eq 0 ]; then
echo " [lint] all checks passed"
fi
exit "$fail"
173 changes: 173 additions & 0 deletions tests/dev-lead/unit/test_dev_lead_lint.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/usr/bin/env bats
# Unit tests for scripts/dev-lead-lint.sh

SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)"
LINT_SCRIPT="$SCRIPT_DIR/scripts/dev-lead-lint.sh"

setup() {
WORK_DIR="$(mktemp -d)"
export WORK_DIR

# Minimal git repo so git commands work
cd "$WORK_DIR"
git init -q
git config user.email "test@example.com"
git config user.name "Test"
git commit --allow-empty -q -m "init"

# Create minimal scripts/ and agents/ structure
mkdir -p scripts agents
}

teardown() {
cd /
rm -rf "$WORK_DIR"
}

# ── shellcheck tests ──────────────────────────────────────────────────────────

@test "lint: passes when no shell scripts exist" {
run bash "$LINT_SCRIPT"
[ "$status" -eq 0 ]
}

@test "lint: passes on a valid shell script" {
cat > "$WORK_DIR/scripts/good.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
echo "hello"
SH
run bash "$LINT_SCRIPT"
[ "$status" -eq 0 ]
}

@test "lint: fails on a shellcheck-warning-level script" {
# SC2034: variable appears unused (warning level — caught by --severity=warning)
cat > "$WORK_DIR/scripts/bad.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
my_unused_var="never referenced"
echo "done"
SH
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

@test "lint: reports shellcheck errors to stderr" {
# SC2034: variable appears unused (warning level)
cat > "$WORK_DIR/scripts/bad.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
my_unused_var="never referenced"
echo "done"
SH
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
# shellcheck output goes to stdout or stderr; both are captured by bats in $output
[[ "$output" =~ "bad.sh" ]]
}

# ── agent-profile validation tests ───────────────────────────────────────────

@test "lint: passes when no agent profiles exist" {
run bash "$LINT_SCRIPT"
[ "$status" -eq 0 ]
}

@test "lint: passes on a valid agent profile" {
cat > "$WORK_DIR/agents/my-agent.md" <<'MD'
---
name: my-agent
description: A test agent
tools:
- read
---

# My Agent
MD
run bash "$LINT_SCRIPT"
[ "$status" -eq 0 ]
}

@test "lint: fails when agent profile is missing frontmatter" {
cat > "$WORK_DIR/agents/bad-agent.md" <<'MD'
# Bad Agent — no frontmatter
MD
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

@test "lint: fails when agent profile name does not match filename" {
cat > "$WORK_DIR/agents/my-agent.md" <<'MD'
---
name: wrong-name
description: A test agent
tools:
- read
---

# Agent
MD
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

@test "lint: fails when agent profile is missing required fields" {
cat > "$WORK_DIR/agents/my-agent.md" <<'MD'
---
name: my-agent
---

# Agent with no description or tools
MD
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

@test "lint: fails when agent name is not kebab-case" {
cat > "$WORK_DIR/agents/MyAgent.md" <<'MD'
---
name: MyAgent
description: Test
tools:
- read
---
MD
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

# ── combined tests ────────────────────────────────────────────────────────────

@test "lint: fails when both shellcheck and agent-profile fail" {
# SC2034: unused variable (warning level)
cat > "$WORK_DIR/scripts/bad.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
my_unused_var="never referenced"
echo "done"
SH
cat > "$WORK_DIR/agents/bad-agent.md" <<'MD'
# no frontmatter
MD
run bash "$LINT_SCRIPT"
[ "$status" -ne 0 ]
}

@test "lint: exits 0 when all checks pass (shell + agent)" {
cat > "$WORK_DIR/scripts/good.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
echo "ok"
SH
cat > "$WORK_DIR/agents/my-agent.md" <<'MD'
---
name: my-agent
description: Test agent
tools:
- read
---
MD
run bash "$LINT_SCRIPT"
[ "$status" -eq 0 ]
}
Loading
Loading