agent-control-plane 0.7.0 → 0.7.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
5
  "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
@@ -57,6 +57,9 @@
57
57
  "tools/bin/github-*.sh",
58
58
  "tools/bin/profile-*.sh",
59
59
  "tools/bin/test-smoke.sh",
60
+ "tools/bin/*-adapter.sh",
61
+ "tools/bin/adapter-*.sh",
62
+ "tools/bin/run-with-adapter.sh",
60
63
  "tools/dashboard/",
61
64
  "tools/templates/issue-prompt-template.md",
62
65
  "tools/templates/pr-fix-template.md",
@@ -69,8 +72,7 @@
69
72
  "tools/vendor/codex-quota-manager/scripts"
70
73
  ],
71
74
  "publishConfig": {
72
- "access": "public",
73
- "provenance": true
75
+ "access": "public"
74
76
  },
75
77
  "engines": {
76
78
  "node": ">=18"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # adapter-capabilities.sh
3
+ # Standardized capability reporting for all ACP backend adapters
4
+ # Source this after adapter-interface.sh
5
+
6
+ set -euo pipefail
7
+
8
+ # Default capabilities (override in adapter)
9
+ ADAPTER_CAP_CONTEXT_WINDOW=0
10
+ ADAPTER_CAP_STREAMING=false
11
+ ADAPTER_CAP_TOOLS_SUPPORT=false
12
+ ADAPTER_CAP_LOCAL_MODEL=false
13
+ ADAPTER_CAP_CLOUD_API=false
14
+ ADAPTER_CAP_RESIDENT_MODE=false
15
+ ADAPTER_CAP_JSON_OUTPUT=false
16
+ ADAPTER_CAP_MAX_TIMEOUT=3600
17
+
18
+ # Print adapter capabilities as key=value pairs
19
+ adapter_capabilities() {
20
+ cat <<EOF
21
+ id=${ADAPTER_ID}
22
+ name=${ADAPTER_NAME}
23
+ type=${ADAPTER_TYPE}
24
+ version=${ADAPTER_VERSION}
25
+ model=${ADAPTER_MODEL:-}
26
+ base_url=${ADAPTER_BASE_URL:-}
27
+ context_window=${ADAPTER_CAP_CONTEXT_WINDOW}
28
+ streaming=${ADAPTER_CAP_STREAMING}
29
+ tools_support=${ADAPTER_CAP_TOOLS_SUPPORT}
30
+ local_model=${ADAPTER_CAP_LOCAL_MODEL}
31
+ cloud_api=${ADAPTER_CAP_CLOUD_API}
32
+ resident_mode=${ADAPTER_CAP_RESIDENT_MODE}
33
+ json_output=${ADAPTER_CAP_JSON_OUTPUT}
34
+ max_timeout=${ADAPTER_CAP_MAX_TIMEOUT}
35
+ EOF
36
+ }
37
+
38
+ # Check if adapter supports a specific capability
39
+ # Usage: adapter_supports CAPABILITY_NAME
40
+ adapter_supports() {
41
+ local cap="$(echo "$1" | tr '[:lower:]' '[:upper:]')"
42
+ local value
43
+ case "$cap" in
44
+ STREAMING) value="${ADAPTER_CAP_STREAMING}" ;;
45
+ TOOLS) value="${ADAPTER_CAP_TOOLS_SUPPORT}" ;;
46
+ LOCAL) value="${ADAPTER_CAP_LOCAL_MODEL}" ;;
47
+ CLOUD) value="${ADAPTER_CAP_CLOUD_API}" ;;
48
+ RESIDENT) value="${ADAPTER_CAP_RESIDENT_MODE}" ;;
49
+ JSON) value="${ADAPTER_CAP_JSON_OUTPUT}" ;;
50
+ *)
51
+ echo "UNKNOWN_CAPABILITY: $1"
52
+ return 1
53
+ ;;
54
+ esac
55
+ [[ "$value" == "true" ]] && return 0
56
+ return 1
57
+ }
58
+
59
+ # Validate adapter implements required functions
60
+ adapter_validate_interface() {
61
+ local errors=0
62
+ for func in adapter_info adapter_health_check adapter_run adapter_status; do
63
+ if ! declare -f "$func" >/dev/null 2>&1; then
64
+ echo "ERROR: $func() not implemented in ${ADAPTER_ID} adapter"
65
+ errors=$((errors + 1))
66
+ fi
67
+ done
68
+ [[ $errors -eq 0 ]] && return 0
69
+ return 1
70
+ }
71
+
72
+ # Enhanced health check that also reports capabilities
73
+ adapter_health_check_with_capabilities() {
74
+ local health_output
75
+ if ! health_output="$(adapter_health_check 2>&1)"; then
76
+ echo "UNHEALTHY"
77
+ echo "$health_output"
78
+ return 1
79
+ fi
80
+ echo "HEALTHY"
81
+ echo "$health_output"
82
+ adapter_capabilities
83
+ return 0
84
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # adapter-interface.sh
3
+ # Standard interface for ACP backend adapters
4
+ # All adapters must implement these functions:
5
+ #
6
+ # adapter_info() - Print adapter metadata (id, name, type, version)
7
+ # adapter_health_check() - Check if backend is available (exit 0 = healthy)
8
+ # adapter_run() - Execute a task (params: MODE SESSION WORKTREE PROMPT_FILE)
9
+ # adapter_status() - Get run status (params: RUNS_ROOT SESSION)
10
+ #
11
+ # Usage: source this file in adapter implementations
12
+
13
+ set -euo pipefail
14
+
15
+ # Default adapter metadata (override in adapter script)
16
+ ADAPTER_ID="${ADAPTER_ID:-unknown}"
17
+ ADAPTER_NAME="${ADAPTER_NAME:-Unknown Adapter}"
18
+ ADAPTER_TYPE="${ADAPTER_TYPE:-unknown}" # coding, local-model, cloud-api
19
+ ADAPTER_VERSION="${ADAPTER_VERSION:-0.0.1}"
20
+
21
+ # Print adapter metadata as key=value pairs
22
+ adapter_info() {
23
+ cat <<EOF
24
+ id=${ADAPTER_ID}
25
+ name=${ADAPTER_NAME}
26
+ type=${ADAPTER_TYPE}
27
+ version=${ADAPTER_VERSION}
28
+ model=${ADAPTER_MODEL:-}
29
+ base_url=${ADAPTER_BASE_URL:-}
30
+ EOF
31
+ }
32
+
33
+ # Default health check (override in adapter)
34
+ # Returns: 0 = healthy, 1 = unhealthy
35
+ adapter_health_check() {
36
+ echo "WARN: adapter_health_check() not implemented for ${ADAPTER_ID}"
37
+ return 0
38
+ }
39
+
40
+ # Default run function (MUST override in adapter)
41
+ adapter_run() {
42
+ echo "ERROR: adapter_run() not implemented for ${ADAPTER_ID}"
43
+ return 1
44
+ }
45
+
46
+ # Default status function (override in adapter if needed)
47
+ adapter_status() {
48
+ local runs_root="${1:?usage: adapter_status RUNS_ROOT SESSION}"
49
+ local session="${2:?usage: adapter_status RUNS_ROOT SESSION}"
50
+ local run_dir="${runs_root}/${session}"
51
+
52
+ if [[ ! -d "$run_dir" ]]; then
53
+ echo "NOT_FOUND"
54
+ return 1
55
+ fi
56
+
57
+ if [[ -f "$run_dir/result.env" ]]; then
58
+ source "$run_dir/result.env"
59
+ echo "${OUTCOME:-UNKNOWN}"
60
+ return 0
61
+ fi
62
+
63
+ if [[ -f "$run_dir/runner.env" ]]; then
64
+ source "$run_dir/runner.env"
65
+ echo "${RUNNER_STATE:-RUNNING}"
66
+ return 0
67
+ fi
68
+
69
+ echo "RUNNING"
70
+ return 0
71
+ }
72
+
73
+ # Load adapter implementation
74
+ # Usage: adapter_load IMPLEMENTATION_SCRIPT
75
+ adapter_load() {
76
+ local impl="${1:?usage: adapter_load IMPLEMENTATION_SCRIPT}"
77
+ if [[ ! -f "$impl" ]]; then
78
+ echo "ERROR: Adapter implementation not found: $impl"
79
+ return 1
80
+ fi
81
+ source "$impl"
82
+ }
83
+
84
+ # Validate adapter implements required functions
85
+ adapter_validate() {
86
+ local missing=()
87
+ type adapter_info >/dev/null 2>&1 || missing+=("adapter_info")
88
+ type adapter_health_check >/dev/null 2>&1 || missing+=("adapter_health_check")
89
+ type adapter_run >/dev/null 2>&1 || missing+=("adapter_run")
90
+
91
+ if [[ ${#missing[@]} -gt 0 ]]; then
92
+ echo "ERROR: Adapter ${ADAPTER_ID} missing required functions: ${missing[*]}"
93
+ return 1
94
+ fi
95
+
96
+ return 0
97
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # claude-adapter.sh
3
+ # Adapter implementation for Claude CLI
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="claude"
12
+ ADAPTER_NAME="Claude CLI"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${CLAUDE_MODEL:-sonnet}"
16
+
17
+ # Claude capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_MAX_TIMEOUT=900
22
+
23
+ adapter_info() {
24
+ cat <<EOF
25
+ id=${ADAPTER_ID}
26
+ name=${ADAPTER_NAME}
27
+ type=${ADAPTER_TYPE}
28
+ version=${ADAPTER_VERSION}
29
+ model=${ADAPTER_MODEL}
30
+ EOF
31
+ }
32
+
33
+ adapter_health_check() {
34
+ if ! command -v claude >/dev/null 2>&1; then
35
+ echo "ERROR: claude CLI not found in PATH"
36
+ return 1
37
+ fi
38
+ echo "OK: Claude adapter healthy"
39
+ return 0
40
+ }
41
+
42
+ adapter_run() {
43
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
44
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
45
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
46
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
47
+
48
+ local permission_mode="${CLAUDE_PERMISSION_MODE:-acceptEdits}"
49
+ local timeout_seconds="${CLAUDE_TIMEOUT_SECONDS:-900}"
50
+
51
+ echo "Claude adapter: Running session ${session}"
52
+
53
+ cd "${worktree}" || return 1
54
+
55
+ prompt="$(cat "${prompt_file}")"
56
+
57
+ if ! timeout "${timeout_seconds}" claude \
58
+ --permission-mode "${permission_mode}" \
59
+ --model "${ADAPTER_MODEL}" \
60
+ --print \
61
+ "${prompt}" 2>&1; then
62
+ echo "ERROR: Claude run failed"
63
+ return 1
64
+ fi
65
+
66
+ return 0
67
+ }
68
+
69
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
70
+ adapter_info
71
+ echo "---"
72
+ adapter_health_check
73
+ fi
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # codex-adapter.sh
3
+ # Adapter implementation for Codex (Claude CLI)
4
+ # Implements: adapter-interface.sh
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "${SCRIPT_DIR}/adapter-interface.sh"
10
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
11
+
12
+ # Codex adapter metadata
13
+ ADAPTER_ID="codex"
14
+ ADAPTER_NAME="Codex (Claude CLI)"
15
+ ADAPTER_TYPE="cloud-api"
16
+ ADAPTER_VERSION="1.0.0"
17
+ ADAPTER_MODEL="${CODEX_MODEL:-sonnet}"
18
+ ADAPTER_BASE_URL=""
19
+
20
+ # Codex capabilities
21
+ ADAPTER_CAP_CLOUD_API=true
22
+ ADAPTER_CAP_STREAMING=true
23
+ ADAPTER_CAP_JSON_OUTPUT=true
24
+ ADAPTER_CAP_MAX_TIMEOUT=900
25
+
26
+ # Print adapter info
27
+ adapter_info() {
28
+ cat <<EOF
29
+ id=${ADAPTER_ID}
30
+ name=${ADAPTER_NAME}
31
+ type=${ADAPTER_TYPE}
32
+ version=${ADAPTER_VERSION}
33
+ model=${ADAPTER_MODEL}
34
+ base_url=${ADAPTER_BASE_URL}
35
+ EOF
36
+ }
37
+
38
+ # Health check: verify claude CLI is available
39
+ adapter_health_check() {
40
+ if ! command -v claude >/dev/null 2>&1; then
41
+ echo "ERROR: claude CLI not found in PATH"
42
+ return 1
43
+ fi
44
+
45
+ # Check if API key is set
46
+ if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${OPENROUTER_API_KEY:-}" ]]; then
47
+ echo "WARN: No ANTHROPIC_API_KEY or OPENROUTER_API_KEY found"
48
+ # Don't fail - user might use OAuth
49
+ fi
50
+
51
+ echo "OK: Codex adapter healthy (claude CLI available)"
52
+ return 0
53
+ }
54
+
55
+ # Run a task using codex (claude CLI)
56
+ adapter_run() {
57
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
58
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
59
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
60
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
61
+
62
+ local permission_mode="${CLAUDE_PERMISSION_MODE:-acceptEdits}"
63
+ local timeout_seconds="${CLAUDE_TIMEOUT_SECONDS:-900}"
64
+ local max_attempts="${CLAUDE_MAX_ATTEMPTS:-3}"
65
+
66
+ echo "Codex adapter: Running session ${session} with model ${ADAPTER_MODEL}"
67
+
68
+ # Read the prompt
69
+ local prompt
70
+ prompt="$(cat "${prompt_file}")"
71
+
72
+ # Change to worktree
73
+ cd "${worktree}" || return 1
74
+
75
+ # Run claude with the prompt
76
+ if ! timeout "${timeout_seconds}" claude \
77
+ --permission-mode "${permission_mode}" \
78
+ --model "${ADAPTER_MODEL}" \
79
+ --print \
80
+ "${prompt}" 2>&1; then
81
+ echo "ERROR: Codex run failed or timed out after ${timeout_seconds}s"
82
+ return 1
83
+ fi
84
+
85
+ echo "Codex adapter: Session ${session} completed"
86
+ return 0
87
+ }
88
+
89
+ # Status check
90
+ adapter_status() {
91
+ local runs_root="${1:?usage: adapter_status RUNS_ROOT SESSION}"
92
+ local session="${2:?usage: adapter_status RUNS_ROOT SESSION}"
93
+ local run_dir="${runs_root}/${session}"
94
+
95
+ if [[ ! -d "$run_dir" ]]; then
96
+ echo "NOT_FOUND"
97
+ return 1
98
+ fi
99
+
100
+ # Check for result file
101
+ if [[ -f "$run_dir/result.env" ]]; then
102
+ source "$run_dir/result.env"
103
+ echo "${OUTCOME:-UNKNOWN}"
104
+ return 0
105
+ fi
106
+
107
+ # Check if claude process is running
108
+ if pgrep -f "claude.*${session}" >/dev/null 2>&1; then
109
+ echo "RUNNING"
110
+ return 0
111
+ fi
112
+
113
+ echo "UNKNOWN"
114
+ return 0
115
+ }
116
+
117
+ # Self-register: validate this adapter implements required functions
118
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
119
+ # Running directly - print info
120
+ adapter_info
121
+ echo "---"
122
+ adapter_health_check
123
+ fi
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # kilo-adapter.sh
3
+ # Adapter implementation for Kilo Code
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="kilo"
12
+ ADAPTER_NAME="Kilo Code"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${KILO_MODEL:-anthropic/claude-sonnet-4-20250514}"
16
+
17
+ # Kilo capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_MAX_TIMEOUT=900
22
+
23
+ adapter_info() {
24
+ cat <<EOF
25
+ id=${ADAPTER_ID}
26
+ name=${ADAPTER_NAME}
27
+ type=${ADAPTER_TYPE}
28
+ version=${ADAPTER_VERSION}
29
+ model=${ADAPTER_MODEL}
30
+ EOF
31
+ }
32
+
33
+ adapter_health_check() {
34
+ if ! command -v kilo >/dev/null 2>&1; then
35
+ echo "ERROR: kilo CLI not found in PATH"
36
+ return 1
37
+ fi
38
+
39
+ # Verify kilo can actually run (version check)
40
+ if ! kilo --version >/dev/null 2>&1; then
41
+ echo "ERROR: kilo CLI cannot run (check installation)"
42
+ return 1
43
+ fi
44
+
45
+ local version
46
+ version="$(kilo --version 2>/dev/null || true)"
47
+ if [[ -z "$version" ]]; then
48
+ echo "WARN: Could not detect kilo version"
49
+ else
50
+ echo "INFO: Kilo version: $version"
51
+ fi
52
+
53
+ # Verify model is specified
54
+ if [[ -z "${ADAPTER_MODEL}" ]]; then
55
+ echo "WARN: No model specified for Kilo adapter"
56
+ fi
57
+
58
+ echo "OK: Kilo adapter healthy (model: ${ADAPTER_MODEL})"
59
+ return 0
60
+ }
61
+
62
+ adapter_run() {
63
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
64
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
65
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
66
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
67
+
68
+ # Validate prompt file
69
+ if [[ ! -f "${prompt_file}" ]]; then
70
+ echo "ERROR: Prompt file not found: ${prompt_file}"
71
+ return 1
72
+ fi
73
+ if [[ ! -s "${prompt_file}" ]]; then
74
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
75
+ return 1
76
+ fi
77
+
78
+ local timeout_seconds="${KILO_TIMEOUT_SECONDS:-900}"
79
+
80
+ echo "Kilo adapter: Running session ${session} with model ${ADAPTER_MODEL}"
81
+
82
+ cd "${worktree}" || return 1
83
+
84
+ prompt="$(cat "${prompt_file}")"
85
+
86
+ # Run kilo and capture output
87
+ local output
88
+ if ! output="$(timeout "${timeout_seconds}" kilo --model "${ADAPTER_MODEL}" "${prompt}" 2>&1)"; then
89
+ echo "ERROR: Kilo run failed or timed out after ${timeout_seconds}s"
90
+ return 1
91
+ fi
92
+
93
+ # Validate JSON stream output (kilo outputs JSON events)
94
+ if ! echo "$output" | python3 -c "import sys, json; [json.loads(line) for line in sys.stdin if line.strip()]" 2>/dev/null; then
95
+ echo "WARN: Kilo output is not valid JSON stream"
96
+ else
97
+ echo "INFO: Kilo output validated as JSON stream"
98
+ fi
99
+
100
+ echo "Kilo adapter: Session ${session} completed"
101
+ return 0
102
+ }
103
+
104
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
105
+ adapter_info
106
+ echo "---"
107
+ adapter_health_check
108
+ fi
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # ollama-adapter.sh
3
+ # Adapter implementation for Ollama local models
4
+ # Implements: adapter-interface.sh
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "${SCRIPT_DIR}/adapter-interface.sh"
10
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
11
+
12
+ # Ollama adapter metadata
13
+ ADAPTER_ID="ollama"
14
+ ADAPTER_NAME="Ollama Local Models"
15
+ ADAPTER_TYPE="local-model"
16
+ ADAPTER_VERSION="1.0.0"
17
+ ADAPTER_MODEL="${OLLAMA_MODEL:-qwen2.5-coder:7b}"
18
+ ADAPTER_BASE_URL="${OLLAMA_BASE_URL:-http://localhost:11434}"
19
+
20
+ # Ollama capabilities
21
+ ADAPTER_CAP_LOCAL_MODEL=true
22
+ ADAPTER_CAP_STREAMING=true
23
+ ADAPTER_CAP_TOOLS_SUPPORT=true
24
+ ADAPTER_CAP_CONTEXT_WINDOW=32768 # Default, will be detected dynamically
25
+ ADAPTER_CAP_MAX_TIMEOUT=3600
26
+
27
+ # Print adapter info
28
+ adapter_info() {
29
+ cat <<EOF
30
+ id=${ADAPTER_ID}
31
+ name=${ADAPTER_NAME}
32
+ type=${ADAPTER_TYPE}
33
+ version=${ADAPTER_VERSION}
34
+ model=${ADAPTER_MODEL}
35
+ base_url=${ADAPTER_BASE_URL}
36
+ EOF
37
+ }
38
+
39
+ # Health check: verify ollama is running and model is available
40
+ adapter_health_check() {
41
+ # Check if ollama is running
42
+ if ! curl -sf "${ADAPTER_BASE_URL}/api/tags" >/dev/null 2>&1; then
43
+ echo "ERROR: Ollama not reachable at ${ADAPTER_BASE_URL}"
44
+ return 1
45
+ fi
46
+
47
+ # Check if model is available (try to pull if not)
48
+ if ! ollama list 2>/dev/null | grep -q "${ADAPTER_MODEL}"; then
49
+ echo "WARN: Model ${ADAPTER_MODEL} not found locally. Attempting pull..."
50
+ if ! ollama pull "${ADAPTER_MODEL}" 2>&1; then
51
+ echo "ERROR: Failed to pull model ${ADAPTER_MODEL}"
52
+ return 1
53
+ fi
54
+ fi
55
+
56
+ # Detect context window and update capability dynamically
57
+ local context_window
58
+ # Ollama API returns context_length inside model_info with architecture prefix
59
+ # e.g., qwen2.context_length, llama.context_length, etc.
60
+ if command -v jq &>/dev/null; then
61
+ context_window="$(curl -sf "${ADAPTER_BASE_URL}/api/show" -d "{\"name\":\"${ADAPTER_MODEL}\"}" 2>/dev/null | jq -r '.model_info // {} | to_entries[] | select(.key | endswith("context_length")) | .value' 2>/dev/null | head -1 || true)"
62
+ else
63
+ # Fallback: use grep for common patterns
64
+ context_window="$(curl -sf "${ADAPTER_BASE_URL}/api/show" -d "{\"name\":\"${ADAPTER_MODEL}\"}" 2>/dev/null | grep -o '"[a-z]*\.context_length":[0-9]*' | head -1 | grep -o '[0-9]*$' || true)"
65
+ fi
66
+ if [[ -n "$context_window" ]]; then
67
+ ADAPTER_CAP_CONTEXT_WINDOW="$context_window"
68
+ echo "INFO: Detected context window: $context_window tokens"
69
+ else
70
+ echo "WARN: Could not detect context window from Ollama API"
71
+ fi
72
+
73
+ echo "OK: Ollama healthy, model ${ADAPTER_MODEL} available"
74
+ return 0
75
+ }
76
+
77
+ # Run a task using ollama
78
+ adapter_run() {
79
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
80
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
81
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
82
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
83
+
84
+ # Validate prompt file
85
+ if [[ ! -f "${prompt_file}" ]]; then
86
+ echo "ERROR: Prompt file not found: ${prompt_file}"
87
+ return 1
88
+ fi
89
+ if [[ ! -s "${prompt_file}" ]]; then
90
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
91
+ return 1
92
+ fi
93
+
94
+ local timeout_seconds="${OLLAMA_TIMEOUT_SECONDS:-900}"
95
+
96
+ echo "Ollama adapter: Running session ${session} with model ${ADAPTER_MODEL}"
97
+
98
+ # Read the prompt
99
+ local prompt
100
+ prompt="$(cat "${prompt_file}")"
101
+
102
+ # Run ollama with the prompt
103
+ # Use perl for timeout on macOS (which lacks GNU timeout)
104
+ if command -v timeout >/dev/null 2>&1; then
105
+ if ! timeout "${timeout_seconds}" ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
106
+ echo "ERROR: Ollama run failed or timed out after ${timeout_seconds}s"
107
+ return 1
108
+ fi
109
+ elif command -v perl >/dev/null 2>&1; then
110
+ if ! perl -e "alarm ${timeout_seconds}; exec @ARGV" ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
111
+ echo "ERROR: Ollama run failed or timed out after ${timeout_seconds}s"
112
+ return 1
113
+ fi
114
+ else
115
+ # No timeout available, run without timeout
116
+ if ! ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
117
+ echo "ERROR: Ollama run failed"
118
+ return 1
119
+ fi
120
+ fi
121
+
122
+ echo "Ollama adapter: Session ${session} completed"
123
+ return 0
124
+ }
125
+
126
+ # Status check (override default)
127
+ adapter_status() {
128
+ local runs_root="${1:?usage: adapter_status RUNS_ROOT SESSION}"
129
+ local session="${2:?usage: adapter_status RUNS_ROOT SESSION}"
130
+ local run_dir="${runs_root}/${session}"
131
+
132
+ if [[ ! -d "$run_dir" ]]; then
133
+ echo "NOT_FOUND"
134
+ return 1
135
+ fi
136
+
137
+ # Check for result file
138
+ if [[ -f "$run_dir/result.env" ]]; then
139
+ source "$run_dir/result.env"
140
+ echo "${OUTCOME:-UNKNOWN}"
141
+ return 0
142
+ fi
143
+
144
+ # Check if ollama process is running
145
+ if pgrep -f "ollama run ${ADAPTER_MODEL}" >/dev/null 2>&1; then
146
+ echo "RUNNING"
147
+ return 0
148
+ fi
149
+
150
+ echo "UNKNOWN"
151
+ return 0
152
+ }
153
+
154
+ # Self-register: validate this adapter implements required functions
155
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
156
+ # Running directly - print info
157
+ adapter_info
158
+ echo "---"
159
+ adapter_health_check
160
+ fi
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # openclaw-adapter.sh
3
+ # Adapter implementation for OpenClaw.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="openclaw"
12
+ ADAPTER_NAME="OpenClaw"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${OPENCLAW_MODEL:-openrouter/qwen/qwen3.5-plus:free}"
16
+
17
+ # OpenClaw capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_RESIDENT_MODE=true
22
+ ADAPTER_CAP_MAX_TIMEOUT=900
23
+
24
+ adapter_info() {
25
+ cat <<EOF
26
+ id=${ADAPTER_ID}
27
+ name=${ADAPTER_NAME}
28
+ type=${ADAPTER_TYPE}
29
+ version=${ADAPTER_VERSION}
30
+ model=${ADAPTER_MODEL}
31
+ EOF
32
+ }
33
+
34
+ adapter_health_check() {
35
+ if ! command -v openclaw >/dev/null 2>&1; then
36
+ echo "ERROR: openclaw CLI not found in PATH"
37
+ return 1
38
+ fi
39
+ echo "OK: OpenClaw adapter healthy"
40
+ return 0
41
+ }
42
+
43
+ adapter_run() {
44
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
45
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
46
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
47
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
48
+
49
+ local timeout_seconds="${OPENCLAW_TIMEOUT_SECONDS:-900}"
50
+
51
+ echo "OpenClaw adapter: Running session ${session}"
52
+
53
+ cd "${worktree}" || return 1
54
+
55
+ prompt="$(cat "${prompt_file}")"
56
+
57
+ if ! timeout "${timeout_seconds}" openclaw --model "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
58
+ echo "ERROR: OpenClaw run failed"
59
+ return 1
60
+ fi
61
+
62
+ return 0
63
+ }
64
+
65
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
66
+ adapter_info
67
+ echo "---"
68
+ adapter_health_check
69
+ fi
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # opencode-adapter.sh
3
+ # Adapter implementation for OpenCode (Crush)
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="opencode"
12
+ ADAPTER_NAME="OpenCode (Crush)"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${OPENCODE_MODEL:-anthropic/claude-sonnet-4-20250514}"
16
+
17
+ # OpenCode capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_MAX_TIMEOUT=900
22
+
23
+ adapter_info() {
24
+ cat <<EOF
25
+ id=${ADAPTER_ID}
26
+ name=${ADAPTER_NAME}
27
+ type=${ADAPTER_TYPE}
28
+ version=${ADAPTER_VERSION}
29
+ model=${ADAPTER_MODEL}
30
+ EOF
31
+ }
32
+
33
+ adapter_health_check() {
34
+ if ! command -v crush >/dev/null 2>&1; then
35
+ echo "ERROR: crush CLI not found in PATH"
36
+ return 1
37
+ fi
38
+
39
+ # Verify crush can actually run (version check)
40
+ if ! crush --version >/dev/null 2>&1; then
41
+ echo "ERROR: crush CLI cannot run (check installation)"
42
+ return 1
43
+ fi
44
+
45
+ local version
46
+ version="$(crush --version 2>/dev/null || true)"
47
+ if [[ -z "$version" ]]; then
48
+ echo "WARN: Could not detect crush version"
49
+ else
50
+ echo "INFO: Crush version: $version"
51
+ fi
52
+
53
+ # Verify model is specified
54
+ if [[ -z "${ADAPTER_MODEL}" ]]; then
55
+ echo "WARN: No model specified for OpenCode adapter"
56
+ fi
57
+
58
+ echo "OK: OpenCode adapter healthy (model: ${ADAPTER_MODEL})"
59
+ return 0
60
+ }
61
+
62
+ adapter_run() {
63
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
64
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
65
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
66
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
67
+
68
+ # Validate prompt file
69
+ if [[ ! -f "${prompt_file}" ]]; then
70
+ echo "ERROR: Prompt file not found: ${prompt_file}"
71
+ return 1
72
+ fi
73
+ if [[ ! -s "${prompt_file}" ]]; then
74
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
75
+ return 1
76
+ fi
77
+
78
+ local timeout_seconds="${OPENCODE_TIMEOUT_SECONDS:-900}"
79
+
80
+ echo "OpenCode adapter: Running session ${session} with model ${ADAPTER_MODEL}"
81
+
82
+ cd "${worktree}" || return 1
83
+
84
+ prompt="$(cat "${prompt_file}")"
85
+
86
+ if ! timeout "${timeout_seconds}" crush --model "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
87
+ echo "ERROR: OpenCode run failed"
88
+ return 1
89
+ fi
90
+
91
+ return 0
92
+ }
93
+
94
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
95
+ adapter_info
96
+ echo "---"
97
+ adapter_health_check
98
+ fi
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # pi-adapter.sh
3
+ # Adapter implementation for Pi CLI (OpenRouter)
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="pi"
12
+ ADAPTER_NAME="Pi Coding Agent"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${PI_MODEL:-openrouter/qwen/qwen3.5-plus:free}"
16
+
17
+ # Pi capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_MAX_TIMEOUT=900
22
+
23
+ adapter_info() {
24
+ cat <<EOF
25
+ id=${ADAPTER_ID}
26
+ name=${ADAPTER_NAME}
27
+ type=${ADAPTER_TYPE}
28
+ version=${ADAPTER_VERSION}
29
+ model=${ADAPTER_MODEL}
30
+ EOF
31
+ }
32
+
33
+ adapter_health_check() {
34
+ if ! command -v pi >/dev/null 2>&1; then
35
+ echo "ERROR: pi CLI not found in PATH"
36
+ return 1
37
+ fi
38
+
39
+ if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
40
+ echo "ERROR: OPENROUTER_API_KEY is required for Pi adapter"
41
+ return 1
42
+ fi
43
+
44
+ # Test API connectivity with a simple model list call
45
+ if ! pi models 2>/dev/null | grep -q "."; then
46
+ echo "ERROR: Pi CLI cannot connect to OpenRouter API (check OPENROUTER_API_KEY)"
47
+ return 1
48
+ fi
49
+
50
+ # Verify configured model is available
51
+ if [[ -n "${ADAPTER_MODEL}" ]] && ! pi models 2>/dev/null | grep -q "${ADAPTER_MODEL}"; then
52
+ echo "WARN: Configured model ${ADAPTER_MODEL} not found in available models"
53
+ fi
54
+
55
+ echo "OK: Pi adapter healthy (model: ${ADAPTER_MODEL})"
56
+ return 0
57
+ }
58
+
59
+ adapter_run() {
60
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
61
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
62
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
63
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
64
+
65
+ # Validate prompt file
66
+ if [[ ! -f "${prompt_file}" ]]; then
67
+ echo "ERROR: Prompt file not found: ${prompt_file}"
68
+ return 1
69
+ fi
70
+ if [[ ! -s "${prompt_file}" ]]; then
71
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
72
+ return 1
73
+ fi
74
+
75
+ local timeout_seconds="${PI_TIMEOUT_SECONDS:-900}"
76
+
77
+ echo "Pi adapter: Running session ${session}"
78
+
79
+ cd "${worktree}" || return 1
80
+
81
+ prompt="$(cat "${prompt_file}")"
82
+
83
+ if ! timeout "${timeout_seconds}" pi --model "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
84
+ echo "ERROR: Pi run failed"
85
+ return 1
86
+ fi
87
+
88
+ return 0
89
+ }
90
+
91
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
92
+ adapter_info
93
+ echo "---"
94
+ adapter_health_check
95
+ fi
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # run-with-adapter.sh
3
+ # Generic runner that uses the adapter interface
4
+ # Usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE
5
+
6
+ set -euo pipefail
7
+
8
+ ADAPTER_SCRIPT="${1:?usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE}"
9
+ MODE="${2:?usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE}"
10
+ SESSION="${3:?usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE}"
11
+ WORKTREE="${4:?usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE}"
12
+ PROMPT_FILE="${5:?usage: run-with-adapter.sh ADAPTER_SCRIPT MODE SESSION WORKTREE PROMPT_FILE}"
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+
16
+ # Load adapter interface
17
+ source "${SCRIPT_DIR}/adapter-interface.sh"
18
+
19
+ # Load adapter implementation
20
+ adapter_load "${ADAPTER_SCRIPT}"
21
+
22
+ # Validate adapter
23
+ if ! adapter_validate; then
24
+ echo "ERROR: Adapter validation failed"
25
+ exit 1
26
+ fi
27
+
28
+ # Print adapter info
29
+ echo "=== Using Adapter ==="
30
+ adapter_info
31
+ echo "===================="
32
+
33
+ # Run the task
34
+ adapter_run "${MODE}" "${SESSION}" "${WORKTREE}" "${PROMPT_FILE}"