openthrottle 0.1.11 → 0.1.14
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/dist/config-cmd.js +125 -0
- package/dist/index.js +15 -0
- package/dist/run.js +140 -0
- package/package.json +4 -1
- package/templates/wake-sandbox.yml +26 -68
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// openthrottle config — View and edit .openthrottle.yml settings.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// openthrottle config Show current config
|
|
6
|
+
// openthrottle config <key> <value> Set a value (dot-notation)
|
|
7
|
+
//
|
|
8
|
+
// Examples:
|
|
9
|
+
// openthrottle config phases.plan.model opus
|
|
10
|
+
// openthrottle config phases.build.timeout 3600
|
|
11
|
+
// openthrottle config limits.task_timeout 7200
|
|
12
|
+
// openthrottle config review.enabled false
|
|
13
|
+
// =============================================================================
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { parse, stringify } from 'yaml';
|
|
17
|
+
const CONFIG_FILE = '.openthrottle.yml';
|
|
18
|
+
function configPath() {
|
|
19
|
+
return join(process.cwd(), CONFIG_FILE);
|
|
20
|
+
}
|
|
21
|
+
function loadYaml() {
|
|
22
|
+
const path = configPath();
|
|
23
|
+
if (!existsSync(path)) {
|
|
24
|
+
console.error(`No ${CONFIG_FILE} found. Run "openthrottle init" first.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
return parse(readFileSync(path, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
function saveYaml(doc) {
|
|
30
|
+
const header = [
|
|
31
|
+
'# openthrottle.yml — project config for Open Throttle (Daytona runtime)',
|
|
32
|
+
'# Edit directly or use: openthrottle config <key> <value>',
|
|
33
|
+
'',
|
|
34
|
+
].join('\n');
|
|
35
|
+
writeFileSync(configPath(), header + stringify(doc));
|
|
36
|
+
}
|
|
37
|
+
/** Get a nested value using dot notation: "phases.plan.model" */
|
|
38
|
+
function getDeep(obj, path) {
|
|
39
|
+
const parts = path.split('.');
|
|
40
|
+
let cur = obj;
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (cur == null || typeof cur !== 'object')
|
|
43
|
+
return undefined;
|
|
44
|
+
cur = cur[part];
|
|
45
|
+
}
|
|
46
|
+
return cur;
|
|
47
|
+
}
|
|
48
|
+
/** Set a nested value using dot notation, creating intermediate objects. */
|
|
49
|
+
function setDeep(obj, path, value) {
|
|
50
|
+
const parts = path.split('.');
|
|
51
|
+
let cur = obj;
|
|
52
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
53
|
+
if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') {
|
|
54
|
+
cur[parts[i]] = {};
|
|
55
|
+
}
|
|
56
|
+
cur = cur[parts[i]];
|
|
57
|
+
}
|
|
58
|
+
cur[parts[parts.length - 1]] = value;
|
|
59
|
+
}
|
|
60
|
+
/** Coerce string values to appropriate types. */
|
|
61
|
+
function coerce(value) {
|
|
62
|
+
if (value === 'true')
|
|
63
|
+
return true;
|
|
64
|
+
if (value === 'false')
|
|
65
|
+
return false;
|
|
66
|
+
const num = Number(value);
|
|
67
|
+
if (!isNaN(num) && value.trim() !== '')
|
|
68
|
+
return num;
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function printConfig(doc, prefix = '', indent = 0) {
|
|
72
|
+
const pad = ' '.repeat(indent);
|
|
73
|
+
for (const [key, val] of Object.entries(doc)) {
|
|
74
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
75
|
+
if (val != null && typeof val === 'object' && !Array.isArray(val)) {
|
|
76
|
+
console.log(`${pad}${key}:`);
|
|
77
|
+
printConfig(val, fullKey, indent + 1);
|
|
78
|
+
}
|
|
79
|
+
else if (Array.isArray(val)) {
|
|
80
|
+
console.log(`${pad}${key}:`);
|
|
81
|
+
for (const item of val) {
|
|
82
|
+
if (typeof item === 'object') {
|
|
83
|
+
console.log(`${pad} -`);
|
|
84
|
+
printConfig(item, fullKey, indent + 2);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(`${pad} - ${item}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(`${pad}${key}: ${val}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export default function configCmd(args) {
|
|
97
|
+
const doc = loadYaml();
|
|
98
|
+
// No args → show config
|
|
99
|
+
if (args.length === 0) {
|
|
100
|
+
printConfig(doc);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// One arg → show specific key
|
|
104
|
+
if (args.length === 1) {
|
|
105
|
+
const val = getDeep(doc, args[0]);
|
|
106
|
+
if (val === undefined) {
|
|
107
|
+
console.error(`Key not found: ${args[0]}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (typeof val === 'object') {
|
|
111
|
+
printConfig(val);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(val);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Two args → set key
|
|
119
|
+
const [key, ...rest] = args;
|
|
120
|
+
const rawValue = rest.join(' ');
|
|
121
|
+
const value = coerce(rawValue);
|
|
122
|
+
setDeep(doc, key, value);
|
|
123
|
+
saveYaml(doc);
|
|
124
|
+
console.log(` ${key}: ${value}`);
|
|
125
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,9 @@ function cmdShip(args) {
|
|
|
127
127
|
'prd-queued', 'prd-running', 'prd-complete', 'prd-failed',
|
|
128
128
|
'needs-review', 'reviewing',
|
|
129
129
|
'bug-queued', 'bug-running', 'bug-complete', 'bug-failed',
|
|
130
|
+
'bug-confirmed', 'bug-unconfirmed',
|
|
131
|
+
'triage-queued', 'triage-running', 'triage-complete',
|
|
132
|
+
'skip-plan',
|
|
130
133
|
];
|
|
131
134
|
for (const label of labels) {
|
|
132
135
|
try {
|
|
@@ -278,6 +281,8 @@ const HELP = `Usage: openthrottle <command>
|
|
|
278
281
|
Commands:
|
|
279
282
|
init Set up Open Throttle in your project
|
|
280
283
|
ship <file.md> [--base <branch>] Create a GitHub issue to trigger a sandbox
|
|
284
|
+
run Run sandbox lifecycle (used by GH Actions)
|
|
285
|
+
config [key] [value] View or edit .openthrottle.yml settings
|
|
281
286
|
status Show running, queued, and completed tasks
|
|
282
287
|
logs Show recent GitHub Actions workflow runs
|
|
283
288
|
|
|
@@ -301,6 +306,16 @@ async function main() {
|
|
|
301
306
|
await init();
|
|
302
307
|
return;
|
|
303
308
|
}
|
|
309
|
+
if (command === 'config') {
|
|
310
|
+
const { default: configCmd } = await import('./config-cmd.js');
|
|
311
|
+
configCmd(args.slice(1));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (command === 'run') {
|
|
315
|
+
const { default: run } = await import('./run.js');
|
|
316
|
+
await run();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
304
319
|
preflight();
|
|
305
320
|
switch (command) {
|
|
306
321
|
case 'ship':
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// openthrottle run — Daytona sandbox lifecycle manager.
|
|
3
|
+
//
|
|
4
|
+
// Creates a sandbox, streams entrypoint logs while the orchestrator
|
|
5
|
+
// works inside, then stops and deletes the sandbox when it finishes.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
import { Daytona } from "@daytonaio/sdk";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function requireEnv(name) {
|
|
12
|
+
const value = process.env[name];
|
|
13
|
+
if (!value) {
|
|
14
|
+
console.error(`error: ${name} is required`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
const PASSTHROUGH_KEYS = [
|
|
20
|
+
"ANTHROPIC_API_KEY",
|
|
21
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
22
|
+
"OPENAI_API_KEY",
|
|
23
|
+
"SUPABASE_ACCESS_TOKEN",
|
|
24
|
+
"TELEGRAM_BOT_TOKEN",
|
|
25
|
+
"TELEGRAM_CHAT_ID",
|
|
26
|
+
"FROM_PR",
|
|
27
|
+
];
|
|
28
|
+
const SKIP_PATTERN = /^(GITHUB|RUNNER|ACTIONS|INPUT|CI|HOME|PATH|SHELL|USER|TERM|PWD|NODE|SNAPSHOT|DAYTONA|LANG|LC_|DOTNET|JAVA|ANDROID|CHROME|DEBIAN|HOMEBREW|GOROOT|PIPX|CONDA|TASK_TIMEOUT)/;
|
|
29
|
+
function buildSandboxEnv(githubToken, githubRepo, workItem, taskType) {
|
|
30
|
+
const env = {
|
|
31
|
+
GITHUB_TOKEN: githubToken,
|
|
32
|
+
GITHUB_REPO: githubRepo,
|
|
33
|
+
WORK_ITEM: workItem,
|
|
34
|
+
TASK_TYPE: taskType,
|
|
35
|
+
};
|
|
36
|
+
for (const key of PASSTHROUGH_KEYS) {
|
|
37
|
+
const value = process.env[key];
|
|
38
|
+
if (value)
|
|
39
|
+
env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
// Project-specific secrets (scaffolder-injected @@ENV_SECRETS@@)
|
|
42
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
43
|
+
if (!value || key in env)
|
|
44
|
+
continue;
|
|
45
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(key))
|
|
46
|
+
continue;
|
|
47
|
+
if (SKIP_PATTERN.test(key))
|
|
48
|
+
continue;
|
|
49
|
+
env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Main (exported for use by CLI index.ts)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
export default async function run() {
|
|
57
|
+
const apiKey = requireEnv("DAYTONA_API_KEY");
|
|
58
|
+
const snapshot = requireEnv("SNAPSHOT");
|
|
59
|
+
const workItem = requireEnv("WORK_ITEM");
|
|
60
|
+
const taskType = requireEnv("TASK_TYPE");
|
|
61
|
+
const githubRepo = process.env.GITHUB_REPO || process.env["github.repository"] || "";
|
|
62
|
+
const githubToken = process.env.GH_PAT || process.env.GITHUB_TOKEN || "";
|
|
63
|
+
const runId = process.env.GITHUB_RUN_ID || String(Date.now());
|
|
64
|
+
const taskTimeout = parseInt(process.env.TASK_TIMEOUT || "7200", 10);
|
|
65
|
+
const daytona = new Daytona({ apiKey });
|
|
66
|
+
let sandbox;
|
|
67
|
+
const sandboxName = `ot-${taskType}-${workItem}-${runId}`;
|
|
68
|
+
const cleanup = async (signal) => {
|
|
69
|
+
if (signal)
|
|
70
|
+
console.log(`\n[run] Received ${signal}`);
|
|
71
|
+
if (!sandbox)
|
|
72
|
+
return;
|
|
73
|
+
const s = sandbox;
|
|
74
|
+
sandbox = undefined;
|
|
75
|
+
try {
|
|
76
|
+
console.log("[run] Stopping sandbox...");
|
|
77
|
+
await s.stop();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// may already be stopped
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
console.log("[run] Deleting sandbox...");
|
|
84
|
+
await s.delete();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error("[run] Delete failed:", e);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
process.once("SIGINT", () => cleanup("SIGINT").then(() => process.exit(130)));
|
|
91
|
+
process.once("SIGTERM", () => cleanup("SIGTERM").then(() => process.exit(143)));
|
|
92
|
+
try {
|
|
93
|
+
console.log(`[run] Creating sandbox: ${sandboxName}`);
|
|
94
|
+
console.log(`[run] Snapshot: ${snapshot}`);
|
|
95
|
+
console.log(`[run] Task: ${taskType} #${workItem}`);
|
|
96
|
+
sandbox = await daytona.create({
|
|
97
|
+
snapshot,
|
|
98
|
+
name: sandboxName,
|
|
99
|
+
envVars: buildSandboxEnv(githubToken, githubRepo, workItem, taskType),
|
|
100
|
+
labels: {
|
|
101
|
+
project: githubRepo.split("/").pop() || "unknown",
|
|
102
|
+
task_type: taskType,
|
|
103
|
+
issue: workItem,
|
|
104
|
+
},
|
|
105
|
+
autoStopInterval: Math.ceil(taskTimeout / 60) + 10,
|
|
106
|
+
autoDeleteInterval: Math.ceil(taskTimeout / 60) + 30,
|
|
107
|
+
});
|
|
108
|
+
console.log(`[run] Sandbox created: ${sandboxName}`);
|
|
109
|
+
console.log(`[run] Streaming logs (timeout: ${taskTimeout}s)...`);
|
|
110
|
+
// Stream entrypoint logs — blocks until entrypoint exits
|
|
111
|
+
const logStreamDone = sandbox.process.getEntrypointLogs((chunk) => process.stdout.write(chunk), (chunk) => process.stderr.write(chunk));
|
|
112
|
+
await Promise.race([
|
|
113
|
+
logStreamDone,
|
|
114
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Task timeout (${taskTimeout}s) exceeded`)), taskTimeout * 1000)),
|
|
115
|
+
]);
|
|
116
|
+
// Get exit code
|
|
117
|
+
let exitCode = 0;
|
|
118
|
+
try {
|
|
119
|
+
const session = await sandbox.process.getEntrypointSession();
|
|
120
|
+
const commands = session?.commands || [];
|
|
121
|
+
if (commands.length > 0) {
|
|
122
|
+
const last = commands[commands.length - 1];
|
|
123
|
+
if (last.exitCode !== undefined && last.exitCode !== null) {
|
|
124
|
+
exitCode = last.exitCode;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// default to 0
|
|
130
|
+
}
|
|
131
|
+
console.log(`[run] Finished (exit code: ${exitCode})`);
|
|
132
|
+
await cleanup();
|
|
133
|
+
process.exit(exitCode);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`[run] Error: ${err?.message ?? err}`);
|
|
137
|
+
await cleanup();
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openthrottle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "CLI for Open Throttle — ship prompts to Daytona sandboxes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
|
+
"test": "echo 'No tests'",
|
|
16
|
+
"lint": "echo 'No linter configured'",
|
|
15
17
|
"prepublishOnly": "npm run build"
|
|
16
18
|
},
|
|
17
19
|
"dependencies": {
|
|
20
|
+
"@daytonaio/sdk": "^0.153.0",
|
|
18
21
|
"prompts": "^2.4.2",
|
|
19
22
|
"yaml": "^2.4.0"
|
|
20
23
|
},
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
# - PR review with changes_requested → review-fix sandbox
|
|
7
7
|
#
|
|
8
8
|
# Each trigger creates a fresh ephemeral sandbox — no polling, no long-lived state.
|
|
9
|
-
#
|
|
9
|
+
# The GH Actions runner stays alive to stream logs and clean up the sandbox.
|
|
10
10
|
#
|
|
11
11
|
# Required repository secrets:
|
|
12
12
|
# DAYTONA_API_KEY — API key from daytona.io
|
|
13
|
+
# GH_PAT — GitHub PAT for sandbox git operations
|
|
13
14
|
# ANTHROPIC_API_KEY — (option a) or
|
|
14
15
|
# CLAUDE_CODE_OAUTH_TOKEN — (option b) for Claude Code auth
|
|
15
16
|
# TELEGRAM_BOT_TOKEN — optional, for notifications
|
|
16
17
|
# TELEGRAM_CHAT_ID — optional, for notifications
|
|
17
|
-
# SUPABASE_ACCESS_TOKEN — optional, for Supabase MCP
|
|
18
18
|
|
|
19
19
|
name: Wake Sandbox
|
|
20
20
|
|
|
@@ -37,8 +37,9 @@ permissions:
|
|
|
37
37
|
|
|
38
38
|
jobs:
|
|
39
39
|
run-task:
|
|
40
|
-
if: ${{ contains(fromJSON('["prd-queued","bug-queued","needs-review","needs-investigation"]'), github.event.label.name) || (github.event.review.state == 'changes_requested') }}
|
|
40
|
+
if: ${{ contains(fromJSON('["prd-queued","bug-queued","needs-review","needs-investigation","triage-queued"]'), github.event.label.name) || (github.event.review.state == 'changes_requested') }}
|
|
41
41
|
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 150
|
|
42
43
|
steps:
|
|
43
44
|
- uses: actions/checkout@v6
|
|
44
45
|
|
|
@@ -62,7 +63,7 @@ jobs:
|
|
|
62
63
|
fi
|
|
63
64
|
echo "item=$WORK_ITEM" >> "$GITHUB_OUTPUT"
|
|
64
65
|
|
|
65
|
-
# Determine task type
|
|
66
|
+
# Determine task type
|
|
66
67
|
TASK_TYPE="prd"
|
|
67
68
|
if [[ "$EVENT_LABEL" == "bug-queued" ]]; then
|
|
68
69
|
TASK_TYPE="bug"
|
|
@@ -70,94 +71,51 @@ jobs:
|
|
|
70
71
|
TASK_TYPE="review"
|
|
71
72
|
elif [[ "$EVENT_LABEL" == "needs-investigation" ]]; then
|
|
72
73
|
TASK_TYPE="investigation"
|
|
74
|
+
elif [[ "$EVENT_LABEL" == "triage-queued" ]]; then
|
|
75
|
+
TASK_TYPE="triage"
|
|
73
76
|
elif [[ "$EVENT_REVIEW_STATE" == "changes_requested" ]]; then
|
|
74
77
|
TASK_TYPE="review-fix"
|
|
75
78
|
fi
|
|
76
79
|
echo "task_type=$TASK_TYPE" >> "$GITHUB_OUTPUT"
|
|
77
80
|
|
|
78
|
-
# For review-fix tasks, pass the PR number so the sandbox can
|
|
79
|
-
# use `claude --from-pr` to resume conversation context
|
|
80
81
|
FROM_PR=""
|
|
81
82
|
if [[ "$TASK_TYPE" == "review-fix" ]]; then
|
|
82
83
|
FROM_PR="${EVENT_PR_NUM}"
|
|
83
84
|
fi
|
|
84
85
|
echo "from_pr=$FROM_PR" >> "$GITHUB_OUTPUT"
|
|
85
86
|
|
|
86
|
-
- name:
|
|
87
|
-
run: curl -Lo daytona "https://download.daytona.io/cli/latest/daytona-linux-amd64" && sudo chmod +x daytona && sudo mv daytona /usr/local/bin/
|
|
88
|
-
|
|
89
|
-
- name: Install yq
|
|
90
|
-
run: sudo snap install yq
|
|
91
|
-
|
|
92
|
-
- name: Validate config
|
|
87
|
+
- name: Read snapshot from config
|
|
93
88
|
id: config
|
|
94
89
|
run: |
|
|
95
|
-
SNAPSHOT=$(
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
SNAPSHOT=$(grep '^snapshot:' .openthrottle.yml 2>/dev/null | awk '{print $2}' || echo "")
|
|
91
|
+
TASK_TIMEOUT=$(grep 'task_timeout:' .openthrottle.yml 2>/dev/null | awk '{print $2}' || echo "7200")
|
|
92
|
+
if [[ -z "$SNAPSHOT" ]]; then
|
|
93
|
+
echo "::error::Missing 'snapshot' in .openthrottle.yml"
|
|
98
94
|
exit 1
|
|
99
95
|
fi
|
|
100
96
|
echo "snapshot=$SNAPSHOT" >> "$GITHUB_OUTPUT"
|
|
97
|
+
echo "task_timeout=$TASK_TIMEOUT" >> "$GITHUB_OUTPUT"
|
|
101
98
|
|
|
102
|
-
- name:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
- name: Setup Node.js
|
|
100
|
+
uses: actions/setup-node@v4
|
|
101
|
+
with:
|
|
102
|
+
node-version: '22'
|
|
106
103
|
|
|
107
|
-
- name:
|
|
104
|
+
- name: Run sandbox lifecycle
|
|
108
105
|
env:
|
|
109
106
|
DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
|
|
110
107
|
SNAPSHOT: ${{ steps.config.outputs.snapshot }}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
echo "Available snapshots:"
|
|
117
|
-
daytona snapshot list 2>/dev/null || echo " (could not list snapshots)"
|
|
118
|
-
exit 1
|
|
119
|
-
fi
|
|
120
|
-
echo "Snapshot verified: $SNAPSHOT"
|
|
121
|
-
|
|
122
|
-
- name: Create and run sandbox
|
|
123
|
-
env:
|
|
124
|
-
DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
|
|
125
|
-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
108
|
+
WORK_ITEM: ${{ steps.work.outputs.item }}
|
|
109
|
+
TASK_TYPE: ${{ steps.work.outputs.task_type }}
|
|
110
|
+
TASK_TIMEOUT: ${{ steps.config.outputs.task_timeout }}
|
|
111
|
+
GH_PAT: ${{ secrets.GH_PAT }}
|
|
112
|
+
GITHUB_REPO: ${{ github.repository }}
|
|
126
113
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
127
114
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
128
115
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
129
116
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
TASK_TYPE: ${{ steps.work.outputs.task_type }}
|
|
117
|
+
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
118
|
+
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
|
133
119
|
FROM_PR: ${{ steps.work.outputs.from_pr }}
|
|
134
120
|
# @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here
|
|
135
|
-
run:
|
|
136
|
-
SANDBOX_NAME="ot-${TASK_TYPE}-${WORK_ITEM}-${GITHUB_RUN_ID}"
|
|
137
|
-
|
|
138
|
-
# Create ephemeral sandbox
|
|
139
|
-
daytona create \
|
|
140
|
-
--snapshot "$SNAPSHOT" \
|
|
141
|
-
--name "$SANDBOX_NAME" \
|
|
142
|
-
--auto-stop 180 \
|
|
143
|
-
--label "project=${{ github.event.repository.name }}" \
|
|
144
|
-
--label "task_type=${TASK_TYPE}" \
|
|
145
|
-
--label "issue=${WORK_ITEM}" \
|
|
146
|
-
--env "GITHUB_TOKEN=${{ secrets.GH_PAT }}" \
|
|
147
|
-
--env "GITHUB_REPO=${{ github.repository }}" \
|
|
148
|
-
--env "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" \
|
|
149
|
-
--env "CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN}" \
|
|
150
|
-
--env "OPENAI_API_KEY=${OPENAI_API_KEY}" \
|
|
151
|
-
--env "SUPABASE_ACCESS_TOKEN=${SUPABASE_ACCESS_TOKEN}" \
|
|
152
|
-
--env "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" \
|
|
153
|
-
--env "TELEGRAM_CHAT_ID=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
|
154
|
-
--env "WORK_ITEM=${WORK_ITEM}" \
|
|
155
|
-
--env "TASK_TYPE=${TASK_TYPE}" \
|
|
156
|
-
--env "FROM_PR=${FROM_PR}" \
|
|
157
|
-
# @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here
|
|
158
|
-
|
|
159
|
-
echo "Sandbox created: $SANDBOX_NAME"
|
|
160
|
-
echo "Task: ${TASK_TYPE} #${WORK_ITEM}"
|
|
161
|
-
if [[ -n "$FROM_PR" ]]; then
|
|
162
|
-
echo "Review-fix mode: sandbox will use 'claude --from-pr $FROM_PR' to resume PR context"
|
|
163
|
-
fi
|
|
121
|
+
run: npx openthrottle@latest run
|