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.
- package/index.mjs +335 -0
- package/package.json +33 -0
- package/templates/docker/Dockerfile +25 -0
- package/templates/docker/agent-lib.sh +223 -0
- package/templates/docker/entrypoint.sh +285 -0
- package/templates/docker/git-hooks/pre-push +15 -0
- package/templates/docker/hooks/auto-format.sh +44 -0
- package/templates/docker/hooks/block-push-to-main.sh +116 -0
- package/templates/docker/hooks/log-commands.sh +48 -0
- package/templates/docker/run-builder.sh +351 -0
- package/templates/docker/run-reviewer.sh +251 -0
- package/templates/docker/task-adapter.sh +123 -0
- package/templates/wake-sandbox.yml +146 -0
|
@@ -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 }}"
|