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.
Files changed (45) hide show
  1. agentic_factory-0.1.0/.claude/hooks/auto-commit.sh +71 -0
  2. agentic_factory-0.1.0/.claude/hooks/per-edit-fix.sh +51 -0
  3. agentic_factory-0.1.0/.claude/hooks/quality-gate.sh +101 -0
  4. agentic_factory-0.1.0/.claude/hooks/session-start.sh +23 -0
  5. agentic_factory-0.1.0/.claude/hooks/style-guide-check.sh +56 -0
  6. agentic_factory-0.1.0/.claude/settings.json +47 -0
  7. agentic_factory-0.1.0/.github/actions/run-agent/action.yaml +190 -0
  8. agentic_factory-0.1.0/.github/workflows/build-images.yaml +60 -0
  9. agentic_factory-0.1.0/.github/workflows/ci.yaml +64 -0
  10. agentic_factory-0.1.0/.github/workflows/pypi-publish.yaml +21 -0
  11. agentic_factory-0.1.0/.gitignore +49 -0
  12. agentic_factory-0.1.0/.pre-commit-config.yaml +6 -0
  13. agentic_factory-0.1.0/CLAUDE.md +43 -0
  14. agentic_factory-0.1.0/Makefile +27 -0
  15. agentic_factory-0.1.0/PKG-INFO +77 -0
  16. agentic_factory-0.1.0/README.md +65 -0
  17. agentic_factory-0.1.0/images/base/Dockerfile +31 -0
  18. agentic_factory-0.1.0/images/claude/Dockerfile +7 -0
  19. agentic_factory-0.1.0/images/opencode/Dockerfile +7 -0
  20. agentic_factory-0.1.0/images/pi/Dockerfile +7 -0
  21. agentic_factory-0.1.0/infrastructure/ansible/ansible.cfg +9 -0
  22. agentic_factory-0.1.0/infrastructure/ansible/inventory/group_vars/all.yaml +13 -0
  23. agentic_factory-0.1.0/infrastructure/ansible/inventory/hosts.yaml +15 -0
  24. agentic_factory-0.1.0/infrastructure/ansible/playbooks/provision-hetzner-runner.yaml +41 -0
  25. agentic_factory-0.1.0/infrastructure/ansible/playbooks/setup-macbook-runner.yaml +11 -0
  26. agentic_factory-0.1.0/infrastructure/ansible/playbooks/teardown-runner.yaml +26 -0
  27. agentic_factory-0.1.0/infrastructure/ansible/roles/docker/tasks/main.yaml +57 -0
  28. agentic_factory-0.1.0/infrastructure/ansible/roles/github-runner/tasks/main.yaml +65 -0
  29. agentic_factory-0.1.0/infrastructure/ansible/roles/hetzner-vm/tasks/main.yaml +14 -0
  30. agentic_factory-0.1.0/plans/phase-2-containerized.md +5 -0
  31. agentic_factory-0.1.0/plans/phase-3-devcontainer.md +5 -0
  32. agentic_factory-0.1.0/pyproject.toml +91 -0
  33. agentic_factory-0.1.0/pyrightconfig.json +8 -0
  34. agentic_factory-0.1.0/src/agentic_factory/__init__.py +3 -0
  35. agentic_factory-0.1.0/src/agentic_factory/discord.py +115 -0
  36. agentic_factory-0.1.0/src/agentic_factory/discord_bot.py +118 -0
  37. agentic_factory-0.1.0/src/agentic_factory/discord_poll.py +155 -0
  38. agentic_factory-0.1.0/statusline-correct.png +0 -0
  39. agentic_factory-0.1.0/tests/__init__.py +0 -0
  40. agentic_factory-0.1.0/tests/test_action/__init__.py +0 -0
  41. agentic_factory-0.1.0/tests/test_action/test_output_formatting.py +58 -0
  42. agentic_factory-0.1.0/tests/test_discord.py +93 -0
  43. agentic_factory-0.1.0/tests/test_discord_bot.py +85 -0
  44. agentic_factory-0.1.0/tests/test_discord_poll.py +283 -0
  45. 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