create-openthrottle 1.0.0

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.
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # task-adapter.sh — Thin abstraction over task management APIs
4
+ #
5
+ # Wraps issue/task lifecycle operations so the runner scripts don't call
6
+ # `gh issue ...` directly. Currently GitHub-only; designed so a Linear (or
7
+ # other) backend can be swapped in later by adding a case branch.
8
+ #
9
+ # PR/code-hosting operations (gh pr ...) are NOT abstracted here — those are
10
+ # always GitHub regardless of task provider.
11
+ #
12
+ # Usage:
13
+ # source /opt/openthrottle/task-adapter.sh # (in snapshot)
14
+ # source ./scripts/task-adapter.sh # (local dev)
15
+ #
16
+ # Requires:
17
+ # GITHUB_REPO — owner/repo
18
+ # GITHUB_TOKEN — auth token (used implicitly by gh)
19
+ #
20
+ # Optional:
21
+ # TASK_PROVIDER — "github" (default). Future: "linear".
22
+ # =============================================================================
23
+
24
+ TASK_PROVIDER="${TASK_PROVIDER:-github}"
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # task_transition ID OLD_STATUS NEW_STATUS
28
+ # Atomically moves a task from one state to another.
29
+ # Removes the old label, adds the new one.
30
+ # ---------------------------------------------------------------------------
31
+ task_transition() {
32
+ local ID="$1" OLD_STATUS="$2" NEW_STATUS="$3"
33
+
34
+ case "$TASK_PROVIDER" in
35
+ github)
36
+ gh issue edit "$ID" --repo "$GITHUB_REPO" \
37
+ --remove-label "$OLD_STATUS" --add-label "$NEW_STATUS" 2>/dev/null || true
38
+ ;;
39
+ *)
40
+ echo "[task-adapter] Unknown provider: ${TASK_PROVIDER}" >&2
41
+ return 1
42
+ ;;
43
+ esac
44
+ }
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # task_close ID
48
+ # Closes a task/issue.
49
+ # ---------------------------------------------------------------------------
50
+ task_close() {
51
+ local ID="$1"
52
+
53
+ case "$TASK_PROVIDER" in
54
+ github)
55
+ gh issue close "$ID" --repo "$GITHUB_REPO" 2>/dev/null || true
56
+ ;;
57
+ *)
58
+ echo "[task-adapter] Unknown provider: ${TASK_PROVIDER}" >&2
59
+ return 1
60
+ ;;
61
+ esac
62
+ }
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # task_comment ID BODY
66
+ # Posts a comment on a task/issue.
67
+ # ---------------------------------------------------------------------------
68
+ task_comment() {
69
+ local ID="$1" BODY="$2"
70
+
71
+ case "$TASK_PROVIDER" in
72
+ github)
73
+ gh issue comment "$ID" --repo "$GITHUB_REPO" --body "$BODY" 2>/dev/null || true
74
+ ;;
75
+ *)
76
+ echo "[task-adapter] Unknown provider: ${TASK_PROVIDER}" >&2
77
+ return 1
78
+ ;;
79
+ esac
80
+ }
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # task_view ID [--json FIELDS] [--jq EXPR]
84
+ # Read task/issue details. Passes through --json and --jq to gh.
85
+ # ---------------------------------------------------------------------------
86
+ task_view() {
87
+ local ID="$1"; shift
88
+
89
+ case "$TASK_PROVIDER" in
90
+ github)
91
+ gh issue view "$ID" --repo "$GITHUB_REPO" "$@"
92
+ ;;
93
+ *)
94
+ echo "[task-adapter] Unknown provider: ${TASK_PROVIDER}" >&2
95
+ return 1
96
+ ;;
97
+ esac
98
+ }
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # task_read_comments ID FILTER_PATTERN
102
+ # Read comments from a task, optionally filtered by a grep pattern.
103
+ # Returns the matching comment bodies.
104
+ # ---------------------------------------------------------------------------
105
+ task_read_comments() {
106
+ local ID="$1" FILTER="${2:-}"
107
+
108
+ case "$TASK_PROVIDER" in
109
+ github)
110
+ if [[ -n "$FILTER" ]]; then
111
+ gh issue view "$ID" --repo "$GITHUB_REPO" --json comments \
112
+ --jq "[.comments[] | select(.body | contains(\"${FILTER}\"))] | last | .body" 2>/dev/null || echo ""
113
+ else
114
+ gh issue view "$ID" --repo "$GITHUB_REPO" --json comments \
115
+ --jq '[.comments[].body] | join("\n\n---\n\n")' 2>/dev/null || echo ""
116
+ fi
117
+ ;;
118
+ *)
119
+ echo "[task-adapter] Unknown provider: ${TASK_PROVIDER}" >&2
120
+ return 1
121
+ ;;
122
+ esac
123
+ }
@@ -0,0 +1,146 @@
1
+ # Wake Daytona sandbox when work arrives on GitHub.
2
+ #
3
+ # Triggers:
4
+ # - Issue labeled prd-queued or bug-queued → builder sandbox
5
+ # - PR labeled needs-review → reviewer sandbox
6
+ # - PR review with changes_requested → review-fix sandbox
7
+ #
8
+ # Each trigger creates a fresh ephemeral sandbox — no polling, no long-lived state.
9
+ # Multiple triggers fire in parallel (one sandbox per task).
10
+ #
11
+ # Required repository secrets:
12
+ # DAYTONA_API_KEY — API key from daytona.io
13
+ # ANTHROPIC_API_KEY — (option a) or
14
+ # CLAUDE_CODE_OAUTH_TOKEN — (option b) for Claude Code auth
15
+ # TELEGRAM_BOT_TOKEN — optional, for notifications
16
+ # TELEGRAM_CHAT_ID — optional, for notifications
17
+ # SUPABASE_ACCESS_TOKEN — optional, for Supabase MCP
18
+
19
+ name: Wake Sandbox
20
+
21
+ on:
22
+ issues:
23
+ types: [labeled]
24
+ pull_request:
25
+ types: [labeled]
26
+ pull_request_review:
27
+ types: [submitted]
28
+
29
+ concurrency:
30
+ group: openthrottle-${{ github.event.issue.number || github.event.pull_request.number }}
31
+ cancel-in-progress: false
32
+
33
+ permissions:
34
+ contents: read
35
+ issues: write
36
+ pull-requests: write
37
+
38
+ jobs:
39
+ run-task:
40
+ if: >-
41
+ contains(fromJSON('["prd-queued", "bug-queued", "needs-review", "needs-investigation"]'), github.event.label.name) ||
42
+ (github.event.review.state == 'changes_requested')
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+
47
+ - name: Resolve work item
48
+ id: work
49
+ env:
50
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51
+ EVENT_LABEL: ${{ github.event.label.name }}
52
+ EVENT_REVIEW_STATE: ${{ github.event.review.state }}
53
+ EVENT_ISSUE_NUM: ${{ github.event.issue.number }}
54
+ EVENT_PR_NUM: ${{ github.event.pull_request.number }}
55
+ run: |
56
+ # Determine work item number
57
+ WORK_ITEM="${EVENT_ISSUE_NUM}"
58
+ if [[ -z "$WORK_ITEM" ]]; then
59
+ WORK_ITEM="${EVENT_PR_NUM}"
60
+ fi
61
+ if [[ -z "$WORK_ITEM" ]]; then
62
+ echo "::error::Could not determine work item number from event payload"
63
+ exit 1
64
+ fi
65
+ echo "item=$WORK_ITEM" >> "$GITHUB_OUTPUT"
66
+
67
+ # Determine task type (using env vars, not inline ${{ }} expressions)
68
+ TASK_TYPE="prd"
69
+ if [[ "$EVENT_LABEL" == "bug-queued" ]]; then
70
+ TASK_TYPE="bug"
71
+ elif [[ "$EVENT_LABEL" == "needs-review" ]]; then
72
+ TASK_TYPE="review"
73
+ elif [[ "$EVENT_LABEL" == "needs-investigation" ]]; then
74
+ TASK_TYPE="investigation"
75
+ elif [[ "$EVENT_REVIEW_STATE" == "changes_requested" ]]; then
76
+ TASK_TYPE="review-fix"
77
+ fi
78
+ echo "task_type=$TASK_TYPE" >> "$GITHUB_OUTPUT"
79
+
80
+ # Extract session ID for review fixes (portable — no grep -oP)
81
+ RESUME_SESSION=""
82
+ if [[ "$TASK_TYPE" == "review-fix" ]]; then
83
+ RESUME_SESSION=$(gh pr view "$EVENT_PR_NUM" --json comments \
84
+ --jq '.comments[] | select(.body | contains("session-id:")) | .body' \
85
+ | sed -n 's/.*session-id: \([^ ]*\).*/\1/p' | tail -1) || true
86
+ fi
87
+ echo "resume_session=$RESUME_SESSION" >> "$GITHUB_OUTPUT"
88
+
89
+ - name: Validate config
90
+ run: |
91
+ SNAPSHOT=$(yq '.snapshot // ""' .openthrottle.yml)
92
+ if [[ -z "$SNAPSHOT" || "$SNAPSHOT" == "null" ]]; then
93
+ echo "::error::Missing 'snapshot' key in .openthrottle.yml — cannot create sandbox"
94
+ exit 1
95
+ fi
96
+ echo "snapshot=$SNAPSHOT" >> "$GITHUB_OUTPUT"
97
+ id: config
98
+
99
+ - name: Activate snapshot (reactivates if idle >2 weeks)
100
+ env:
101
+ DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
102
+ run: |
103
+ daytona snapshot activate "${{ steps.config.outputs.snapshot }}" 2>/dev/null || true
104
+
105
+ - name: Create and run sandbox
106
+ env:
107
+ DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
108
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
110
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
111
+ SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
112
+ run: |
113
+ # Create ephemeral sandbox (capture both stdout and stderr for error reporting)
114
+ OUTPUT=$(daytona create \
115
+ --snapshot "${{ steps.config.outputs.snapshot }}" \
116
+ --auto-delete 0 \
117
+ --auto-stop 60 \
118
+ --cpu 2 --memory 4096 --disk 10 \
119
+ --label project=${{ github.event.repository.name }} \
120
+ --label task_type="${{ steps.work.outputs.task_type }}" \
121
+ --label issue="${{ steps.work.outputs.item }}" \
122
+ --volume openthrottle-${{ github.repository_id }}:/home/daytona/.claude \
123
+ --env GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
124
+ --env GITHUB_REPO=${{ github.repository }} \
125
+ --env ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
126
+ --env CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN} \
127
+ --env SUPABASE_ACCESS_TOKEN=${SUPABASE_ACCESS_TOKEN} \
128
+ --env TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }} \
129
+ --env TELEGRAM_CHAT_ID=${{ secrets.TELEGRAM_CHAT_ID }} \
130
+ --env WORK_ITEM="${{ steps.work.outputs.item }}" \
131
+ --env TASK_TYPE="${{ steps.work.outputs.task_type }}" \
132
+ --env RESUME_SESSION="${{ steps.work.outputs.resume_session }}" \
133
+ 2>&1) || {
134
+ # Redact secrets from error output
135
+ SAFE_OUTPUT=$(echo "$OUTPUT" | sed \
136
+ -e "s/${ANTHROPIC_API_KEY:-___}/[REDACTED]/g" \
137
+ -e "s/${CLAUDE_CODE_OAUTH_TOKEN:-___}/[REDACTED]/g" \
138
+ -e "s/${SUPABASE_ACCESS_TOKEN:-___}/[REDACTED]/g" \
139
+ -e "s/${GH_TOKEN:-___}/[REDACTED]/g")
140
+ echo "::error::Sandbox creation failed: $SAFE_OUTPUT"
141
+ exit 1
142
+ }
143
+ SANDBOX_ID="$OUTPUT"
144
+
145
+ echo "Sandbox created: $SANDBOX_ID"
146
+ echo "Task: ${{ steps.work.outputs.task_type }} #${{ steps.work.outputs.item }}"