agentic-factory 0.1.0__tar.gz
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.
- agentic_factory-0.1.0/.claude/hooks/auto-commit.sh +71 -0
- agentic_factory-0.1.0/.claude/hooks/per-edit-fix.sh +51 -0
- agentic_factory-0.1.0/.claude/hooks/quality-gate.sh +101 -0
- agentic_factory-0.1.0/.claude/hooks/session-start.sh +23 -0
- agentic_factory-0.1.0/.claude/hooks/style-guide-check.sh +56 -0
- agentic_factory-0.1.0/.claude/settings.json +47 -0
- agentic_factory-0.1.0/.github/actions/run-agent/action.yaml +190 -0
- agentic_factory-0.1.0/.github/workflows/build-images.yaml +60 -0
- agentic_factory-0.1.0/.github/workflows/ci.yaml +64 -0
- agentic_factory-0.1.0/.github/workflows/pypi-publish.yaml +21 -0
- agentic_factory-0.1.0/.gitignore +49 -0
- agentic_factory-0.1.0/.pre-commit-config.yaml +6 -0
- agentic_factory-0.1.0/CLAUDE.md +43 -0
- agentic_factory-0.1.0/Makefile +27 -0
- agentic_factory-0.1.0/PKG-INFO +77 -0
- agentic_factory-0.1.0/README.md +65 -0
- agentic_factory-0.1.0/images/base/Dockerfile +31 -0
- agentic_factory-0.1.0/images/claude/Dockerfile +7 -0
- agentic_factory-0.1.0/images/opencode/Dockerfile +7 -0
- agentic_factory-0.1.0/images/pi/Dockerfile +7 -0
- agentic_factory-0.1.0/infrastructure/ansible/ansible.cfg +9 -0
- agentic_factory-0.1.0/infrastructure/ansible/inventory/group_vars/all.yaml +13 -0
- agentic_factory-0.1.0/infrastructure/ansible/inventory/hosts.yaml +15 -0
- agentic_factory-0.1.0/infrastructure/ansible/playbooks/provision-hetzner-runner.yaml +41 -0
- agentic_factory-0.1.0/infrastructure/ansible/playbooks/setup-macbook-runner.yaml +11 -0
- agentic_factory-0.1.0/infrastructure/ansible/playbooks/teardown-runner.yaml +26 -0
- agentic_factory-0.1.0/infrastructure/ansible/roles/docker/tasks/main.yaml +57 -0
- agentic_factory-0.1.0/infrastructure/ansible/roles/github-runner/tasks/main.yaml +65 -0
- agentic_factory-0.1.0/infrastructure/ansible/roles/hetzner-vm/tasks/main.yaml +14 -0
- agentic_factory-0.1.0/plans/phase-2-containerized.md +5 -0
- agentic_factory-0.1.0/plans/phase-3-devcontainer.md +5 -0
- agentic_factory-0.1.0/pyproject.toml +91 -0
- agentic_factory-0.1.0/pyrightconfig.json +8 -0
- agentic_factory-0.1.0/src/agentic_factory/__init__.py +3 -0
- agentic_factory-0.1.0/src/agentic_factory/discord.py +115 -0
- agentic_factory-0.1.0/src/agentic_factory/discord_bot.py +118 -0
- agentic_factory-0.1.0/src/agentic_factory/discord_poll.py +155 -0
- agentic_factory-0.1.0/statusline-correct.png +0 -0
- agentic_factory-0.1.0/tests/__init__.py +0 -0
- agentic_factory-0.1.0/tests/test_action/__init__.py +0 -0
- agentic_factory-0.1.0/tests/test_action/test_output_formatting.py +58 -0
- agentic_factory-0.1.0/tests/test_discord.py +93 -0
- agentic_factory-0.1.0/tests/test_discord_bot.py +85 -0
- agentic_factory-0.1.0/tests/test_discord_poll.py +283 -0
- agentic_factory-0.1.0/uv.lock +1426 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Auto-commit hook: commits and pushes changes when Claude stops
|
|
3
|
+
# Uses Claude CLI to generate commit messages
|
|
4
|
+
# Exit code 2 = blocking (Claude will respond to fix issues)
|
|
5
|
+
|
|
6
|
+
HOOK_LOG="${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/hook-debug.log"
|
|
7
|
+
debuglog() {
|
|
8
|
+
echo "[auto-commit] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$HOOK_LOG"
|
|
9
|
+
}
|
|
10
|
+
debuglog "=== HOOK STARTED (pid=$$) ==="
|
|
11
|
+
|
|
12
|
+
# Guard against infinite loop
|
|
13
|
+
if [ -n "$CLAUDE_HOOK_RUNNING" ]; then
|
|
14
|
+
echo "[auto-commit] Skipping (already in hook)" >&2
|
|
15
|
+
debuglog "Skipping (CLAUDE_HOOK_RUNNING already set)"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
export CLAUDE_HOOK_RUNNING=1
|
|
19
|
+
|
|
20
|
+
cd "$CLAUDE_PROJECT_DIR"
|
|
21
|
+
|
|
22
|
+
# Check for uncommitted changes
|
|
23
|
+
if [ -z "$(git status --porcelain)" ]; then
|
|
24
|
+
echo "[auto-commit] No changes to commit" >&2
|
|
25
|
+
debuglog "No changes to commit (exit 0)"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
echo "[auto-commit] Detected uncommitted changes" >&2
|
|
30
|
+
|
|
31
|
+
# Stage all changes
|
|
32
|
+
git add -A
|
|
33
|
+
|
|
34
|
+
# Get diff for commit message generation
|
|
35
|
+
diff_summary=$(git diff --cached --stat)
|
|
36
|
+
changed_files=$(git diff --cached --name-only | head -10 | tr '\n' ', ')
|
|
37
|
+
|
|
38
|
+
echo "[auto-commit] Generating commit message..." >&2
|
|
39
|
+
|
|
40
|
+
# Try Claude for commit message, fallback to simple message
|
|
41
|
+
commit_msg=$(echo "$diff_summary" | claude -p "Generate a concise git commit message (max 72 chars first line) for these changes. Output ONLY the commit message, no quotes or explanation:" --model sonnet 2>/dev/null) || {
|
|
42
|
+
commit_msg="WIP: ${changed_files%, }"
|
|
43
|
+
echo "[auto-commit] Using fallback commit message" >&2
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
echo "[auto-commit] Committing: $commit_msg" >&2
|
|
47
|
+
|
|
48
|
+
# Commit (no GPG sign to avoid timeout in automated contexts)
|
|
49
|
+
commit_output=$(git commit --no-gpg-sign -m "$commit_msg
|
|
50
|
+
|
|
51
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" 2>&1) || {
|
|
52
|
+
echo "" >&2
|
|
53
|
+
echo "[auto-commit] Pre-commit hook failed:" >&2
|
|
54
|
+
echo "$commit_output" >&2
|
|
55
|
+
echo "" >&2
|
|
56
|
+
echo "Please fix the issues above." >&2
|
|
57
|
+
exit 2
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
echo "[auto-commit] Commit successful" >&2
|
|
61
|
+
|
|
62
|
+
echo "[auto-commit] Pushing to origin..." >&2
|
|
63
|
+
push_output=$(git push -u origin HEAD 2>&1) || {
|
|
64
|
+
echo "[auto-commit] Push failed: $push_output" >&2
|
|
65
|
+
echo "[auto-commit] You may need to pull first" >&2
|
|
66
|
+
exit 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
echo "[auto-commit] Push successful" >&2
|
|
70
|
+
debuglog "=== HOOK FINISHED — push successful (exit 0) ==="
|
|
71
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Per-edit hook: runs fast auto-fixers on changed Python files
|
|
3
|
+
# Triggered by PostToolUse on Edit|Write
|
|
4
|
+
# Exit 0 = success (fixes applied silently)
|
|
5
|
+
# Exit 2 = unfixable issues fed back to Claude
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty')
|
|
9
|
+
|
|
10
|
+
# Only process Python files
|
|
11
|
+
if [[ -z "$FILE_PATH" ]] || [[ "$FILE_PATH" != *.py ]]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Verify file exists
|
|
16
|
+
if [[ ! -f "$FILE_PATH" ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
ERRORS=""
|
|
21
|
+
|
|
22
|
+
# 1. Ruff lint with auto-fix (safe fixes only)
|
|
23
|
+
LINT_OUTPUT=$(uv run --no-sync ruff check --fix --quiet "$FILE_PATH" 2>&1)
|
|
24
|
+
LINT_EXIT=$?
|
|
25
|
+
if [ $LINT_EXIT -ne 0 ]; then
|
|
26
|
+
REMAINING=$(uv run --no-sync ruff check --quiet "$FILE_PATH" 2>&1)
|
|
27
|
+
if [ -n "$REMAINING" ]; then
|
|
28
|
+
ERRORS="${ERRORS}LINT (ruff):\n${REMAINING}\n\n"
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 2. Ruff format (always auto-fixes)
|
|
33
|
+
uv run --no-sync ruff format --quiet "$FILE_PATH" 2>&1
|
|
34
|
+
|
|
35
|
+
# 3. Codespell with auto-fix
|
|
36
|
+
SPELL_OUTPUT=$(uv run --no-sync codespell --quiet-level=2 "$FILE_PATH" 2>&1)
|
|
37
|
+
if [ -n "$SPELL_OUTPUT" ]; then
|
|
38
|
+
uv run --no-sync codespell --write-changes --quiet-level=2 "$FILE_PATH" 2>/dev/null
|
|
39
|
+
REMAINING=$(uv run --no-sync codespell --quiet-level=2 "$FILE_PATH" 2>&1)
|
|
40
|
+
if [ -n "$REMAINING" ]; then
|
|
41
|
+
ERRORS="${ERRORS}SPELLING (codespell):\n${REMAINING}\n\n"
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Report unfixable issues back to Claude
|
|
46
|
+
if [ -n "$ERRORS" ]; then
|
|
47
|
+
echo -e "Per-edit check found issues in ${FILE_PATH}:\n${ERRORS}" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Quality gate hook for Claude Code Stop event
|
|
3
|
+
# Fail-fast: stops at the first failing check, outputs its full stderr/stdout.
|
|
4
|
+
# Exit 2 feeds stderr to Claude for automatic fixing.
|
|
5
|
+
|
|
6
|
+
set -o pipefail
|
|
7
|
+
|
|
8
|
+
HOOK_LOG="${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/hook-debug.log"
|
|
9
|
+
debuglog() {
|
|
10
|
+
echo "[quality-gate] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$HOOK_LOG"
|
|
11
|
+
}
|
|
12
|
+
debuglog "=== HOOK STARTED (pid=$$) ==="
|
|
13
|
+
|
|
14
|
+
# Per-tool diagnostic hints for Claude auto-fix.
|
|
15
|
+
# Uses a function instead of associative arrays for bash 3.x compatibility (macOS).
|
|
16
|
+
get_hint() {
|
|
17
|
+
case "$1" in
|
|
18
|
+
pytest) echo "Read the failing test file and the source it tests. Run 'uv run --no-sync pytest path/to/test_file.py::TestClass::test_name -x --tb=long' to see the full traceback. Fix the source code, not the test, unless the test itself is wrong." ;;
|
|
19
|
+
coverage) echo "Run 'uv run --no-sync pytest --cov=src/ --cov-report=term-missing' to see which lines are uncovered. Add tests for the uncovered code paths." ;;
|
|
20
|
+
"ruff check") echo "Run 'uv run --no-sync ruff check src/ tests/ --output-format=full' for detailed explanations. Most issues are auto-fixable with 'uv run --no-sync ruff check --fix'. Read the file at the reported line before editing." ;;
|
|
21
|
+
"ruff format") echo "Run 'uv run --no-sync ruff format src/ tests/' to auto-fix all formatting issues." ;;
|
|
22
|
+
codespell) echo "Fix the typo in the reported file at the reported location. Run 'uv run --no-sync codespell --write-changes FILE' to auto-fix if possible." ;;
|
|
23
|
+
pyright) echo "Read the file at the reported line number. Check type annotations, imports, and function signatures. Run 'uv run --no-sync pyright src/path/to/file.py' to re-check a single file after fixing." ;;
|
|
24
|
+
bandit) echo "Read the flagged code. Common fixes: use 'secrets' module instead of random for security, avoid shell=True in subprocess calls, use parameterized queries for SQL." ;;
|
|
25
|
+
vulture) echo "The reported code is detected as unused (dead code). Read the file to verify it is truly unused. If it is, delete it. If it's used dynamically (e.g. via getattr or as a public API), add it to a vulture whitelist." ;;
|
|
26
|
+
xenon) echo "The reported function has cyclomatic complexity grade D or worse (CC > 15). Read the function and extract helper functions to reduce branching." ;;
|
|
27
|
+
refurb) echo "Run 'uv run --no-sync refurb --explain ERRCODE' to understand the suggested modernization. These are usually simple one-line replacements." ;;
|
|
28
|
+
interrogate) echo "The reported module or function is missing a docstring. Add a one-line docstring to each flagged public function/class/module." ;;
|
|
29
|
+
style-guide) echo "CLI output formatting must follow the style guide: section headings use emoji + click.style(ALL CAPS text, fg=COLOR, bold=True). No ASCII splitter lines." ;;
|
|
30
|
+
*) echo "" ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fail() {
|
|
35
|
+
local name="$1"
|
|
36
|
+
local cmd="$2"
|
|
37
|
+
local output="$3"
|
|
38
|
+
local hint
|
|
39
|
+
hint=$(get_hint "$name")
|
|
40
|
+
|
|
41
|
+
echo "" >&2
|
|
42
|
+
echo "QUALITY GATE FAILED [$name]:" >&2
|
|
43
|
+
echo "Command: $cmd" >&2
|
|
44
|
+
echo "" >&2
|
|
45
|
+
echo "$output" >&2
|
|
46
|
+
echo "" >&2
|
|
47
|
+
if [ -n "$hint" ]; then
|
|
48
|
+
echo "Hint: $hint" >&2
|
|
49
|
+
echo "" >&2
|
|
50
|
+
fi
|
|
51
|
+
echo "ACTION REQUIRED: You MUST fix the issue shown above. Do NOT stop or explain — read the failing file, edit the source code to resolve it, and the quality gate will re-run automatically." >&2
|
|
52
|
+
debuglog "=== FAILED: $name ==="
|
|
53
|
+
exit 2
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
run_check() {
|
|
57
|
+
local name="$1"; shift
|
|
58
|
+
local cmd="$*"
|
|
59
|
+
debuglog "Running $name..."
|
|
60
|
+
OUTPUT=$("$@" 2>&1) || fail "$name" "$cmd" "$OUTPUT"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run_check_nonempty() {
|
|
64
|
+
local name="$1"; shift
|
|
65
|
+
local cmd="$*"
|
|
66
|
+
debuglog "Running $name..."
|
|
67
|
+
OUTPUT=$("$@" 2>&1)
|
|
68
|
+
[ -n "$OUTPUT" ] && fail "$name" "$cmd" "$OUTPUT"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Sync dependencies once upfront
|
|
72
|
+
uv sync --dev --quiet
|
|
73
|
+
|
|
74
|
+
# Checks ordered by speed and likelihood of failure.
|
|
75
|
+
# [check:pytest]
|
|
76
|
+
run_check "pytest" uv run --no-sync pytest -x --tb=short
|
|
77
|
+
# [check:coverage]
|
|
78
|
+
run_check "coverage" uv run --no-sync pytest --cov=src/ --cov-report=term --cov-fail-under=60 -q
|
|
79
|
+
# [check:ruff-lint]
|
|
80
|
+
run_check "ruff check" uv run --no-sync ruff check src/ tests/
|
|
81
|
+
# [check:ruff-format]
|
|
82
|
+
run_check "ruff format" uv run --no-sync ruff format --check src/ tests/
|
|
83
|
+
# [check:codespell]
|
|
84
|
+
run_check_nonempty "codespell" uv run --no-sync codespell src/ tests/
|
|
85
|
+
# [check:pyright]
|
|
86
|
+
run_check "pyright" uv run --no-sync pyright src/
|
|
87
|
+
# [check:bandit]
|
|
88
|
+
run_check "bandit" uv run --no-sync bandit -r src/ -q -ll
|
|
89
|
+
# [check:vulture]
|
|
90
|
+
run_check_nonempty "vulture" uv run --no-sync vulture src/ --min-confidence 80
|
|
91
|
+
# [check:xenon]
|
|
92
|
+
run_check "xenon" uv run --no-sync xenon --max-absolute C --max-modules A --max-average A src/
|
|
93
|
+
# [check:refurb]
|
|
94
|
+
run_check_nonempty "refurb" uv run --no-sync refurb src/ --python-version 3.12
|
|
95
|
+
# [check:interrogate]
|
|
96
|
+
run_check "interrogate" uv run --no-sync interrogate src/ -v --fail-under 50 -e tests/
|
|
97
|
+
# [check:style-guide]
|
|
98
|
+
run_check "style-guide" "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/style-guide-check.sh"
|
|
99
|
+
|
|
100
|
+
debuglog "=== ALL CHECKS PASSED ==="
|
|
101
|
+
exit 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Session start hook: dependency hygiene checks
|
|
3
|
+
# Runs once when a Claude Code session begins
|
|
4
|
+
# Non-blocking — reports issues but doesn't prevent session start
|
|
5
|
+
|
|
6
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
7
|
+
|
|
8
|
+
WARNINGS=""
|
|
9
|
+
|
|
10
|
+
# 1. Dependency hygiene (deptry) — find unused/missing/transitive deps
|
|
11
|
+
DEPTRY_OUTPUT=$(uv run --no-sync deptry . 2>&1)
|
|
12
|
+
DEPTRY_EXIT=$?
|
|
13
|
+
if [ $DEPTRY_EXIT -ne 0 ]; then
|
|
14
|
+
WARNINGS="${WARNINGS}DEPENDENCY ISSUES (deptry):\n${DEPTRY_OUTPUT}\n\n"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [ -n "$WARNINGS" ]; then
|
|
18
|
+
echo -e "Session start checks found issues:\n${WARNINGS}" >&2
|
|
19
|
+
echo "These are non-blocking warnings. Consider fixing them during this session." >&2
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Style guide check for CLI output formatting.
|
|
3
|
+
# Only applies to files using click for CLI output.
|
|
4
|
+
#
|
|
5
|
+
# Rules:
|
|
6
|
+
# 1. No ASCII art splitter lines (===, ---, ***) in click.echo/print calls
|
|
7
|
+
# 2. Section headings must use click.style() with bold=True and a color
|
|
8
|
+
# 3. Section headings should include an emoji
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SRC_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}/src/"
|
|
13
|
+
|
|
14
|
+
# Find all Python files that use click
|
|
15
|
+
CLI_FILES=()
|
|
16
|
+
while IFS= read -r -d '' f; do
|
|
17
|
+
if grep -qE '(import click|from click)' "$f" 2>/dev/null; then
|
|
18
|
+
CLI_FILES+=("$f")
|
|
19
|
+
fi
|
|
20
|
+
done < <(find "$SRC_DIR" -name '*.py' -print0 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
# Exit cleanly if no click-using files found
|
|
23
|
+
[ ${#CLI_FILES[@]} -eq 0 ] && exit 0
|
|
24
|
+
|
|
25
|
+
ERRORS=()
|
|
26
|
+
|
|
27
|
+
for f in "${CLI_FILES[@]}"; do
|
|
28
|
+
[ -f "$f" ] || continue
|
|
29
|
+
basename=$(basename "$f")
|
|
30
|
+
|
|
31
|
+
# Rule 1: No ASCII splitter lines in echo/print calls
|
|
32
|
+
while IFS= read -r match; do
|
|
33
|
+
ERRORS+=("$basename: ASCII splitter line detected — use emoji + click.style(ALL CAPS, bold=True) instead: $match")
|
|
34
|
+
done < <(grep -nE '(echo|print)\(.*"[=\-\*]{3,}' "$f" 2>/dev/null || true)
|
|
35
|
+
|
|
36
|
+
# Rule 2: Section heading echo() calls should use click.style with bold
|
|
37
|
+
while IFS= read -r match; do
|
|
38
|
+
if echo "$match" | grep -q 'click\.style'; then
|
|
39
|
+
continue
|
|
40
|
+
fi
|
|
41
|
+
ERRORS+=("$basename: Unstyled ALL-CAPS heading — wrap with click.style(..., bold=True, fg=COLOR): $match")
|
|
42
|
+
done < <(grep -nE 'click\.echo\("[^"]*[A-Z]{3,}[^"]*"\)' "$f" 2>/dev/null || true)
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [ ${#ERRORS[@]} -gt 0 ]; then
|
|
46
|
+
echo "STYLE GUIDE VIOLATIONS:" >&2
|
|
47
|
+
echo "" >&2
|
|
48
|
+
for err in "${ERRORS[@]}"; do
|
|
49
|
+
echo " - $err" >&2
|
|
50
|
+
done
|
|
51
|
+
echo "" >&2
|
|
52
|
+
echo "Design rules: Section headings must use emoji + click.style(ALL CAPS text, fg=COLOR, bold=True). No ASCII splitter lines (===, ---, ***)." >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": ".claude/hooks/session-start.sh",
|
|
9
|
+
"timeout": 30
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": ".claude/hooks/per-edit-fix.sh",
|
|
21
|
+
"timeout": 30
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"Stop": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": ".claude/hooks/quality-gate.sh",
|
|
32
|
+
"timeout": 120
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": ".claude/hooks/auto-commit.sh",
|
|
41
|
+
"timeout": 60
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
name: "Run Agent"
|
|
2
|
+
description: "Run an AI agent CLI tool with configurable options"
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
tool:
|
|
6
|
+
description: "CLI tool to use: claude-code, opencode, pi-coding-agent"
|
|
7
|
+
required: true
|
|
8
|
+
prompt:
|
|
9
|
+
description: "The task prompt"
|
|
10
|
+
required: true
|
|
11
|
+
system-prompt:
|
|
12
|
+
description: "System prompt / persona"
|
|
13
|
+
required: false
|
|
14
|
+
default: ""
|
|
15
|
+
model:
|
|
16
|
+
description: "Model to use (tool-specific)"
|
|
17
|
+
required: false
|
|
18
|
+
default: ""
|
|
19
|
+
max-turns:
|
|
20
|
+
description: "Max agentic turns"
|
|
21
|
+
required: false
|
|
22
|
+
default: "10"
|
|
23
|
+
allowed-tools:
|
|
24
|
+
description: "Auto-approved tools (comma-separated)"
|
|
25
|
+
required: false
|
|
26
|
+
default: "Read,Glob,Grep,WebSearch,WebFetch"
|
|
27
|
+
timeout-minutes:
|
|
28
|
+
description: "Hard timeout in minutes"
|
|
29
|
+
required: false
|
|
30
|
+
default: "15"
|
|
31
|
+
post-to:
|
|
32
|
+
description: "Where to post results: issue, pr, none"
|
|
33
|
+
required: false
|
|
34
|
+
default: "none"
|
|
35
|
+
post-number:
|
|
36
|
+
description: "Issue/PR number to comment on"
|
|
37
|
+
required: false
|
|
38
|
+
default: ""
|
|
39
|
+
|
|
40
|
+
outputs:
|
|
41
|
+
result:
|
|
42
|
+
description: "Agent output"
|
|
43
|
+
value: ${{ steps.run.outputs.result }}
|
|
44
|
+
|
|
45
|
+
runs:
|
|
46
|
+
using: "composite"
|
|
47
|
+
steps:
|
|
48
|
+
- name: Run agent
|
|
49
|
+
id: run
|
|
50
|
+
shell: bash
|
|
51
|
+
env:
|
|
52
|
+
TOOL: ${{ inputs.tool }}
|
|
53
|
+
PROMPT: ${{ inputs.prompt }}
|
|
54
|
+
SYSTEM_PROMPT: ${{ inputs.system-prompt }}
|
|
55
|
+
MODEL: ${{ inputs.model }}
|
|
56
|
+
MAX_TURNS: ${{ inputs.max-turns }}
|
|
57
|
+
ALLOWED_TOOLS: ${{ inputs.allowed-tools }}
|
|
58
|
+
TIMEOUT_MINUTES: ${{ inputs.timeout-minutes }}
|
|
59
|
+
run: |
|
|
60
|
+
set +e
|
|
61
|
+
OUTPUT_FILE="${RUNNER_TEMP}/agent-output.txt"
|
|
62
|
+
TIMEOUT_SECONDS=$((TIMEOUT_MINUTES * 60))
|
|
63
|
+
|
|
64
|
+
# Build command based on tool
|
|
65
|
+
case "${TOOL}" in
|
|
66
|
+
claude-code)
|
|
67
|
+
CMD="claude"
|
|
68
|
+
ARGS="-p"
|
|
69
|
+
if [ -n "${SYSTEM_PROMPT}" ]; then
|
|
70
|
+
ARGS="${ARGS} --system-prompt \"${SYSTEM_PROMPT}\""
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "${MODEL}" ]; then
|
|
73
|
+
ARGS="${ARGS} --model ${MODEL}"
|
|
74
|
+
fi
|
|
75
|
+
ARGS="${ARGS} --max-turns ${MAX_TURNS}"
|
|
76
|
+
ARGS="${ARGS} --allowedTools ${ALLOWED_TOOLS}"
|
|
77
|
+
;;
|
|
78
|
+
opencode)
|
|
79
|
+
CMD="opencode"
|
|
80
|
+
ARGS="--headless"
|
|
81
|
+
if [ -n "${MODEL}" ]; then
|
|
82
|
+
ARGS="${ARGS} --model ${MODEL}"
|
|
83
|
+
fi
|
|
84
|
+
;;
|
|
85
|
+
pi-coding-agent)
|
|
86
|
+
CMD="pi-coding-agent"
|
|
87
|
+
ARGS=""
|
|
88
|
+
if [ -n "${MODEL}" ]; then
|
|
89
|
+
ARGS="${ARGS} --model ${MODEL}"
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
echo "::error::Unknown tool: ${TOOL}"
|
|
94
|
+
exit 1
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
# Run with timeout
|
|
99
|
+
echo "Running: ${CMD} ${ARGS} \"<prompt>\""
|
|
100
|
+
timeout "${TIMEOUT_SECONDS}" bash -c "${CMD} ${ARGS} \"${PROMPT}\"" > "${OUTPUT_FILE}" 2>&1
|
|
101
|
+
EXIT_CODE=$?
|
|
102
|
+
|
|
103
|
+
if [ ${EXIT_CODE} -eq 124 ]; then
|
|
104
|
+
echo "::warning::Agent timed out after ${TIMEOUT_MINUTES} minutes"
|
|
105
|
+
echo "AGENT TIMED OUT after ${TIMEOUT_MINUTES} minutes." >> "${OUTPUT_FILE}"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Set output
|
|
109
|
+
RESULT=$(cat "${OUTPUT_FILE}")
|
|
110
|
+
{
|
|
111
|
+
echo "result<<AGENT_OUTPUT_EOF"
|
|
112
|
+
echo "${RESULT}"
|
|
113
|
+
echo "AGENT_OUTPUT_EOF"
|
|
114
|
+
} >> "${GITHUB_OUTPUT}"
|
|
115
|
+
|
|
116
|
+
echo "exit_code=${EXIT_CODE}" >> "${GITHUB_OUTPUT}"
|
|
117
|
+
|
|
118
|
+
- name: Post results
|
|
119
|
+
if: inputs.post-to != 'none' && inputs.post-number != ''
|
|
120
|
+
shell: bash
|
|
121
|
+
env:
|
|
122
|
+
POST_TO: ${{ inputs.post-to }}
|
|
123
|
+
POST_NUMBER: ${{ inputs.post-number }}
|
|
124
|
+
RESULT: ${{ steps.run.outputs.result }}
|
|
125
|
+
EXIT_CODE: ${{ steps.run.outputs.exit_code }}
|
|
126
|
+
run: |
|
|
127
|
+
# Determine status emoji
|
|
128
|
+
if [ "${EXIT_CODE}" = "0" ]; then
|
|
129
|
+
STATUS_ICON="✅"
|
|
130
|
+
elif [ "${EXIT_CODE}" = "124" ]; then
|
|
131
|
+
STATUS_ICON="⏱️"
|
|
132
|
+
else
|
|
133
|
+
STATUS_ICON="❌"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
CHAR_COUNT=${#RESULT}
|
|
137
|
+
|
|
138
|
+
# Format body
|
|
139
|
+
if [ ${CHAR_COUNT} -gt 60000 ]; then
|
|
140
|
+
# Truncate very long output
|
|
141
|
+
BODY="${STATUS_ICON} **Agent Result**
|
|
142
|
+
|
|
143
|
+
<details>
|
|
144
|
+
<summary>Output (truncated — ${CHAR_COUNT} chars)</summary>
|
|
145
|
+
|
|
146
|
+
\`\`\`
|
|
147
|
+
${RESULT:0:50000}
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
⚠️ Output truncated. See [full logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
|
151
|
+
</details>"
|
|
152
|
+
elif [ ${CHAR_COUNT} -gt 4000 ]; then
|
|
153
|
+
# Collapsible for medium output
|
|
154
|
+
BODY="${STATUS_ICON} **Agent Result**
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>Output (${CHAR_COUNT} chars)</summary>
|
|
158
|
+
|
|
159
|
+
\`\`\`
|
|
160
|
+
${RESULT}
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
</details>"
|
|
164
|
+
else
|
|
165
|
+
# Short output inline
|
|
166
|
+
BODY="${STATUS_ICON} **Agent Result**
|
|
167
|
+
|
|
168
|
+
\`\`\`
|
|
169
|
+
${RESULT}
|
|
170
|
+
\`\`\`"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Post comment
|
|
174
|
+
if [ "${POST_TO}" = "pr" ]; then
|
|
175
|
+
gh pr comment "${POST_NUMBER}" --body "${BODY}"
|
|
176
|
+
elif [ "${POST_TO}" = "issue" ]; then
|
|
177
|
+
gh issue comment "${POST_NUMBER}" --body "${BODY}"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
- name: Discord notification
|
|
181
|
+
if: always() && env.DISCORD_WEBHOOK_URL != ''
|
|
182
|
+
shell: bash
|
|
183
|
+
env:
|
|
184
|
+
EXIT_CODE: ${{ steps.run.outputs.exit_code }}
|
|
185
|
+
run: |
|
|
186
|
+
if [ "${EXIT_CODE}" = "0" ]; then
|
|
187
|
+
af-discord send --status success "Agent completed successfully for ${{ github.repository }}#${{ inputs.post-number }}" || true
|
|
188
|
+
else
|
|
189
|
+
af-discord send --channel agent-errors --status error "Agent failed (exit ${EXIT_CODE}) for ${{ github.repository }}#${{ inputs.post-number }}" || true
|
|
190
|
+
fi
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Build Container Images
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: ["images/**"]
|
|
7
|
+
workflow_dispatch: {}
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
packages: write
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
REGISTRY: ghcr.io
|
|
15
|
+
IMAGE_PREFIX: ghcr.io/ondrasek/agent-runtime
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build-base:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: docker/login-action@v3
|
|
24
|
+
with:
|
|
25
|
+
registry: ${{ env.REGISTRY }}
|
|
26
|
+
username: ${{ github.actor }}
|
|
27
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
|
|
29
|
+
- uses: docker/build-push-action@v6
|
|
30
|
+
with:
|
|
31
|
+
context: .
|
|
32
|
+
file: images/base/Dockerfile
|
|
33
|
+
push: true
|
|
34
|
+
tags: |
|
|
35
|
+
${{ env.IMAGE_PREFIX }}-base:latest
|
|
36
|
+
${{ env.IMAGE_PREFIX }}-base:${{ github.sha }}
|
|
37
|
+
|
|
38
|
+
build-tools:
|
|
39
|
+
needs: build-base
|
|
40
|
+
strategy:
|
|
41
|
+
matrix:
|
|
42
|
+
tool: [claude, opencode, pi]
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- uses: docker/login-action@v3
|
|
48
|
+
with:
|
|
49
|
+
registry: ${{ env.REGISTRY }}
|
|
50
|
+
username: ${{ github.actor }}
|
|
51
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
52
|
+
|
|
53
|
+
- uses: docker/build-push-action@v6
|
|
54
|
+
with:
|
|
55
|
+
context: .
|
|
56
|
+
file: images/${{ matrix.tool }}/Dockerfile
|
|
57
|
+
push: true
|
|
58
|
+
tags: |
|
|
59
|
+
${{ env.IMAGE_PREFIX }}-${{ matrix.tool }}:latest
|
|
60
|
+
${{ env.IMAGE_PREFIX }}-${{ matrix.tool }}:${{ github.sha }}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: uv sync --dev
|
|
18
|
+
- run: uv run pytest --cov=src/ --cov-fail-under=60 --cov-report=term-missing --cov-report=xml
|
|
19
|
+
- uses: codecov/codecov-action@v5
|
|
20
|
+
with:
|
|
21
|
+
files: coverage.xml
|
|
22
|
+
if: github.event_name == 'pull_request'
|
|
23
|
+
|
|
24
|
+
lint:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: astral-sh/setup-uv@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.12"
|
|
31
|
+
- run: uv sync --dev
|
|
32
|
+
- run: uv run ruff check src/ tests/
|
|
33
|
+
- run: uv run ruff format --check src/ tests/
|
|
34
|
+
- run: uv run codespell src/ tests/
|
|
35
|
+
|
|
36
|
+
typecheck:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: astral-sh/setup-uv@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
- run: uv sync --dev
|
|
44
|
+
- run: uv run pyright src/
|
|
45
|
+
|
|
46
|
+
security:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: astral-sh/setup-uv@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.12"
|
|
53
|
+
- run: uv sync --dev
|
|
54
|
+
- run: uv run bandit -r src/ -q -ll
|
|
55
|
+
|
|
56
|
+
deadcode:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
- uses: astral-sh/setup-uv@v5
|
|
61
|
+
with:
|
|
62
|
+
python-version: "3.12"
|
|
63
|
+
- run: uv sync --dev
|
|
64
|
+
- run: uv run vulture src/ --min-confidence 80
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v4
|
|
19
|
+
|
|
20
|
+
- name: Build and publish
|
|
21
|
+
run: uv build && uv publish
|